mcp
The official Python SDK for the Model Context Protocol, the standard for exposing tools, resources, and prompts to LLM hosts like Claude, ChatGPT, Cursor, and VS Code. Version 2 is a major rework: the decorator server is now MCPServer (was FastMCP), a first-class Client replaces the old transport-plus-ClientSession layering, and the SDK speaks the 2026-07-28 spec revision (sessionless HTTP, multi-round-trip requests) while still serving every older client with no configuration.
The canonical SDK for a protocol that won: if you expose tools to LLM hosts from Python, this is the default choice, and the v2 design is genuinely better. Budget real time for the v1 break, because most example code on the internet is still v1.
Use it if
- You are shipping an MCP server so hosts like Claude Code, Claude Desktop, or Cursor can call your tools; two type-hinted functions and @mcp.tool() replace all the schema and protocol code
- You need the client side too: one Client object talks to a server in memory, over stdio, or over Streamable HTTP, which also makes testing servers trivial
- You want current-spec behavior for free: one deployment answers both 2025-era session clients and 2026-era sessionless clients, so replicas behind a plain load balancer work for modern traffic
- You are starting something new in Python; 2.x is the stable line and pip install mcp now resolves to it
- You have a working 1.x server and no pressing need to move: v2 renames or removes most import paths (FastMCP, get_context, streamablehttp_client, the WebSocket transport) and the migration is real work, so pin mcp<2 until you schedule it
- Your tools only ever get called from your own app: passing plain Python functions to your agent framework skips a protocol layer, a subprocess, and a schema round trip
- You depend on v1 behaviors that changed silently: the low-level Server no longer jsonschema-validates tool arguments, sync handlers now run on worker threads, and the HTTP stack moved from httpx to httpx2 with OS trust-store TLS, which breaks minimal containers
- You need the experimental Tasks API or WebSocket transport; both were removed outright in v2
Setup reality
pip install "mcp[cli]" and a two-function server runs under mcp dev in minutes, though the Inspector needs npx because it is a Node app. The real friction is version skew: pip install mcp now lands 2.0.0, so nearly every v1 tutorial, blog post, and AI-generated snippet you will meet imports from mcp.server.fastmcp and fails on arrival. Other tripwires: stdio servers must keep stdout clean (a stray flushed print corrupts the wire), and transport options like port moved from the constructor to run(), so old examples raise TypeError.
Patterns
A tool and a resource in one fileminimal-server
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()This is the v2 import; from mcp.server.fastmcp import FastMCP is gone, not deprecated, so v1 tutorials fail on the first line. Type hints are the input schema and the docstring is the description.
Serve over HTTP instead of stdiorun-streamable-http
from mcp.server import MCPServer
mcp = MCPServer("Demo")
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers."""
return a + b
if __name__ == "__main__":
mcp.run(transport="streamable-http", port=3001)
# clients connect to http://127.0.0.1:3001/mcpTransport options (host, port, stateless_http, json_response) belong to run(), never the constructor; MCPServer("x", port=9000) raises TypeError in v2. SSE still exists but is superseded, do not build on it.
Request context and progress inside a tooltool-context-progress
from mcp.server import MCPServer
from mcp.server.mcpserver import Context
mcp = MCPServer("Jobs")
@mcp.tool()
async def crunch(dataset: str, ctx: Context) -> str:
"""Process a dataset with progress updates."""
for i in range(5):
await ctx.report_progress(i, 5, f"chunk {i}")
return f"[request {ctx.request_id}] done with {dataset}"The SDK injects Context by annotation, so the parameter name does not matter and it never appears in the tool's input schema. get_context() from v1 is removed; declaring the parameter is the only way.
Return typed data from a toolstructured-output
from pydantic import BaseModel
from mcp.server import MCPServer
mcp = MCPServer("Bookshop")
class Book(BaseModel):
title: str
author: str
year: int
@mcp.tool()
def lookup_book(title: str) -> Book:
"""Look up a book by its exact title."""
return Book(title="Dune", author="Frank Herbert", year=1965)A Pydantic return type gives clients result.structured_content plus an output schema, alongside the plain text blocks. Raising an ordinary exception here becomes an is_error result the model can read and retry.
Test a server with no transport at allin-memory-testing
import asyncio
from mcp import Client
from server import mcp # your MCPServer instance
async def main() -> None:
async with Client(mcp) as client:
result = await client.call_tool("add", {"a": 1, "b": 2})
print(result.structured_content) # {'result': 3}
asyncio.run(main())Client(server_object) connects in process, no subprocess or port, which is the v2 testing story and replaces v1's create_connected_server_and_client_session helper. The same code with a URL string talks to a remote server.
Connect to a remote server and list toolshttp-client
from mcp import Client
async def main() -> None:
async with Client("http://localhost:3001/mcp") as client:
print(client.protocol_version)
listing = await client.list_tools()
for tool in listing.tools:
print(tool.name, tool.input_schema)Every Python attribute is snake_case in v2 (input_schema, is_error, next_cursor) even though the wire stays camelCase. The client also validates responses now, so a sloppy third-party server that v1 tolerated can raise pydantic.ValidationError.
Ask the user a question mid-toolelicit-user-input
from typing import Annotated
from pydantic import BaseModel
from mcp.server import MCPServer
from mcp.server.mcpserver import AcceptedElicitation, Elicit, ElicitationResult, Resolve
mcp = MCPServer("Bookshop")
class Quantity(BaseModel):
copies: int
async def ask_quantity() -> Elicit[Quantity]:
return Elicit("How many copies?", Quantity)
@mcp.tool()
async def reserve(title: str, quantity: Annotated[ElicitationResult[Quantity], Resolve(ask_quantity)]) -> str:
"""Reserve copies of a book."""
if isinstance(quantity, AcceptedElicitation):
return f"Reserved {quantity.data.copies} of {title!r}."
return "Nothing reserved."Resolve parameters are invisible to the model and work on both protocol eras. Calling ctx.elicit() directly raises NoBackChannelError against 2026-era clients, including your own v2 test client, which is the classic post-migration surprise.
Errors the model sees vs protocol errorstool-error-handling
from mcp import MCPError
from mcp.types import INVALID_PARAMS
from mcp.server import MCPServer
mcp = MCPServer("Bookshop")
@mcp.tool()
def checkout(book_id: str) -> str:
"""Check out a book."""
if not book_id.startswith("bk_"):
# model-visible failure -> is_error tool result, model can retry
raise ValueError(f"bad id {book_id!r}, expected bk_ prefix")
if book_id == "bk_banned":
# deliberate wire error -> passes through with code intact
raise MCPError(INVALID_PARAMS, "this title is restricted")
return f"checked out {book_id}"This split flipped in v2: MCPError (v1's McpError) is now a protocol error the model never reads, while other exceptions become is_error results. Get it backwards and the model sees an opaque -32602 instead of a message it could act on.
Develop against the MCP Inspectorinspector-dev-loop
uv add "mcp[cli]"
uv run mcp dev server.py
uv run mcp dev server.py --with pandas
# register with Claude Desktop
uv run mcp install server.py --name "Bookshop" -v API_KEY=abc123mcp dev needs npx on PATH (the Inspector is Node), and mcp install only knows Claude Desktop; every other host takes the same launch command in its own config file. Both commands now pin the spawned environment to your installed SDK version instead of resolving latest.
Stay on 1.x until you migratepin-v1-requirement
# requirements.txt / pyproject dependency
mcp>=1.28,<2
# v1 docs live on at https://py.sdk.modelcontextprotocol.io/v1/
# v1.x branch still receives critical and security fixesAn unpinned mcp requirement silently resolves to 2.x now and your FastMCP imports stop working. Libraries that depend on mcp should carry this upper bound in their published metadata, not just in a lockfile.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| fastmcp | PyPI | You want the independent FastMCP 2.x feature set (server composition, auth providers, deployment helpers) with a decorator API closer to mcp 1.x |
| @modelcontextprotocol/sdk | npm | Your stack is TypeScript or Node; this is the same official protocol implementation for that side |
| openai-agents | PyPI | You are building the agent side and only need to consume MCP servers other people wrote |