Skip to content
sobolewski.proAI today, every eveningSubscribe

Once a day. Unsubscribe with one click. Privacy policy

How to build an MCP server in Python, step by step

How to build an MCP server: we write our own tools in Python with SDK 2.0, test them in MCP Inspector and connect them to Claude Code and Claude Desktop.

7 min read

This article in other languages:العربيةDeutschEspañolFrançaisItaliano日本語Polski简体中文

In this article
  1. What will we need?
  2. Step 1: how do I set up the project?
  3. Step 2: how to build an MCP server with a first tool
  4. Step 3: why can’t I use print()?
  5. Step 4: how do I test the server in MCP Inspector?
  6. Step 5: how do I connect the server to Claude Code?
  7. Step 6: how do I add the server to Claude Desktop?
  8. Step 7: how do I run an MCP server over HTTP?
  9. What should you watch out for?
  10. FAQ

Wondering how to build an MCP server? In Python it takes one file and the official MCP Python SDK: we write an ordinary function with type hints and a docstring, add the @mcp.tool() decorator, and the SDK turns it into a tool that Claude, ChatGPT or Cursor can call. The whole server from the documentation’s example is about a dozen lines long.

In this tutorial we write a server from scratch, test it in MCP Inspector and connect it to Claude Code and Claude Desktop. The commands and code come from the official “Build an MCP server” guide, the SDK’s README and the Claude Code documentation.

What will we need?

I’m assuming we already know what MCP is, meaning the host, client, server and tools (if not, start with my tutorial What is MCP). For the work itself we need:

  1. Python 3.11+ (that’s what the current SDK’s README requires),
  2. the uv package manager, which the official guide uses,
  3. Node.js 22.19.0 or newer, if we want to test the server in MCP Inspector,
  4. an MCP host to check the whole thing, such as Claude Code or Claude Desktop.

One thing to know before we start: version 2.0 of the SDK renamed the main class. According to the v2 changelog, the old from mcp.server.fastmcp import FastMCP is now from mcp.server import MCPServer. A lot of tutorials online still show the old version, and the official guide requires SDK 2.0.0 or newer.

Step 1: how do I set up the project?

First we install uv (macOS and Linux):

curl -LsSf https://astral.sh/uv/install.sh | sh

On Windows, in PowerShell:

powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

After installing, we restart the terminal so the uv command is picked up. Then we create the project and install the SDK:

uv init my-server
cd my-server
uv venv
source .venv/bin/activate
uv add "mcp[cli]"
touch server.py

The [cli] extra installs the mcp command (with mcp dev, mcp run and mcp install), which will come in handy for testing (on Windows, activate the environment with .venv\Scripts\activate).

Step 2: how to build an MCP server with a first tool

Into server.py we paste the example from the SDK’s README and add the line that starts the server at the end, as the official guide does:

from mcp.server import MCPServer

mcp = MCPServer("Demo")


@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b


@mcp.resource("greeting://{name}")
def greeting(name: str) -> str:
    """Greet someone by name."""
    return f"Hello, {name}!"


if __name__ == "__main__":
    mcp.run(transport="stdio")

That’s a complete MCP server, with one tool (add) and one resource with a URI template (greeting://{name}). The part I like is that we don’t write any JSON Schema: according to the README, the signature a: int, b: int is the schema, and the docstring becomes the tool description the model reads. The SDK handles request parsing, validation and the protocol.

So docstrings deserve some care. The model picks a tool based on its name and description, which makes “Add two numbers.” the bare minimum. For real tools we describe the arguments too, as in the official weather example:

@mcp.tool()
async def get_forecast(latitude: float, longitude: float) -> str:
    """Get weather forecast for a location.

    Args:
        latitude: Latitude of the location
        longitude: Longitude of the location
    """

(this example uses the API of the US National Weather Service, NWS, so it only works for locations in the US)

Step 3: why can’t I use print()?

This is the most common mistake in a first server. A local server talks to the host over standard input and output (stdio), and the messages are in JSON-RPC format. The documentation puts it bluntly: writing to stdout corrupts the JSON-RPC messages and breaks the server, and stdout is exactly where print() writes by default.

We use the logging module instead, which writes to stderr:

import logging

logger = logging.getLogger(__name__)

logger.info("Processing request")  # goes to stderr

HTTP servers don’t have this problem, because logs on stdout don’t get mixed up with HTTP responses.

Step 4: how do I test the server in MCP Inspector?

MCP Inspector is the official tool for testing servers. With the SDK, one command starts it:

uv run mcp dev server.py

A browser opens with the Inspector. According to the README, calling add with the arguments a=1 and b=2 returns 3. The same screen shows the list of tools and resources and the raw JSON-RPC messages, which helps a lot when something isn’t working.

The Inspector also has a terminal version (--tui) and a scripting mode (--cli). The latter prints, say, the list of tools and exits, so it fits into CI:

npx @modelcontextprotocol/inspector --cli uv run server.py --method tools/list

(check your Node.js version with node --version; the Inspector requires 22.19.0 or newer)

Step 5: how do I connect the server to Claude Code?

We add a local server as a stdio server. Everything after -- is the command Claude Code will run:

claude mcp add --transport stdio demo -- uv --directory /FULL/PATH/my-server run server.py

The path has to be absolute (run pwd in the project directory to get it). claude mcp get demo shows the connection status, and so does the /mcp command inside a Claude Code session. Now we can ask for something like “add 1234 and 5678 with the add tool” and approve the call.

Two things from the Claude Code documentation are useful right away:

  1. the server startup timeout is set with the MCP_TIMEOUT variable, for example MCP_TIMEOUT=10000 claude gives the server 10 seconds,
  2. Claude Code warns when a tool result exceeds 10,000 tokens and by default cuts it off at 25,000 tokens (the MAX_MCP_OUTPUT_TOKENS variable changes the limit), so tools shouldn’t return entire databases.

Step 6: how do I add the server to Claude Desktop?

In Claude Desktop, local servers go into the claude_desktop_config.json configuration file. It lives in ~/Library/Application Support/Claude/ on macOS, in ~/.config/Claude/ on Linux and in %AppData%\Claude\ on Windows (if it isn’t there, we create it). The entry looks like this:

{
  "mcpServers": {
    "demo": {
      "command": "uv",
      "args": [
        "--directory",
        "/FULL/PATH/my-server",
        "run",
        "server.py"
      ]
    }
  }
}

Then we quit the app completely (Cmd+Q on macOS, closing the window isn’t enough) and start it again. The server should show up in the “Connectors” menu under the plus icon. If it doesn’t, the documentation suggests checking the JSON syntax, making sure the path is absolute, and putting the full path to uv in the command field (which uv prints it). The logs are in ~/Library/Logs/Claude (macOS) or ~/.config/Claude/logs/ (Linux), in the files mcp.log and mcp-server-NAME.log.

Step 7: how do I run an MCP server over HTTP?

A stdio server only works on our own computer. To expose it as a remote server, we run it over Streamable HTTP, the transport that the SDK’s README says is the one to deploy in production:

uv run mcp run server.py --transport streamable-http

The server then listens at http://localhost:8000/mcp, and we add it to Claude Code like any other remote server:

claude mcp add --transport http demo-http http://localhost:8000/mcp

For a server like this to work in the Claude web app as a “custom connector”, it has to be reachable from the internet, because Claude connects to it from Anthropic’s cloud. At that point sign-in becomes useful too: the MCP documentation describes server authorization with OAuth 2.1.

What should you watch out for?

First, what the tool lets the model do. An MCP server gives the model real power to act on our computer, so let’s write tools as narrowly as we can: “read a file from this directory” beats “run any command”.

Second, outside data. If a tool fetches content from the web or from email, it can bring prompt injection along with it, meaning hidden instructions for the model. I wrote more about this in AI agent security.

Third, secrets. Pass API keys through environment variables (--env in Claude Code), not in the code. According to the documentation, the Inspector stores tokens in the system keychain, and when there isn’t one (in a container, for example), in an unencrypted file at ~/.mcp-inspector/secrets.json.

Later we can also plug a server like this into our own agent, which I show in How to build an AI agent in Python.

FAQ

What’s the simplest way to build an MCP server?

The simplest server from the SDK’s README is about a dozen lines: an MCPServer object, one function with the @mcp.tool() decorator, and mcp.run(). The SDK does the rest, meaning the argument schema and the protocol handling.

What is FastMCP?

It’s the former name of the main server class in the MCP Python SDK. Version 2.0 renamed it to MCPServer, and the import is from mcp.server import MCPServer.

Can I write an MCP server in a language other than Python?

Yes. The official guide has versions for TypeScript, Java, Kotlin, C#, Ruby, Rust and Go, among others.

Does an MCP server only work with Claude?

No. MCP is an open standard, so the same server will work in any host that supports the protocol, such as ChatGPT, Visual Studio Code or Cursor.

  • AI
  • AI agents
  • Claude Code
  • programming
  • tutorial

AI today, every evening

The day's biggest AI news and new posts from the blog, every evening. Short, with sources.

Once a day. Unsubscribe with one click. Privacy policy