Skip to content
, ,

Build an MCP Server in Python (2026): MCP SDK v2 Tools, Resources, Prompts and Claude Setup

9–14 minutes
Shocked woman in glasses at a cosy home desk, MCP card, hook MCP SDK 2.3 Python server

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.

TLDR

  • 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 path mcp.server.fastmcp was 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.py opens MCP Inspector.
  • Connect: edit claude_desktop_config.json, or run claude mcp add in 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.

  1. Install uv. Follow the steps on the uv site, then check it with uv --version.
  2. Create a project folder. Run uv init notes-server, then cd notes-server.
  3. Add the SDK. Run uv add "mcp[cli]". The cli extra adds the mcp command with dev, run and install.
  4. Check the version. Run uv run mcp version. It should print MCP version 2.3.0 or newer.
  5. Create the server file. Make an empty server.py in 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.

  1. Install Node.js. Get the LTS build from nodejs.org so that npx works.
  2. Launch the Inspector with your server. In the project folder run uv run mcp dev server.py. This runs npx @modelcontextprotocol/inspector and starts your server under it.
  3. Open the browser tab. The Inspector prints a local URL. Open it and click Connect.
  4. Call a tool. Open the Tools tab, pick add_note, fill in a title and body, and run it.
  5. Check resources and prompts. Read notes://yourtitle in the Resources tab and try summarize_note in 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.

  1. Open the config. In Claude Desktop, open Settings from the app menu, go to Developer, and click Edit Config.
  2. 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.
  3. Add your server. Paste the JSON below, using the absolute path to your project folder.
  4. Restart fully. Quit Claude Desktop completely and open it again.
  5. 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.

  1. Add the stdio server. Run claude mcp add --transport stdio notes -- uv --directory /ABSOLUTE/PATH/TO/notes-server run server.py.
  2. 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).
  3. Or add the HTTP version. With the server running, use claude mcp add --transport http notes http://127.0.0.1:8000/mcp.
  4. Check it. Run claude mcp list in the terminal, or type /mcp inside a Claude Code session.
  5. 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.

Sources

Free weekly email

Get new AI guides every week

One short weekly email with our newest local AI, ComfyUI and AI tool guides. No spam, no pop-ups, just the good stuff.

Free. Unsubscribe anytime in one click. We only use your email to send this newsletter. See our privacy policy.

Shocked woman in glasses at a cosy home desk, MCP card, hook MCP SDK 2.3 Python server