How to Build an MCP Server in Python
Install the official MCP Python SDK, write a server in about 15 lines, test it in MCP Inspector, then add it to Claude Code or Claude Desktop.
You build a Python MCP server with the official mcp package (version 2.3.0 on PyPI in November 2026). Its MCPServer class turns plain type-hinted functions into tools: a: int, b: int is the input schema and the docstring is the description, so you write no JSON Schema and no protocol code. You need Python 3.11 or later and uv or pip.
In SDK v1 the same class was called FastMCP (from mcp.server.fastmcp import FastMCP). v2 renamed it to MCPServer; the decorators work the same way. FastMCP is also the name of a separate framework from Prefect, on PyPI as fastmcp, with its own docs at gofastmcp.com. This guide follows the official SDK and the modelcontextprotocol.io tutorial.
Good for
- Python developers who want Claude to call their own functions or APIs
- Teams wrapping an internal service as MCP tools
- Anyone moving a v1 FastMCP server to the v2 SDK
Build, test and connect a Python MCP server
- 1
Create a project and install the SDK
You need Python 3.11 or later. Install uv on macOS or Linux with
curl -LsSf https://astral.sh/uv/install.sh | shand open a new terminal. Then run:uv init my-servercd my-serveruv add "mcp[cli]"
With pip instead:
pip install "mcp[cli]". Thecliextra adds themcpcommand (mcp dev,mcp run,mcp install). The full block is in the Code section. - 2
Write server.py
Copy
server.pyfrom the Code section below.MCPServer("Demo")creates the server.@mcp.tool()makesadda tool and@mcp.resource("greeting://{name}")makesgreetinga resource template.The SDK reads the name, description and argument schema from the function itself. Keep the parentheses on the decorators:
@mcp.toolwithout()fails at import.Tip: Import from mcp.server, not mcp: there is no from mcp import MCPServer.
- 3
Test it in MCP Inspector
Run
uv run mcp dev server.py. It starts your server and the MCP Inspector. Open the URL it prints, go to Tools, pickadd, entera=1andb=2and run it: the result is3.The Resources tab lists
greetingunder Resource Templates. Give itWorldand read it to getHello, World!. - 4
Check the stdio launch command
The last two lines of
server.pycallmcp.run(), which serves over stdio, the transport Claude Code and Claude Desktop launch. Run the launch command once by hand:uv run --with "mcp[cli]" mcp run /absolute/path/to/server.pyNothing prints and it does not exit. That is correct: a stdio server waits for a host. A traceback here is the real bug. Stop it with Ctrl-C.
- 5
Add it to Claude Code
Register the same command; everything after
--is the launch command:claude mcp add demo -- uv run --with "mcp[cli]" mcp run /absolute/path/to/server.pyStart
claudeand run/mcp:demoshows as connected withaddlisted. Add--scope projectto write the entry to.mcp.jsonin the project root so your team gets it, or--scope userfor all your projects.PromptUse the demo server to add 19 and 23. - 6
Or add it to Claude Desktop
Run
uv run mcp install server.py. It writes the entry intoclaude_desktop_config.jsonwith absolute paths (macOS:~/Library/Application Support/Claude/claude_desktop_config.json, Windows:%APPDATA%\Claude\claude_desktop_config.json). Pass-v API_KEY=abc123or-f .envif the server needs environment variables.Fully quit Claude Desktop (Cmd+Q on macOS, Quit from the tray icon on Windows) and reopen it. The server appears under the + button in Connectors.
- 7
Serve it over HTTP when others need it
To give the server to people who do not have your file, serve it by URL:
uv run mcp run server.py --transport streamable-httplistens athttp://localhost:8000/mcp. Add that to Claude Code withclaude mcp add --transport http demo http://localhost:8000/mcp.Before you deploy behind a real hostname, set
transport_securitywith your allowed hosts. The default accepts onlylocalhost, and clients seeServer returned an error response.
Code
# macOS / Linux: install uv, then open a new terminal
curl -LsSf https://astral.sh/uv/install.sh | sh
uv init my-server
cd my-server
uv add "mcp[cli]" # or: pip install "mcp[cli]"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()# Test in MCP Inspector
uv run mcp dev server.py
# Claude Code (everything after -- is the launch command)
claude mcp add demo -- uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py
# Claude Desktop (writes claude_desktop_config.json for you)
uv run mcp install server.py
# Serve over Streamable HTTP at http://localhost:8000/mcp
uv run mcp run server.py --transport streamable-http{
"mcpServers": {
"demo": {
"command": "uv",
"args": ["run", "--with", "mcp[cli]", "mcp", "run", "/absolute/path/to/server.py"]
}
}
}import pytest
from mcp import Client
from server import mcp
@pytest.mark.anyio
async def test_add() -> None:
async with Client(mcp) as client:
result = await client.call_tool("add", {"a": 1, "b": 2})
assert result.structured_content == {"result": 3}Common errors and fixes
TypeError: The @tool decorator was used incorrectly. Did you forget to call it? Use @tool() instead of @tool
Cause: `@mcp.tool` written without parentheses. `tool()` is a decorator factory.
Fix: Write `@mcp.tool()`. The same applies to `@mcp.resource(...)` and `@mcp.prompt()`. This fails at import, so run `python server.py` to see it.
Server connects, then drops, or shows as failed in Claude Desktop or Claude Code
Cause: Something wrote to stdout, such as a `print()` call or a wrapper script echo. On stdio, stdout carries the protocol.
Fix: Remove `print()` from the server and log with the `logging` module, which writes to stderr.
claude-desktop, claude-code Source
Server does not appear in Claude Desktop after editing claude_desktop_config.json
Cause: A relative path in "args", a JSON syntax error, or Claude Desktop is still running with the old config.
Fix: Use absolute paths, check the JSON, then fully quit (Cmd+Q on macOS, Quit from the tray on Windows) and reopen. Read `~/Library/Logs/Claude/mcp-server-<NAME>.log` for the server's stderr.
claude-desktop Source
Host cannot start the server because uv is not found
Cause: Hosts launch servers with a minimal PATH that may not include uv.
Fix: Put the absolute path from `which uv` (macOS/Linux) or `where uv` (Windows) in "command".
claude-desktop, cursor, vs-code Source
mcp install fails with Claude app not found
Cause: Claude Desktop's config directory does not exist yet.
Fix: Install Claude Desktop and open it once, then run `uv run mcp install server.py` again.
claude-desktop Source
from mcp.server.fastmcp import FastMCP fails after upgrading to mcp 2.x
Cause: SDK v2 renamed FastMCP to MCPServer and moved mcp.server.fastmcp.* to mcp.server.mcpserver.*.
Fix: Use `from mcp.server import MCPServer`. To stay on v1 for now, pin `mcp>=1.28,<2`.
Tool already exists: <name>
Cause: Two tools registered with the same name. The first wins and the second is dropped.
Fix: Rename one of them. The same rule applies to resources and prompts.
MCPError: Server returned an error response
Cause: A deployed Streamable HTTP server rejects the request with 421 Invalid Host header. By default it accepts only a localhost Host header.
Fix: Pass `transport_security=TransportSecuritySettings(allowed_hosts=[...])` to `mcp.streamable_http_app()` with the hostname you serve.
Questions
What is FastMCP?
Two things share the name. In the official MCP Python SDK v1, FastMCP was the high-level server class (from mcp.server.fastmcp import FastMCP). SDK v2 renamed it to MCPServer (from mcp.server import MCPServer) and moved mcp.server.fastmcp.* to mcp.server.mcpserver.*. FastMCP is also a separate framework maintained by Prefect, on PyPI as fastmcp, documented at gofastmcp.com. This page covers the official SDK.
How do I install the MCP Python SDK?
Run uv add "mcp[cli]" in a uv project, or pip install "mcp[cli]". The SDK needs Python 3.11 or later. The cli extra adds the mcp command for mcp dev, mcp run and mcp install; install plain mcp if you do not need it. For a one-off command without a project, uv run --with "mcp[cli]" mcp ... works.
My v1 code says from mcp.server.fastmcp import FastMCP. What do I change?
Change the import to from mcp.server import MCPServer and FastMCP("Demo") to MCPServer("Demo"). @mcp.tool(), @mcp.resource() and @mcp.prompt() accept what they accepted in v1. Transport options such as host, port and stateless_http moved from the constructor to run(). If you cannot migrate yet, pin mcp>=1.28,<2 to stay on the 1.x line.
How do I test a Python MCP server with MCP Inspector?
Run uv run mcp dev server.py and open the URL it prints. The Inspector has a tab each for Tools, Resources and Prompts; you can call a tool with your own arguments and see the exact result a model would get. For any server in any language you can also run npx @modelcontextprotocol/inspector (the Inspector needs Node.js 22.19 or later).
How do I add a Python MCP server to Claude Code?
Run claude mcp add <name> -- uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py, then /mcp inside a session to confirm it is connected. Use an absolute path: the host launches your server from its own working directory.
How do I add it to Claude Desktop?
Run uv run mcp install server.py. It writes the launch command (with the absolute path to uv, --frozen and a version pin) into claude_desktop_config.json. Then fully quit and reopen Claude Desktop. If it fails with Claude app not found, install Claude Desktop and open it once so its config folder exists.
Why does my server break when I use print()?
On stdio, stdout carries the protocol messages. A print() that reaches stdout can corrupt a message and the host drops the connection. Log with the standard logging module, which writes to stderr. Over HTTP, stdout logging is fine.
Can I run it over HTTP or inside FastAPI?
Yes. uv run mcp run server.py --transport streamable-http serves the same server at http://localhost:8000/mcp. To add it to an existing ASGI app such as FastAPI, mount the app from mcp.streamable_http_app(); the SDK's Running your server pages cover mounting and deployment.
Why does my host list zero tools?
Check in this order: run python server.py yourself to catch an import error such as @mcp.tool without parentheses; make sure the host runs the file that defines your mcp object; look for Tool already exists: in the log, which means two tools shared a name; restart the host so it reloads its list.
Sources
Docs, issue trackers and discussions this page is built from.
Show all 12Hide
- MCP Python SDK README · github.com
- mcp on PyPI · pypi.org
- Installation · py.sdk.modelcontextprotocol.io
- What's new in v2 · py.sdk.modelcontextprotocol.io
- fastmcp on PyPI · pypi.org
- First steps · py.sdk.modelcontextprotocol.io
- Connect to a real host · py.sdk.modelcontextprotocol.io
- Connect Claude Code to tools via MCP · code.claude.com
- Troubleshooting · py.sdk.modelcontextprotocol.io
- Build an MCP server · modelcontextprotocol.io
- Get started · py.sdk.modelcontextprotocol.io
- MCP Inspector README · github.com