I Built My First MCP Server in 30 Minutes — Here's What Actually Happened
The official MCP quickstart says 15 minutes. It took me 30, and that's only because I'm not counting the 10 minutes I spent staring at a cryptic error message. Here's the full story of every mistake I made.
Lee Li
Independent Developer · MCP Enthusiast
The official MCP quickstart says you can build a server in 15 minutes. It took me 30, and that's only because I'm not counting the 10 minutes I spent staring at a cryptic error message, wondering if I'd broken something fundamental.
I'd just finished reading the MCP spec for mcp-find.org and figured, OK, I understand the theory now — Host, Client, Server, Tools, the whole thing. If you haven't read that yet, I wrote a plain-English explanation of what MCP actually is. Time to build something. How hard can it be?
Pretty hard, as it turns out. Not because the technology is complex — it's actually pretty straightforward — but because the documentation assumes you already know things that you don't know yet. Here's the full story of my first MCP server, including every dumb mistake I made along the way.
Setting up the environment
First thing: you need Python 3.10 or higher. I had Python 3.11 installed on my machine, so I was fine. Ran python3 --version to double-check. 3.11.7. Good.
Except then I ran pip install mcp and got a dependency error. Something about anyio needing a version that conflicted with something else I had installed. I spent about 5 minutes Googling this before I realised I should use a virtual environment. Which, yes, I should have done from the start. I know. Don't judge me.
python3 -m venv mcp-env
source mcp-env/bin/activate
pip install mcp[cli]
That worked. Total time wasted on something I should have done automatically: 5 minutes.
Writing the actual server
The MCP Python SDK has this thing called FastMCP that makes building servers ridiculously simple. I think 20 lines of code is for a working server. Here's what I wrote:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("my-first-server")
@mcp.tool()
def add_numbers(a: int, b: int) -> int:
"Add two numbers together."""
return a + b
@mcp.tool()
def count_words(text: str) -> int:
"Count the number of words in a text string."""
return len(text.split())
if __name__ == "__main__":
mcp.run()
That's it. That's the whole server. Two tools: one adds numbers, one counts words. Nothing fancy. I wanted the simplest possible thing that would actually work.
Now here's the line I got wrong the first time. I originally wrote the count_words function without the type hints:
def count_words(text):
No error. No warning. The server started fine. But when I connected it to Claude, the tool... didn't show up. Took me about 10 minutes to figure out that FastMCP uses the type hints to generate the tool's input schema: no type hints, no schema, no tool. The server was technically running, but Claude couldn't see what it offered.
This is the kind of thing that should be in big red letters in the documentation. It's not.
First attempt at running it
I saved the file as server.py and ran:
python server.py
Nothing happened. No output. No error. The terminal just sat there. I waited maybe 30 seconds, hit Ctrl+C, and tried again—same thing.
This is actually correct behaviour. When you run an MCP server with stdio transport (which is the default), it doesn't print anything — it just waits for input on stdin. The server was running fine. I didn't know what "running" looked like for an stdio server.
This is one of the most confusing aspects of MCP development. You write a server, you start it, and there's absolutely zero feedback that it's working. No "Server started on port 3000" message. No health check URL. Just silence. I wrote about debugging MCP connections in a separate post because there's so much to say about this.
Connecting to Claude Desktop
OK, so the server works. Now I needed to connect it to something. I went with Claude Desktop because it's the most straightforward MCP client.
The config file lives at:
I'm on macOS. I opened the file (it was empty — just {}) and added:
{
"mcpServers": {
"my-first-server": {
"command": "python",
"args": ["/Users/lee/projects/mcp-test/server.py"]
}
}
}
Saved the file. Opened Claude Desktop. Asked, "Can you add 5 and 3?"
Nothing. Claude just answered "5 + 3 = 8" using its own math ability. It didn't use my tool at all—no MCP icon, no tool indicator, nothing.
I spent a solid 5 minutes thinking my config was wrong before I realised: I hadn't restarted Claude Desktop. And by "restart" I don't mean close the window. I quit the application. On macOS, closing the window doesn't quit the app — it keeps running in the background. I had to right-click the dock icon, select Quit, then reopen it.
After the actual restart, I saw a little hammer icon in the Claude chat input area. That's how you know MCP tools are connected. Clicked it, and there were my two tools: add_numbers and count_words.
Asked Claude: "Can you use the add_numbers tool to add 5 and 3?"
Claude responded: "I'll use the add_numbers tool to calculate that." And then showed the result: 8.
I literally pumped my fist. It's such a simple thing — adding two numbers — but seeing Claude actually call my code through MCP felt like magic.
The path problem that almost broke me
But wait. There's more suffering.
I closed my terminal, went to make coffee, came back, opened Claude Desktop, and tried to use the tools again. Broken. The hammer icon was there, but greyed out. The tools weren't loading.
The problem: in my config, I'd used the Python path python, which pointed to my virtual environment's Python only when the virtual environment was activated. Once I closed the terminal, Python was set to the system Python, and the mcp package wasn't installed.
The fix: use the absolute path to the virtual environment's Python:
"mcpServers": {
"my-first-server": {
"command": "/Users/lee/projects/mcp-test/mcp-env/bin/python",
"args": ["/Users/lee/projects/mcp-test/server.py"]
}
}
This is another thing the docs should scream at you about. Always use absolute paths in your MCP config. No ~, no relative paths, no relying on your PATH variable. Absolute paths for the command AND the script.
After this fix, the tools worked consistently, even after restarting my machine.
Adding a third tool (and discovering hot reload doesn't exist)
Feeling confident, I added a third tool:
@mcp.tool()
def reverse_text(text: str) -> str:
"Reverse a text string."""
return text[::-1]
Saved the file. Went to Claude Desktop. Only two tools are showing. The new one wasn't there.
Turns out, there's no hot reload. If you change your server code, you have to restart Claude Desktop again. The full quit-and-reopen cycle. Every time.
I've since learned that some people use the MCP Inspector tool for development, which lets you test your server without going through Claude Desktop's restart cycle. Wish I'd known that from the start. It would have saved me about 15 restarts that first afternoon.
Here's the command if you want to skip the Claude Desktop pain during development:
npx @modelcontextprotocol/inspector python server.py
This opens a web UI where you can see your tools, call them manually, and check the responses. Much faster than the Claude Desktop restart loop.
Where I went wrong (a summary for the impatient)
Look, I could have saved myself a lot of time. Here's the condensed version of every mistake:
Forgot to use a virtual environment. Cost me 5 minutes. Always use one.
No type hints on function parameters. Cost me 10 minutes of confused debugging. FastMCP needs them to generate the tool schema. Without them, your tool is invisible.
Didn't know what a running stdio server looks like. Cost me 2 minutes of confusion. It looks like nothing. That's normal.
Didn't actually quit Claude Desktop. Cost me 5 minutes. Close the window, ≠ quit the app on macOS. Check your dock.
Used a relative Python path in the config. Cost me 15 minutes of "why did it stop working?" Always use absolute paths for everything.
Expected hot reload. Cost me multiple unnecessary restarts until I found the Inspector tool.
What I'd do differently
If I were starting over today, here's my exact sequence:
That last one — the log file — I didn't discover until days later. Claude Desktop logs MCP communication to a file, and it would have saved me so much debugging time if I'd known about it from the start.
I'm now building mcp-find.org to catalogue and test MCP servers, and the number of repos I've looked at that have the same problems — missing type hints, broken path configs, no error handling — is staggering. I've tested a few dozen servers at this point, and the pattern is always the same: the README says "works in 5 minutes," and the reality is 20 minutes of debugging.
Total time for my first server: about 30 minutes. Total time spent actually writing code: about 5 minutes. Total time spent confused: about 25 minutes.
That ratio gets better, by the way. My second server took 10 minutes, and most of that was writing the actual logic. Once you know the gotchas, MCP development is genuinely fast. It's just that first time that's painful.
Lee Li
Independent Developer · MCP Enthusiast
Building and breaking things with AI tools since 2023. MCP Find started as a personal project to track the rapidly evolving MCP ecosystem. Based in Hong Kong.