Skip to content
HUIAHUB

TutorialAgents & automation

How to build an MCP server in Python with the official SDK

Build an MCP server in Python with the official mcp SDK: add a tool and a resource, run it over stdio or HTTP, and test it in the MCP Inspector.

Edited by Published 7 min read16 sources

To build an MCP server in Python, install the official mcp package, create an MCPServer object, and turn plain functions into tools and resources with decorators. Run the file over stdio for a local client or over Streamable HTTP for a deployed one, then open it in the MCP Inspector before any assistant touches it. The whole server below fits in about 40 lines.

The Python SDK changed shape in version 2.0. Many tutorials that rank for this topic still import FastMCP from mcp.server.fastmcp and serve over SSE. In the current SDK that import raises ModuleNotFoundError, and the docs tell you not to build anything new on SSE. Every snippet here uses the v2 API.

What does an MCP server expose?

An MCP server offers three kinds of thing to a client. The SDK documentation splits them by who decides to use them:

PrimitiveWho triggers itTypical use
ToolThe modelRun a query, call an API, write a record
ResourceThe host applicationLoad a file, a record or an API response into context
PromptThe userA saved message template, often shown as a slash command

A tool is the closest thing to a POST endpoint: it does work and can have side effects. A resource is closer to a GET: it returns data and changes nothing. The server never talks to the model directly. The host (Claude Desktop, Claude Code, an IDE) runs an MCP client for each server it connects to.

Install the MCP Python SDK

The official MCP Python SDK (opens in a new tab) needs Python 3.10 or later. The package on PyPI is called mcp (opens in a new tab), and its current release is 2.3.0. The official build-server tutorial requires 2.0.0 or higher.

The project docs use uv, so the steps below do too:

uv init notes-server
cd notes-server
uv add "mcp[cli]"

The cli extra adds the mcp command (mcp dev, mcp run, mcp install, mcp version). With pip, the equivalent is pip install "mcp[cli]". If you never need the command-line tool, plain mcp is enough.

Build an MCP server in Python: a minimal example

The example below gives an assistant read and write access to a small set of notes. The notes live in a dictionary so the file runs as is. In a real server that dictionary becomes your database or API client.

Create server.py:

from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError

mcp = MCPServer("Notes")

# Illustrative sample data. Swap in your own database or API calls.
NOTES: dict[str, str] = {
    "standup": "Ship the billing fix before Friday.",
    "ideas": "Cache the search results for an hour.",
}


@mcp.tool()
def search_notes(query: str) -> list[str]:
    """Return the IDs of notes whose text contains the query."""
    needle = query.lower()
    return [note_id for note_id, text in NOTES.items() if needle in text.lower()]


@mcp.tool()
def add_note(note_id: str, text: str) -> str:
    """Save a new note under an ID that is not already taken."""
    if note_id in NOTES:
        raise ToolError(f"A note called {note_id!r} already exists.")
    NOTES[note_id] = text
    return f"Saved {note_id}."


@mcp.resource("notes://index")
def note_index() -> str:
    """Every note ID, one per line."""
    return "\n".join(sorted(NOTES))


@mcp.resource("notes://by-id/{note_id}")
def read_note(note_id: str) -> str:
    """The full text of one note."""
    return NOTES.get(note_id, f"No note with ID {note_id}.")


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

How the SDK reads your functions

You never write a JSON Schema. The SDK takes the tool name from the function name, the description from the docstring and the input schema from the type hints. query: str is the whole schema for search_notes.

Why the tool raises ToolError

Raising ToolError returns a result marked as an error, with your message where the model can read it, so the model can retry with better arguments. The SDK docs warn against returning an error string instead: a returned string looks like a successful answer to the client.

Static resources and resource templates

notes://index is a fixed resource. notes://by-id/{note_id} is a resource template: the placeholder must match a function parameter by name, or the decorator fails at import time. Templates appear under resources/templates/list instead of the plain resource list. The function body runs only when a client reads that URI, never during listing.

A prompt works the same way with @mcp.prompt(). The string it returns becomes a user message.

Running the server over stdio or Streamable HTTP

mcp.run() with no arguments uses stdio. The host starts your file as a child process and exchanges messages over its stdin and stdout. Nothing listens on a port. Keep run() under the if __name__ == "__main__": guard, because mcp dev, mcp run and your tests all import the file.

To serve the same object over HTTP, name the transport:

if __name__ == "__main__":
    mcp.run(transport="streamable-http", port=3001)

Clients then connect to http://127.0.0.1:3001/mcp. Without arguments, the HTTP transport listens on 127.0.0.1, port 8000, at the path /mcp. Transport options such as host and port go to run(), never to the MCPServer(...) constructor.

You can also switch transport from the command line without touching the file:

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

mcp run skips your __main__ block and calls run() itself, forwarding only --transport. That command serves the default address, http://localhost:8000/mcp.

Choosing a transport

QuestionstdioStreamable HTTP
Who starts the serverThe host, as a subprocessYou, as a long-running process
Where it runsSame machine as the hostAny machine the client can reach
AddressNonehttp://127.0.0.1:8000/mcp by default
What stdout is forThe protocol stream onlyFree for normal logging
Good fitA personal tool over local files or a private APIA server shared with other people or deployed

SSE is the older HTTP transport. The SDK still runs it for clients that have not moved, and the docs say not to start new work on it.

How to test the server with the MCP Inspector

The MCP Inspector (opens in a new tab) is the official tool for poking at a server before a real client connects. It is a Node.js app, so you need npx on your PATH. The current Inspector (2.x) requires Node 22.19.0 or later.

Step 1: open the web UI

uv run mcp dev server.py

mcp dev launches the server over stdio under the Inspector. Open the URL it prints. The Inspector has a tab per primitive. Under Tools, call search_notes with query set to billing and you should get ["standup"] back. Under Resources, notes://index is in the list, and notes://by-id/{note_id} sits under Resource Templates.

If your server needs extra packages, add them with --with, for example uv run mcp dev server.py --with httpx.

Step 2: script the checks with the Inspector CLI

The Inspector also has a CLI mode that prints results and exits with a status code, which suits CI:

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

npx @modelcontextprotocol/inspector --cli uv run server.py \
  --method tools/call --tool-name add_note \
  --tool-arg note_id=todo --tool-arg text="Renew the TLS cert"

npx @modelcontextprotocol/inspector --cli uv run server.py \
  --method resources/read --uri notes://index

Two details from the Inspector docs matter here. The server command must come first, straight after --cli, or the Inspector silently drops it and falls back to its saved catalog. A tools/call that returns an error exits with code 5, so a failing tool stops a shell && chain. The Inspector smoke-testing guide (opens in a new tab) covers JSON output and the full exit-code list.

Step 3: test the HTTP endpoint

Start the server with uv run mcp run server.py --transport streamable-http in one terminal, then point the Inspector at the URL from another:

npx @modelcontextprotocol/inspector --cli \
  --transport http --server-url http://localhost:8000/mcp --method tools/list

For unit tests, the SDK's Client class accepts the server object itself (Client(mcp)) and connects in memory, without a subprocess or a port.

Connecting the server to a client

Every stdio host needs the same thing: the command that starts your server. The SDK docs suggest a launch command that works from any directory:

uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py

uv run mcp install server.py --name "Notes" writes that entry into Claude Desktop's config for you. For Claude Code, the SDK docs give claude mcp add notes -- uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py, and /mcp inside a session confirms the connection. The official build-server tutorial (opens in a new tab) walks through the Claude Desktop JSON by hand. Hosts start servers with a minimal PATH, so use the absolute path to uv if the host cannot find it.

Security: what your MCP server can reach

A stdio server is an ordinary local process started by the host. It can read any file, call any network address and use any credential that the process can reach. Nothing in the SDK limits what a tool function does, so the limits have to live in your code.

  • Scope each tool narrowly. The model decides when to call a tool and fills in the arguments. Expose search_notes, not run_sql(query). Prefer read-only tools, and add write tools only where the assistant needs them.
  • Validate arguments inside the tool and raise ToolError with a clear message when they fail.
  • Pass secrets through environment variables in the host's config (mcp install takes -v KEY=VALUE or -f .env), never in tool output.
  • Never write to stdout in a stdio server. A stray print() can corrupt the message stream. Use the logging module, which writes to stderr.
  • Keep HTTP servers on localhost until you add authorization. By default the SDK binds 127.0.0.1 and turns on DNS-rebinding protection, which accepts only localhost Host headers. Passing a non-localhost host= switches that check off unless you configure transport_security with an explicit allowlist.
  • The HTTP transport rejects request bodies over 4 MiB by default (max_request_body_size).

The Inspector needs the same care. Its web server binds 127.0.0.1 and guards its API with a random token per launch. The README warns that on machines without an OS keychain it stores OAuth tokens and stdio env values in ~/.mcp-inspector/secrets.json, unencrypted unless you supply a key.

Frequently asked questions

Is FastMCP still part of the MCP Python SDK?

The class was renamed to `MCPServer` in version 2.0, and its module moved from `mcp.server.fastmcp` to `mcp.server.mcpserver`. The old import path was removed rather than deprecated, so it raises `ModuleNotFoundError`. The decorators carry over unchanged, so porting a v1 server is mostly a change of imports.

Can an MCP server written in Python call an async API?

Yes. The official tutorial defines its tools as `async def` functions that await an HTTP client, and the SDK handles both sync and async functions with the same decorators. Use async when a tool waits on the network so one slow call does not block the others.

Do I need Node.js to build a Python MCP server?

The server itself needs only Python 3.10 or later and the `mcp` package. Node.js is needed only for the MCP Inspector, which `mcp dev` starts through `npx`. The current Inspector requires Node 22.19.0 or later.

How do I deploy a Python MCP server for other people to use?

Serve it over Streamable HTTP and give users a URL instead of a launch command. Behind a real hostname, configure `transport_security` with your allowed hosts, or every request is refused with a 421 error. The SDK's Authorization guide covers adding tokens before the server goes public.

Sources

  1. 1.What is the Model Context Protocol (MCP)? - Model Context ProtocolOfficial docsPrimarymodelcontextprotocol.io
  2. 2.Build an MCP server - Model Context ProtocolOfficial docsPrimarymodelcontextprotocol.io
  3. 3.GitHub - modelcontextprotocol/python-sdk: The official Python SDK for Model Context Protocol servers and clients · GitHubRepositoryPrimarygithub.com
  4. 4.GitHub - modelcontextprotocol/inspector: Visual testing tool for MCP servers · GitHubRepositoryPrimarygithub.com
  5. 5.pypi.org/project/mcpWeb
  6. 6.py.sdk.modelcontextprotocol.io/get-started/first-stepsWeb
  7. 7.py.sdk.modelcontextprotocol.io/servers/resourcesWeb
  8. 8.py.sdk.modelcontextprotocol.io/runWeb
  9. 9.py.sdk.modelcontextprotocol.io/servers/handling-errorsWeb
  10. 10.py.sdk.modelcontextprotocol.io/get-started/testingWeb
  11. 11.py.sdk.modelcontextprotocol.io/get-started/real-hostWeb
  12. 12.py.sdk.modelcontextprotocol.io/run/deployWeb
  13. 13.py.sdk.modelcontextprotocol.io/migrationWeb
  14. 14.github.com/modelcontextprotocol/inspector/blob/main/docs/cli-smoke-testing.mdRepositoryPrimary
  15. 15.github.com/modelcontextprotocol/inspector/blob/main/docs/environment-variables.mdRepositoryPrimary
  16. 16.raw.githubusercontent.com/modelcontextprotocol/inspector/main/package.jsonWeb

Spotted an error? See our corrections policy.

Enjoyed this? Get the weekly brief.

New guides, Project & Tool of the Week and the AI developments that matter. One email, no spam.

Read next