mrkeyoor.com_
Wed 05 Aug 10:00 UTC
PyPIAI / MLupdated 05 Aug 2026

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.

Verdict

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.

API stability2/5v2.0.0 renames the core server class, rebuilds the low-level Server around constructor handlers, removes transports and helpers, and changes silent behaviors like argument validation; almost no 1.x code imports unchanged. The new surface is cleaner, but the project just demonstrated it will break everything when the spec moves.
Docs5/5py.sdk.modelcontextprotocol.io is unusually good: a get-started track, one page per feature with CI-tested snippets, a candid what's-new tour, and a migration guide that lists every breaking change with before and after code. v1 docs stay published separately.
Maintenance5/5Pushed within hours of this review, 23k+ stars, maintained under the modelcontextprotocol org that owns the spec, and the v1.x branch keeps receiving critical and security fixes instead of being abandoned.
Ecosystem5/5MCP is the interoperability layer every major host adopted, and this SDK sits under a large share of published Python servers; 74M weekly downloads and a contributor Discord with a dedicated sdk channel back that up.

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
Skip it if

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/mcp

Transport 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=abc123

mcp 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 fixes

An 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

PackageRegistryPick it when
fastmcpPyPIYou want the independent FastMCP 2.x feature set (server composition, auth providers, deployment helpers) with a decorator API closer to mcp 1.x
@modelcontextprotocol/sdknpmYour stack is TypeScript or Node; this is the same official protocol implementation for that side
openai-agentsPyPIYou are building the agent side and only need to consume MCP servers other people wrote