The official MCP Python SDK is now on version 2.3.0, and v2 changed the basics. The old FastMCP class is now called MCPServer, and many older tutorials no longer run as written.
This guide builds a small working notes server from scratch. You will add tools, a resource and a prompt, run it over stdio and Streamable HTTP, test it in MCP Inspector, then connect it to Claude Desktop and Claude Code.
- Install:
uv add "mcp[cli]"(Python 3.10+). The current SDK is mcp 2.3.0 (October 2, 2026). - New import:
from mcp.server import MCPServer. The v1 pathmcp.server.fastmcpwas removed. - Three building blocks:
@mcp.tool(),@mcp.resource()and@mcp.prompt(). Type hints become the JSON schema. - Transports: stdio for local apps, Streamable HTTP (default
http://127.0.0.1:8000/mcp) for remote use. - Test:
uv run mcp dev server.pyopens MCP Inspector. - Connect: edit
claude_desktop_config.json, or runclaude mcp addin Claude Code.
What an MCP Server Is (in Plain Words)
The Model Context Protocol (MCP) is an open standard that lets AI apps talk to outside tools and data. An MCP server is a small program that offers those tools. Apps like Claude Desktop, Claude Code and Cursor act as hosts that connect to it.
The current spec version is 2026-07-28. It uses JSON-RPC 2.0 messages. A server can offer three kinds of things:
- Tools: functions the AI model can call, such as “save a note” or “search the database”. The host asks the user before running them.
- Resources: read-only data, such as a file or an API response, addressed by a URI.
- Prompts: reusable message templates the user can pick, such as “summarize this note”.
If you only want to use existing servers, see my best MCP servers list. This guide is for building your own.
What Changed in the MCP Python SDK v2
Version 2.0.0 shipped on July 28, 2026, the same day as the new spec. Releases 2.1, 2.2 and 2.3 followed. The changes below break most v1 code, so check them before you copy an old example.
| Area | v1 (old) | v2 (current) |
|---|---|---|
| Server class | from mcp.server.fastmcp import FastMCP |
from mcp.server import MCPServer |
| Submodules | mcp.server.fastmcp.* |
mcp.server.mcpserver.* |
| Host and port | FastMCP("x", port=9000) |
Passed to run(). The old style raises a TypeError. |
| Context | mcp.get_context() |
Add a ctx: Context parameter to the tool |
| Errors | McpError, FastMCPError |
MCPError, MCPServerError |
| WebSocket transport | Available | Removed |
| Client | Manual session setup | One Client class that takes a URL, stdio params or the server object |
The 2026-07-28 spec also marks sampling, roots and MCP-level logging (such as ctx.info()) as deprecated. They still work but raise a deprecation warning, so avoid them in new code.
Note: There is also a separate community package on PyPI called fastmcp (now at 4.x). It is a different project from the official mcp SDK. This guide uses the official one.
Set Up the Project
You need Python 3.10 or newer. The official docs use uv, a fast Python package manager, so this guide does too. Plain pip also works.
- Install uv. Follow the steps on the uv site, then check it with
uv --version. - Create a project folder. Run
uv init notes-server, thencd notes-server. - Add the SDK. Run
uv add "mcp[cli]". Thecliextra adds themcpcommand withdev,runandinstall. - Check the version. Run
uv run mcp version. It should printMCP version 2.3.0or newer. - Create the server file. Make an empty
server.pyin the folder.
With pip, the install line is pip install "mcp[cli]" inside a virtual environment.
Write the Server: Tools, Resources and Prompts
Here is a complete notes server. It saves notes as text files in a mcp-notes folder in your home directory. Paste it into server.py.
"""Notes MCP server: a small demo."""
from pathlib import Path
from mcp.server import MCPServer
from mcp.server.mcpserver import Context
mcp = MCPServer("Notes")
NOTES_DIR = Path.home() / "mcp-notes"
NOTES_DIR.mkdir(exist_ok=True)
@mcp.tool()
def add_note(title: str, body: str) -> str:
"""Save a note as a text file. Use a short title."""
safe = "".join(c for c in title if c.isalnum() or c in "-_ ").strip()
path = NOTES_DIR / f"{safe or 'note'}.txt"
path.write_text(body, encoding="utf-8")
return f"Saved {path.name}"
@mcp.tool()
def list_notes() -> list[str]:
"""List the titles of all saved notes."""
return sorted(p.stem for p in NOTES_DIR.glob("*.txt"))
@mcp.tool()
async def word_count(title: str, ctx: Context) -> int:
"""Count the words in one saved note."""
await ctx.report_progress(0.5, 1.0)
text = (NOTES_DIR / f"{title}.txt").read_text(encoding="utf-8")
return len(text.split())
@mcp.resource("notes://{title}")
def read_note(title: str) -> str:
"""Read one saved note by its title."""
return (NOTES_DIR / f"{title}.txt").read_text(encoding="utf-8")
@mcp.prompt()
def summarize_note(title: str) -> str:
"""Ask the model to summarize a saved note."""
return f"Read the resource notes://{title} and summarize it in 3 bullet points."
if __name__ == "__main__":
mcp.run() # stdio by default
How the tools work
Each @mcp.tool() function becomes a tool. The SDK reads the type hints to build the input schema, and the docstring becomes the description the model sees. So write clear docstrings, because the model uses them to decide when to call a tool.
Return values are sent back as text and as structured content. For example, list_notes returns {"result": ["groceries"]} as structured output. Bad input is caught for you: a missing body argument comes back as a validation error.
How the resource works
The notes://{title} pattern makes a resource template. A client can read notes://groceries and the SDK passes "groceries" into the function. Use a fixed URI like config://app for a single static resource.
How the prompt and Context work
The @mcp.prompt() function returns a message the user can choose from the host’s prompt menu. The ctx: Context parameter in word_count gives the tool access to the request, such as progress reports. The client never sees ctx in the schema.
Warning: In a stdio server, never use print(). It writes to stdout and corrupts the JSON-RPC messages. Use the logging module instead, which writes to stderr.
stdio vs Streamable HTTP: Which Transport to Use
The transport is how the host and the server send messages. The SDK supports stdio, Streamable HTTP and the older SSE transport.
| Transport | How it runs | Best for | Start it with |
|---|---|---|---|
| stdio | The host starts your script as a child process and talks over stdin/stdout | Local tools in Claude Desktop, Claude Code, Cursor | mcp.run() |
| Streamable HTTP | Your server listens on a port; clients send HTTP POST requests | Remote or shared servers, many users, cloud deploys | mcp.run(transport="streamable-http") |
| SSE (legacy) | Older HTTP plus server-sent events | Old clients only | mcp.run(transport="sse") |
For HTTP, the defaults are host 127.0.0.1, port 8000 and path /mcp. You can change them in run():
mcp.run(
transport="streamable-http",
host="0.0.0.0",
port=9000,
stateless_http=True, # no sessions, easier to scale
json_response=True, # plain JSON replies instead of SSE streams
)
You can also start HTTP from the command line without editing the file: uv run mcp run server.py --transport streamable-http.
Tip: Start with stdio while you build. Move to Streamable HTTP only when other people or machines need to reach the server, and add authentication before you expose it to the internet.
Test With MCP Inspector
MCP Inspector is the official visual tester for MCP servers. It lets you list and call tools by hand, without an AI model in the loop. The current release is Inspector 2.14.0 and it needs a recent Node.js 22 or newer.
- Install Node.js. Get the LTS build from nodejs.org so that
npxworks. - Launch the Inspector with your server. In the project folder run
uv run mcp dev server.py. This runsnpx @modelcontextprotocol/inspectorand starts your server under it. - Open the browser tab. The Inspector prints a local URL. Open it and click Connect.
- Call a tool. Open the Tools tab, pick
add_note, fill in a title and body, and run it. - Check resources and prompts. Read
notes://yourtitlein the Resources tab and trysummarize_notein the Prompts tab.
To test an HTTP server, start it first, then run npx @modelcontextprotocol/inspector on its own and enter http://127.0.0.1:8000/mcp as the server URL. The Inspector also has a scriptable CLI mode (--cli) for CI checks.
Connect Your Server to Claude Desktop
Claude Desktop runs local servers over stdio. It reads them from a JSON config file.
- Open the config. In Claude Desktop, open Settings from the app menu, go to Developer, and click Edit Config.
- Find the file if needed. On macOS it is
~/Library/Application Support/Claude/claude_desktop_config.json. On Windows it is%APPDATA%\Claude\claude_desktop_config.json. On Linux it is~/.config/Claude/claude_desktop_config.json. - Add your server. Paste the JSON below, using the absolute path to your project folder.
- Restart fully. Quit Claude Desktop completely and open it again.
- Check the tools. Click the “Add files, connectors, and more” button in the chat box, open Connectors, then Manage connectors, and select “notes”.
{
"mcpServers": {
"notes": {
"command": "uv",
"args": [
"--directory",
"/ABSOLUTE/PATH/TO/notes-server",
"run",
"server.py"
]
}
}
}
On Windows, write the path with forward slashes, like C:/Users/you/notes-server. If Claude cannot find uv, put its full path in command. Get it with which uv (macOS/Linux) or where uv (Windows).
There is also a shortcut: uv run mcp install server.py writes the config entry for you. You can add --env-var KEY=value for secrets.
Connect Your Server to Claude Code
Claude Code adds servers with the claude mcp add command. Everything after the -- separator is the command that starts your server. New to Claude Code? Start with my install guide.
- Add the stdio server. Run
claude mcp add --transport stdio notes -- uv --directory /ABSOLUTE/PATH/TO/notes-server run server.py. - Pick a scope (optional). Add
--scope local(default, just you in this project),--scope project(shared through.mcp.json) or--scope user(all your projects). - Or add the HTTP version. With the server running, use
claude mcp add --transport http notes http://127.0.0.1:8000/mcp. - Check it. Run
claude mcp listin the terminal, or type/mcpinside a Claude Code session. - Use it. Ask Claude Code: “Save a note called ideas with three blog topics.”
Pass secrets with --env KEY=value. Remove a server with claude mcp remove notes. For more setup options, see my Claude Code MCP setup guide and the Claude Code commands cheat sheet.
Note: Claude Code warns when one tool reply goes over 10,000 tokens and caps it at 25,000 tokens by default. Raise the cap with the MAX_MCP_OUTPUT_TOKENS environment variable, or return shorter results.
Test Your Server With a Python Client
The same mcp package includes a Client. You can pass it your server object for a fast in-memory test, which is handy for pytest.
import asyncio
from mcp import Client
from server import mcp
async def main():
async with Client(mcp) as client:
tools = await client.list_tools()
print([t.name for t in tools.tools])
r = await client.call_tool("add_note", {"title": "groceries", "body": "milk eggs bread"})
print(r.structured_content) # {'result': 'Saved groceries.txt'}
r = await client.call_tool("word_count", {"title": "groceries"})
print(r.structured_content) # {'result': 3}
asyncio.run(main())
Pass a URL instead, like Client("http://127.0.0.1:8000/mcp"), to test the HTTP server. Pass StdioServerParameters(command="python", args=["server.py"]) to launch it as a subprocess, just like a real host does.
If you plan to build a full agent on top of your tools, the Claude Agent SDK guide shows how to plug MCP servers into an agent loop.
Troubleshooting: Common Problems and Fixes
| Problem | Likely cause | Fix |
|---|---|---|
ModuleNotFoundError: mcp.server.fastmcp |
v1 code on SDK v2 | Use from mcp.server import MCPServer |
TypeError on MCPServer("x", port=9000) |
Host and port moved in v2 | Pass port to mcp.run() |
| Server connects, then breaks or hangs | print() output on stdout |
Log with logging (stderr) only |
| Server missing in Claude Desktop | Bad JSON, relative path, or no restart | Check the JSON, use absolute paths, quit and reopen the app |
spawn uv ENOENT |
The app cannot find uv |
Put the full path to uv in command |
mcp dev says npx not found |
Node.js missing from PATH | Install Node.js LTS, then reopen the terminal |
| Tool returns “Error executing tool” | An exception inside your function | Test in Inspector and read the server log |
Claude Desktop writes MCP logs to ~/Library/Logs/Claude on macOS and %APPDATA%\Claude\logs on Windows. Look at mcp.log and mcp-server-notes.log for the real error.
Frequently Asked Questions
Is FastMCP still part of the official MCP Python SDK?
Not by that name. In SDK v2 the class was renamed to MCPServer and the old mcp.server.fastmcp import path was removed, though the decorator style is the same.
What Python version do I need?
The official mcp package needs Python 3.10 or newer. The current release is 2.3.0, published on October 2, 2026.
Should I use stdio or Streamable HTTP?
Use stdio for a server that runs on your own machine inside Claude Desktop or Claude Code. Use Streamable HTTP when the server runs remotely or many clients need it.
Do I need Node.js to build a Python MCP server?
No, the server itself is pure Python. You only need Node.js to run MCP Inspector through npx.
Can one server have tools, resources and prompts together?
Yes. A single MCPServer can register any mix of tools, resources and prompts, as the notes server in this guide shows.
How do I pass an API key to my server?
Read it from an environment variable in your code. Set it with "env" in claude_desktop_config.json, or with --env KEY=value in claude mcp add.
Does my MCP server work with apps other than Claude?
Yes. MCP is an open standard, so any MCP host can use your server, including Cursor, VS Code and other agent tools.




