SOLFIND
Web Lens
Portal home

What's new in v2 - MCP Python SDK

https://py.sdk.modelcontextprotocol.io/whats-new/ • 104 KB fetched
Open original page


What's new in v2 - MCP Python SDK Skip to content MCP Python SDK What's new in v2 * en - English * de - Deutsch * es - español * fr - français * hi - हिन्दी * ja - 日本語 * ko - 한국어 * pt - português (Brasil) * ru - русский язык * tr - Türkçe * uk - українська мова * zh - 简体中文 * zh-hant - 繁體中文 Search modelcontextprotocol/python-sdk MCP Python SDK modelcontextprotocol/python-sdk * MCP Python SDK * What's new in v2 What's new in v2 On this page * The SDK: v1 to v2 * FastMCP is now MCPServer * Resolve : the new way to ask the user for input * A first-class Client * The low-level Server was rebuilt, not renamed * The wire types moved to mcp-types , and every field is snake_case * Transport configuration moved to run() * Behavior that changes without an import error * Removed outright * The protocol: 2025-11-25 to 2026-07-28 * No handshake, no session * The server cannot call the client: multi-round-trip requests * Roots, sampling, and protocol logging are deprecated; ping is removed * Change notifications become one stream * The rest, quickly * Upgrading from v1? * Get started Get started * Installation * First steps * Connect to a real host * Testing * Servers Servers * Tools * Structured Output * Resources * URI templates * Prompts * Completions * Images, audio & icons * Handling errors * Inside your handler Inside your handler * The Context * Dependencies * Lifespan * Elicitation * Multi-round-trip requests * Sampling and roots * Progress * Logging * Subscriptions * Running your server Running your server * Add to an existing app * Deploy & scale * Authorization * OpenTelemetry * Serving legacy clients * Clients Clients * Callbacks * Transports * OAuth * Identity assertion * Multiple servers * Subscriptions * Caching * Protocol versions * Deprecated features * Advanced Advanced * The low-level Server * Pagination * Middleware * Extensions * MCP Apps * Troubleshooting * Translations * Migration Guide * API Reference API Reference * mcp * mcp_types On this page * The SDK: v1 to v2 * FastMCP is now MCPServer * Resolve : the new way to ask the user for input * A first-class Client * The low-level Server was rebuilt, not renamed * The wire types moved to mcp-types , and every field is snake_case * Transport configuration moved to run() * Behavior that changes without an import error * Removed outright * The protocol: 2025-11-25 to 2026-07-28 * No handshake, no session * The server cannot call the client: multi-round-trip requests * Roots, sampling, and protocol logging are deprecated; ping is removed * Change notifications become one stream * The rest, quickly * Upgrading from v1? What's new in v2 Two things happened at once in v2. The SDK was rebuilt : a new engine under both the client and the server, a first-class Client , and a set of renames that a v1 codebase meets on its first import. And the protocol moved : v2 speaks the 2026-07-28 revision of MCP, which removes the connection handshake, the session, and every server-initiated request, without stranding the clients you already have. This page is the tour of both halves, one section per headline, each ending in the page that owns the topic. It is not the porting manual. That is the Migration Guide : every breaking change, with before and after code. v2 is the stable line pip install mcp installs 2.x, and Installation has the copy-paste install line. If anything in v2 breaks, surprises, or slows you down, tell us . The SDK: v1 to v2 FastMCP is now MCPServer The high-level server class was renamed, and its module with it. This is the first thing every v1 server hits, because the old import path is gone rather than deprecated: from mcp.server import MCPServer # v1: from mcp.server.fastmcp import FastMCP mcp = MCPServer ( "Demo" ) # v1: FastMCP("Demo") It is also, for a decorator-built server, most of the port. @mcp.tool() , @mcp.resource() , and @mcp.prompt() accept what they accepted in v1 ( @mcp.resource() adds one optional security= keyword), and the input schema still comes from your type hints. Around the edges: everything under mcp.server.fastmcp.* now lives under mcp.server.mcpserver.* , ctx.fastmcp is ctx.mcp_server , get_context() is gone (declare a ctx: Context parameter instead), and the exception base FastMCPError is MCPServerError . The Migration Guide has the import table. Resolve : the new way to ask the user for input Not everything a tool needs should come from the model. New in v2, a tool parameter annotated with Resolve(fn) is filled by a function you write instead, invisibly to the model, and that function can return Elicit(...) to put a question in front of the user. This is the preferred way to get anything from the client mid-call: the SDK carries the question over whichever mechanism the connection supports (a live elicitation request for a legacy client, a multi-round-trip on 2026-07-28), so one tool body serves both eras. Dependencies is the page. Note The other two forms remain when you need them: ctx.elicit() still works for clients on legacy connections ( Elicitation ), and a handler can return an InputRequiredResult itself and drive the rounds by hand, which is also how sampling and roots requests travel at 2026-07-28 ( Multi-round-trip requests ). A first-class Client v1 handed you three nested layers: a transport context manager yielding raw streams, a ClientSession wrapped around them, and a hand-called await session.initialize() . v2 has one object: client.py import anyio from mcp import Client async def main () -> None : async with Client ( "http://localhost:8000/mcp" ) as client : print ( client . server_info ) print ( client . server_capabilities ) print ( client . protocol_version ) print ( client . instructions ) if __name__ == "__main__" : anyio . run ( main ) Client takes a URL (Streamable HTTP), a StdioServerParameters (a stdio subprocess), any other transport context manager such as sse_client(...) , or, in tests, the server object itself (in memory, no transport). Entering async with connects and negotiates the protocol version, whichever era the server speaks; client.server_capabilities and client.protocol_version are simply there afterwards, and client.server_info is too when the server identifies itself (it is Implementation | None now, since 2026-era identity is optional). The sampling and elicitation callbacks you registered in v1 still work (their bodies see the same snake_case attribute rename as everything else on this page), they now also answer the 2026-style requests-inside-results (below), and they run concurrently instead of one at a time. ClientSession is still underneath for anyone who wants the low-level surface, and client.session hands it to you; it moved too (it runs on the new dispatcher engine, and some of its own signatures changed), so read the Migration Guide before you drop down. The Client introduces it, Client transports covers the four connection forms, Client callbacks covers the callbacks themselves, and Testing shows the in-memory pattern that replaces v1's create_connected_server_and_client_session() helper. The low-level Server was rebuilt, not renamed If you work at the JSON-RPC layer, this is the "everything is different" part of v2. Here is the same one-tool server both ways; click the markers for what moved. v1 from typing import Any import mcp.types as types from mcp.server.lowlevel import Server server = Server ( "Bookshop" ) @server . list_tools () # (1)! async def list_tools () -> list [ types . Tool ]: return [ # (2)! types . Tool ( name = "search_books" , description = "Search the catalog by title or author." , inputSchema = { # (3)! "type" : "object" , "properties" : { "query" : { "type" : "string" }}, "required" : [ "query" ], }, ) ] @server . call_tool () async def call_tool ( name : str , arguments : dict [ str , Any ]) -> list [ types . ContentBlock ]: # (4)! if name != "search_books" : raise ValueError ( f "Unknown tool: { name } " ) # (5)! ctx = server . request_context # (6)! return [ types . TextContent ( type = "text" , text = f "Found 3 books matching { arguments [ 'query' ] !r} ." )] # (7)! * Handlers are registered with decorators (called, with parentheses), any time after the server exists. * You return a bare list[Tool] and the SDK wraps it into a ListToolsResult . * Fields are camelCase in Python, and the schema is enforced : the SDK jsonschema-validates call_tool arguments against it before your function runs, which is why arguments["query"] below is safe. * One call_tool handler serves every tool, and it receives the tool name and the already-validated arguments, unpacked and never None . * Raising is how a v1 tool signals failure: any exception is caught and returned as CallToolResult(isError=True) with str(e) as its text, so the calling model reads this message and can retry. * The context comes from an ambient ContextVar, reached through the server object mid-request. * Bare content blocks are wrapped into a CallToolResult for you. v2 from mcp import MCPError from mcp.server import Server , ServerRequestContext from mcp.types import ( INVALID_PARAMS , CallToolRequestParams , CallToolResult , ListToolsResult , PaginatedRequestParams , TextContent , Tool , ) SEARCH_BOOKS = Tool ( name = "search_books" , description = "Search the catalog by title or author." , input_schema = { # (1)! "type" : "object" , "properties" : { "query" : { "type" : "string" }}, "required" : [ "query" ], }, ) async def list_tools ( ctx : ServerRequestContext , params : PaginatedRequestParams | None ) -> ListToolsResult : # (2)! return ListToolsResult ( tools = [ SEARCH_BOOKS ]) # (3)! async def call_tool ( ctx : ServerRequestContext , params : CallToolRequestParams ) -> CallToolResult : # (4)! if params . name != "search_books" : raise MCPError ( INVALID_PARAMS , f "Unknown tool: { params . name } " ) # (5)! args = params . arguments or {} # (6)! text = f "Found 3 books matching { args [ 'query' ] !r} ." return CallToolResult ( content = [ TextContent ( type = "text" , text = text )]) # (7)! server = Server ( "Bookshop" , on_list_tools = list_tools , on_call_tool = call_tool ) # (8)! * Fields are snake_case now, and the schema is advertised but never applied : nothing checks the arguments before your handler runs. * Every handler has the same shape: async (ctx, params) -> result . The context is the first argument ( ctx.session , ctx.request_id , ctx.protocol_version live on it); this is where server.request_context went. * You build the full ListToolsResult yourself. Returning a bare list is a server-side TypeError now, not something the SDK wraps. * Typed params in ( params.name , params.arguments ), a full result out. Nothing is unpacked, wrapped, or converted for you. * Same check, different verb. A ValueError here would reach the model as an opaque -32603 (see below), so a deliberate wire error is raised as MCPError : it passes through with its code and message intact, and -32602 with this text is the spec's own answer for an unknown tool. * params.arguments can be None ; v1 defaulted it to {} before your code ever saw it. With no validation in front of the handler, this line is load-bearing. * An unexpected exception raised here becomes a sanitized protocol error, -32603 "Internal server error" : the model never sees the message. For a failure the model should read and react to, return CallToolResult(is_error=True, ...) . * Handlers are constructor arguments, so the server's surface is complete the moment it exists; add_request_handler() is the post-construction escape hatch, and the door to custom methods. The example is the pattern. More generally: every handler has the same shape, with typed params in and a full result type out; the old jsonschema check of tool arguments is gone; an exception is a protocol error, never an is_error=True tool result; and the ambient server.request_context ContextVar is gone. Custom, vendor-namespaced methods are first class through add_request_handler(method, params_type, handler) , which validates inbound params against your model before your handler runs. And a middleware list (deliberately marked provisional) wraps every inbound message, replacing the private _handle_* methods people used to override. Underneath, the v1 BaseSession receive loop was replaced by a dispatcher engine that the client and the server now share, and it is what makes several things on this page true at once: one Server object serves both protocol eras, Client(server) dispatches in process with no JSON-RPC framing, and a timed-out client request now actually cancels the server-side handler. The low-level Server is the page; the Migration Guide walks every removed hook. If you never dropped below MCPServer , none of this touches you. The wire types moved to mcp-types , and every field is snake_case The protocol types now live in their own distribution, mcp-types . It depends on nothing but pydantic and typing-extensions, so a gateway, a proxy, or a code generator can consume MCP's wire shapes without installing an HTTP stack: such a project installs mcp-types and imports mcp_types . mcp itself depends on that package at an exact version and re-exposes it, so code that depends on the SDK keeps writing import mcp.types as types and from mcp.types import Tool (a permanent alias, every name the same object) and declares only its one real dependency, mcp . The rule of thumb: import through whichever package you actually depend on. On those types, every Python attribute is now snake_case: result.is_error , tool.input_schema , listing.next_cursor . The JSON on the wire is camelCase, exactly as before; only the attribute spelling changed. Two stricter defaults ride along: unknown fields are ignored instead of round-tripped (put extras in _meta ), and both sides validate traffic against the protocol version they negotiated. See the Migration Guide for the rename table. Transport configuration moved to run() MCPServer(...) is about what your server is : its name, its instructions, its lifespan, its auth. How it is served now belongs to run() and the app builders, which is where host , port , stateless_http , json_response , the endpoint paths, and transport_security went ( MCPServer("x", port=9000) is a TypeError ). The overloads are typed per transport, so your editor tells you which options stdio takes and which streamable-http takes. One removal worth knowing: mount_path is gone; mounting the ASGI app is the supported way to serve under a prefix. Running your server covers the options; Add to an existing app covers mounting. Behavior that changes without an import error The renames announce themselves. These do not: * Sync functions run on a worker thread. A def tool (or resource, prompt, or resolver) no longer blocks the event loop; the trade is that its body no longer runs on the event-loop thread, which matters to thread-affine code. async def handlers are untouched. Migration Guide . * MCPError (v1's McpError ) raised inside a tool is a protocol error now. The model never sees it. Every other exception still becomes an is_error=True result, but only a ToolError 's message reaches the model: any other exception now reads Error executing tool <name> , with the traceback in your server log. Handling errors is the split. * Results are validated before they leave. A hand-built Tool whose input_schema is {} now fails tools/list (the spec requires "type": "object" ). Servers built on @mcp.tool() never see this; the SDK writes their schemas. * Your client validates what it receives. list_tools() and call_tool() check the server's answer against the negotiated protocol version, so a not-quite-valid server that v1's lenient parse tolerated now raises pydantic.ValidationError . If you connect to servers you do not control, expect to be the one who finds them; the Migration Guide has the details. * URI templates are real RFC 6570 now. {+path} , {?query} and friends work, matching is exact instead of regex-loose, and path traversal in extracted values is rejected by default. Stricter templates fail at decoration time, not on the first request. URI templates . * The streamable HTTP lifespan runs once , at startup, and its state is shared by every session and request. In v1 it ran once per session, and once per request under stateless_http=True . Pools and caches built in a lifespan get dramatically cheaper; anything that acquired a per-connection resource there belongs in the handler body now. Lifespan . * mcp dev and mcp install pin the environment they spawn to your installed SDK version. Both commands run your server in a fresh uv run --with ... environment, which used to resolve mcp to the newest stable release rather than the version you are developing against. Migration Guide . * The HTTP client is now httpx2 , not httpx . The dependency swap changes what your code catches and passes ( httpx2.AsyncClient , httpx2.ConnectError ), and it changes how TLS certificates are verified: httpx2 validates through truststore against the operating system trust store instead of certifi's bundled CA list. Most environments never notice; a minimal container with no system CA store, or a private CA that only certifi's bundle knew about, starts failing the TLS handshake. Set SSL_CERT_FILE / SSL_CERT_DIR or pass verify=ssl_context to your client. Migration Guide . Removed outright Each of these is a section in the Migration Guide : * The WebSocket transport , both sides, and the mcp[ws] extra. It was never part of the MCP specification. * The experimental Tasks API ( mcp.*.experimental ). 2026-07-28 moves tasks out of the core protocol and into an official extension ( SEP-2663 ), which this SDK does not implement yet. * mcp.shared.version , mcp.shared.progress

Links found on this page

  1. Skip to content [direct]
  2. en - English [direct]
  3. de - Deutsch [direct]
  4. es - español [direct]
  5. fr - français [direct]
  6. hi - हिन्दी [direct]
  7. ja - 日本語 [direct]
  8. ko - 한국어 [direct]
  9. pt - português (Brasil) [direct]
  10. ru - русский язык [direct]
  11. tr - Türkçe [direct]
  12. uk - українська мова [direct]
  13. zh - 简体中文 [direct]
  14. zh-hant - 繁體中文 [direct]
  15. modelcontextprotocol/python-sdk [direct]
  16. Get started [direct]
  17. Installation [direct]
  18. First steps [direct]
  19. Connect to a real host [direct]
  20. Testing [direct]
  21. Servers [direct]
  22. Tools [direct]
  23. Structured Output [direct]
  24. Resources [direct]
  25. URI templates [direct]
  26. Prompts [direct]
  27. Completions [direct]
  28. Images, audio & icons [direct]
  29. Handling errors [direct]
  30. Inside your handler [direct]
  31. The Context [direct]
  32. Dependencies [direct]
  33. Lifespan [direct]
  34. Elicitation [direct]
  35. Multi-round-trip requests [direct]
  36. Sampling and roots [direct]
  37. Progress [direct]
  38. Logging [direct]
  39. Subscriptions [direct]
  40. Running your server [direct]
  41. Add to an existing app [direct]
  42. Deploy & scale [direct]
  43. Authorization [direct]
  44. OpenTelemetry [direct]
  45. Serving legacy clients [direct]
  46. Clients [direct]
  47. Callbacks [direct]
  48. Transports [direct]
  49. OAuth [direct]
  50. Identity assertion [direct]
  51. Multiple servers [direct]
  52. Subscriptions [direct]
  53. Caching [direct]
  54. Protocol versions [direct]
  55. Deprecated features [direct]
  56. Advanced [direct]
  57. The low-level Server [direct]
  58. Pagination [direct]
  59. Middleware [direct]
  60. Extensions [direct]
  61. MCP Apps [direct]
  62. Troubleshooting [direct]
  63. Translations [direct]
  64. Migration Guide [direct]
  65. mcp [direct]
  66. mcp_types [direct]
  67. tell us [direct]
  68. SEP-2663 [direct]
  69. SEP-2577 [direct]
  70. spec #3002 [direct]
  71. SEP-2243 [direct]
  72. SEP-2549 [direct]
  73. SEP-2133 [direct]
  74. RFC 9207 [direct]
  75. SEP-990 [direct]
  76. /v1/ [direct]
  77. Zensical [direct]