Skip to content

docs: modernise the Python examples for the 2026-07-28 release - #3124

Merged
localden merged 2 commits into
modelcontextprotocol:docs/2026-07-28-releasefrom
maxisbey:python-examples-v2
Jul 27, 2026
Merged

docs: modernise the Python examples for the 2026-07-28 release#3124
localden merged 2 commits into
modelcontextprotocol:docs/2026-07-28-releasefrom
maxisbey:python-examples-v2

Conversation

@maxisbey

Copy link
Copy Markdown
Contributor

The Python SDK ships 2.0 stable alongside this spec revision, and the Python in
these six pages was written against 1.x. Some of it is merely old, and some of
it no longer imports at all. This brings all twenty Python blocks up to the v2
interfaces and the 2026-07-28 protocol shapes, and adjusts the prose wherever it
described code that is no longer there. Only the Python tabs are touched;
TypeScript, Java, Kotlin, C#, Ruby, Rust and Go are left exactly as they were.

Three things account for most of the diff. There is no initialize handshake at
2026-07-28, so examples that called it were negotiating the previous revision.
mcp.server.fastmcp no longer exists, so anything importing it raises on the
first line. And the protocol logging capability is deprecated in this revision
with no replacement, so examples may not teach it.

What changed, page by page

docs/docs/draft/develop/build-client.mdx

The largest change. The tab connected, but taught the previous era and carried
two bugs in the tool loop.

# before
from mcp import ClientSession, StdioServerParameters
from contextlib import AsyncExitStack

class MCPClient:
    def __init__(self):
        self.session: Optional[ClientSession] = None
        self.exit_stack = AsyncExitStack()

    async def connect_to_server(self, server_script_path: str):
        stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
        self.stdio, self.write = stdio_transport
        self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))
        await self.session.initialize()
# after
from mcp import Client, StdioServerParameters
from mcp.client.stdio import stdio_client

async with Client(stdio_client(server_params(sys.argv[1]))) as client:
    tool_list = await client.list_tools()

await self.session.initialize() was the handshake, so the page negotiated
2025-11-25 against a server that speaks 2026-07-28. The whole AsyncExitStack
and ClientSession scaffold existed because 1.x had no context manager owning
the connection, and Client is that context manager, so the class dissolves
into module-level functions and cleanup() goes away entirely.

Two bugs went with it. The tool loop handed raw MCP content blocks straight to
the model API, where result.content is a union of TextContent,
ImageContent, ResourceLink and more, so it now narrows with
isinstance(block, TextContent) before reading .text. And
assistant_message_content was appended to inside the loop and then mutated
afterwards, so any response asking for two tools built a duplicated assistant
turn with an unanswered tool_use. Results are now collected and appended once.
A failing tool comes back as an ordinary result with is_error set rather than
raising, so that flag is passed through to the model.

Separately, the chat loop called input() inside except Exception, so
pressing Ctrl-D raised EOFError, printed it, and looped forever. It produced
5.7 million lines in eight seconds. input() now runs on a worker thread with
an explicit EOF exit, which also stops it blocking the event loop.

The prose moved with the code: the sections on sessions, exit stacks,
initialization and cleanup are rewritten, and one heading changed from "Basic
Client Structure" to "Imports and Setup" since there is no client class any
more. No page links to that anchor.

docs/docs/draft/tutorials/security/authorization.mdx

The server block did not import on v2:

# before
from mcp.server.fastmcp.server import FastMCP

def create_server() -> FastMCP:
    app = FastMCP(
        name="MCP Resource Server",
        host=config.HOST,
        port=config.PORT,
        streamable_http_path="/",
        ...
    )
# after
from mcp.server import MCPServer

def create_server() -> MCPServer:
    app = MCPServer(
        name="MCP Resource Server",
        ...
    )

Past the import, host, port and streamable_http_path are not constructor
arguments in v2; they belong on run(), which is what
examples/servers/simple-auth in the SDK does, so they moved there. The
verifier block imported the wrong HTTP library, AccessToken is now built with
the subject and claims fields that exist in v2, and a swallowed exception
that bound an unused name now goes through the module's logger.

Three lines in the config module were removed because nothing read them, and
OAUTH_CLIENT_SECRET now defaults to an empty string with a sentence pointing
at the step where those credentials are created, which is what the TypeScript
tab already does.

The prose above the block told Python readers to rely on a separate third-party
project to explain code that does not use it. It now names MCPServer and
describes what the SDK actually does for you, each claim of which is visible in
the transcript below.

docs/docs/draft/learn/architecture.mdx

Four pseudo-code blocks. The first called session.initialize(), which does not
merely look stale: run against a stock v2 server it silently negotiates
2025-11-25. The other three used session vocabulary for objects that are
clients.

# before
async def handle_tools_changed_notification(session):
    tools_response = await session.list_tools()
    app.update_available_tools(session, tools_response.tools)
# after
async def follow_tool_changes(client):
    async with client.listen(tools_list_changed=True) as sub:
        async for _event in sub:
            tools_response = await client.list_tools()
            app.update_available_tools(client, tools_response.tools)

That last one is a shape change, not a rename. A function taking a session and
waiting to be called is a callback, and it presumes an unsolicited server push.
At 2026-07-28 notifications are opt-in on a stream the client opens, and there
is no registration point for a tools-changed callback, so renaming the parameter
would leave a function nothing ever calls. I checked this rather than assuming
it: with a message handler recording every inbound message and no listen call,
zero messages arrive while the tool list demonstrably changes.

docs/docs/draft/tools/debugging.mdx

The one Python block taught the deprecated logging capability, in a spelling
that could not run. Pasted verbatim it raises NameError: name 'server' is not defined, and its Context annotation had no working import.

# before
@server.tool()
async def my_tool(ctx: Context) -> str:
    await ctx.session.send_log_message(level="info", data="Server started successfully")
    return "done"
# after
import logging

from mcp.server import MCPServer

logger = logging.getLogger(__name__)

mcp = MCPServer("reports")


@mcp.tool()
async def fetch_report(report_id: str) -> str:
    """Fetch a report by id."""
    logger.info("Fetching report %s", report_id)
    return f"Report {report_id} is ready."

The two lines of prose introducing it changed with it, since they promised a log
message notification the code no longer sends.

docs/docs/draft/develop/build-server.mdx

Nothing here was broken. The weather tutorial already ran end to end. The changes
are staleness: httpx to httpx2, the long mcp.server.mcpserver import to
the short mcp.server spelling the SDK's own first-steps page teaches, and
httpx dropped from both install lines because httpx2>=2.5.0 is a hard
dependency of mcp and the old line installed a second HTTP stack.

The logging section moved to the logging.getLogger(__name__) idiom and lost
its print(..., file=sys.stderr) example, which was presented as a good pattern
but is the one thing the SDK's logging guidance rules out unconditionally. The
main() wrapper around mcp.run(transport="stdio") is gone, since the SDK puts
mcp.run() directly under the guard and mcp run weather.py skips the
__main__ block entirely.

docs/extensions/auth/oauth-client-credentials.mdx

Both blocks were a hard SyntaxError, so neither had ever run: the connect was
a top-level async with. The v2 API underneath was already correct. Each is now
wrapped in async def main() with an asyncio.run(main()) runner, matching the
SDK's own examples and the other Python examples in this repository. One
open(...).read() that dropped its file handle became Path(...).read_text().

How this was validated

This repository has no automated check for documentation code in any language,
so everything below was run by hand, and the programs are kept as evidence.

The method was the same for each page: extract the fenced blocks back out of the
.mdx after editing, assemble them into the file a reader would actually
create, and execute it. Nothing was checked by reading. All twenty Python blocks
across the six pages run.

The two quickstart pages are a matched pair a reader follows in sequence, so the
client tutorial was driven against the server tutorial's server, both assembled
from the pages:

server_params -> command='python' args=['weather.py']
negotiated protocol version: 2026-07-28
tools: ['get_alerts', 'get_forecast']
get_forecast.input_schema keys: ['latitude', 'longitude']
--- process_query returned ---
Let me look that up.
[Calling tool get_forecast with args {'latitude': 38.58, 'longitude': -121.49}]
Here is the answer.
--- end ---
tool_result.is_error   = False
tool_result.content[:80] = '\nToday:\nTemperature: 94°F\nWind: 3 to 8 mph S\nForecast: Sunny, with a high near 9'
messages sent round two: ['user', 'assistant', 'user']

That is live National Weather Service data through the page's own
make_nws_request, and the round-two message shape is the documented one: a
single assistant turn, a single user turn carrying the results, one follow-up
call. Running client.py weather.py as a plain subprocess also exits 0 on
Ctrl-D and reports a failed query without ending the session.

The other pages:

Page Blocks How it was driven Result
build-server.mdx 5 stdio subprocess, live upstream API pass
build-client.mdx 5 driven against the server above pass
architecture.mdx 4 executed against a real stdio server pass
debugging.mdx 1 stdio subprocess, v2 client pass
authorization.mdx 3 live server plus an RFC 7662 introspection endpoint pass
oauth-client-credentials.mdx 2 live server plus an authorization server pass

The architecture blocks are pseudo-code referencing the reader's own
application object, so a stand-in for that object was supplied and the block
bodies executed verbatim. Block 4 opens the stream, a tool change is published,
and the refetch observes the new tool.

For the authorization page:

=== PRM discovery (no token) ===
HTTP/1.1 200 OK
{"resource":"http://localhost:3222/","authorization_servers":[...],"scopes_supported":["mcp:tools"]}

=== tools/list with NO Authorization header ===
HTTP/1.1 401 Unauthorized
www-authenticate: Bearer error="invalid_token", resource_metadata=".../.well-known/oauth-protected-resource"

=== real MCP client, four token cases ===
no token         -> refused
inactive token   -> refused
wrong audience   -> refused
valid token      -> tools: ['add_numbers', 'multiply_numbers']
valid token      -> add_numbers(2,3) = {'operation': 'addition', ..., 'result': 5.0}

For the client credentials page, both blocks reached a server through a real
token exchange, one with client_secret_basic and one with a
private_key_jwt assertion whose RS256 signature was verified:

[AS] client_secret_basic ACCEPTED: my-service:s3cr3t
[AS] private_key_jwt ACCEPTED (RS256 signature verified): iss=my-service alg=RS256
[WIRE] POST /mcp method='server/discover'
[WIRE] POST /mcp method='tools/list'

That wire log is worth noting on its own: server/discover followed by
tools/list, with no initialize anywhere, so the examples are provably on the
current revision rather than merely free of the old call in their text.

Blast radius was checked mechanically rather than by eye. Every fenced block on
all six pages was parsed before and after and compared as a (tab, language,
body) tuple: every non-Python fence is byte-identical to HEAD, TypeScript
included. The two exceptions are the install lines on build-server, which are
shell fences inside the Python tab. Heading sets are unchanged except for the
one noted above, and the only fragment link into any of these pages in the
whole repository is /docs/learn/architecture#example, whose anchor is intact.
npm run check:docs passes: prettier, the MDX comment check, and the broken
link check.

Left alone on purpose

  • The lessons themselves. These are tutorials with a narrative, and the aim
    was to modernise the code, not to redesign the teaching. The weather tools
    still return formatted strings rather than models, the architecture blocks are
    still pseudo-code at the same altitude, and the authorization page still
    teaches introspection rather than local signature checks even though the SDK's
    own guide leads with the latter.
  • Every non-Python tab, including the ones that are also out of date. The
    debugging.mdx TypeScript tab still uses the deprecated logging capability,
    and after this change the severity-level prose beneath the group describes
    only that tab. Somebody should make the same call for TypeScript, but not from
    a Python change.
  • architecture.mdx step 4's JSON narrative, which still describes the
    server proactively notifying clients and ties notifications to listChanged
    at initialization. Those are framings of the wire rather than of the Python,
    and #3068 rewrites them along with the JSON. Changing the prose without the
    JSON would swap one mismatch for a worse one.
  • The model identifier in build-client, which is shared verbatim with the
    seven other language tabs. Refreshing it is a page-wide change, not a
    Python-only one, and it should be done separately. More on this below.
  • The two overview bullets and the severity-level paragraph in
    debugging.mdx
    , both of which remain accurate for the TypeScript tab.

Things you may want to reconcile

  • #3069 rewrites all four Python blocks in architecture.mdx and touches
    debugging.mdx. Its architecture.mdx replacement does not run on v2: it
    lands Client(read, write) and await client.discover(), where
    Client.__init__ takes one positional argument and Client.discover does not
    exist. If both land, the Python from this branch is the one that works.
    #3067 and #3069 also already conflict with each other on debugging.mdx,
    independently of this branch.
  • #3067 overlaps our debugging.mdx hunk. Take its Streamable HTTP paragraph
    and its warning wholesale and keep the lead-in sentence here.
  • #3070 reworked this authorization tutorial and solved the Python problem by
    removing the Python tab, but it merged into #3062's branch and #3062 was
    closed, so that work never reached the release branch. This change assumes the
    Python tab should exist and makes it correct. If you would rather it went
    away, say so and the authorization page can come out of this branch.
  • The model identifier build-client.mdx passes to the API is past the
    end-of-life date its own client library warns about, and that deprecation
    warning fires on every run. The same string sits in all eight language tabs,
    so refreshing it is a page-wide change rather than a Python-only one, but it
    is worth doing before release.
  • The linked complete-code repositories are now behind these pages.
    quickstart-resources/weather-server-python/weather.py still imports
    mcp.server.fastmcp, which does not exist in v2, and
    mcp-client-python/client.py still holds the ClientSession version. Both
    were already stale; this widens the gap. Every language tab links to the same
    repository, so it needs its own change regardless.

AI Disclaimer

The Python tabs across the release-branch guides were written against the v1
SDK. Two of them no longer run at all: the authorization tutorial imports
`mcp.server.fastmcp`, which v2 deleted, and the debugging page calls
`ctx.session.send_log_message`, where v2's `Context` has no `session`. The
rest execute but teach shapes v2 replaced.

- build-server: `from mcp.server import MCPServer`, and httpx2 in place of
  httpx. The SDK depends on httpx2, so `uv add "mcp[cli]"` already brings it
  in and the install line no longer needs to name an HTTP library. The stdio
  logging guidance moves to a module logger.
- build-client: rebuilt on the high-level `Client` instead of `ClientSession`
  plus `AsyncExitStack`, which removes the connect and cleanup pair entirely.
  Tool schemas are read as `tool.input_schema`, and tool results are narrowed
  to text blocks with `is_error` handed to the model rather than raised.
- architecture: the four pseudo-code blocks use `Client`, and the notification
  one follows changes with `client.listen(...)`.
- debugging: standard library logging. The protocol logging capability is
  deprecated at 2026-07-28 and the SDK marks its `Context.log` deprecated
  alongside it.
- authorization: `MCPServer`, with host, port and path moved from the
  constructor to `run()`, and httpx2 in the token verifier.
- oauth-client-credentials: both snippets wrapped in `main()` so they run as
  pasted rather than raising a SyntaxError.

Each block was assembled into the file a reader would actually create and
executed against the shipping v2 SDK over stdio. The build-client tutorial was
run against the build-server tutorial's server: it lists the tools, calls one,
and negotiates 2026-07-28.

Only the Python tabs changed. The other language tabs are untouched.

No-Verification-Needed: docs-only change, examples driven end-to-end instead
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Jul 24, 2026
@maxisbey
maxisbey marked this pull request as ready for review July 25, 2026 22:28
@maxisbey
maxisbey requested review from a team as code owners July 25, 2026 22:28
Comment thread docs/docs/draft/develop/build-client.mdx Outdated
The model id in the build-client examples was past its end-of-life date, so
the Anthropic client warns on every run and a reader copying the page may get an
error instead of a working chatbot. Move all eight language tabs to
claude-opus-5.

No-Verification-Needed: docs-only string swap
@olaservo

Copy link
Copy Markdown
Member

Btw, I took at pass at making updates to the quick start repos, including the Python one: modelcontextprotocol/quickstart-resources#164

I referred to this doc PR to align them. It looks like the main difference between the quick start and docs is that the quick start repos add a few more details like multiple turns, different outputs, etc.

Eventually, I think it could make sense to consolidate separate things like the quick starts into skills that also point to the docs and sdk examples instead.

@localden
localden merged commit 4bdd125 into modelcontextprotocol:docs/2026-07-28-release Jul 27, 2026
4 checks passed
@claude claude Bot mentioned this pull request Jul 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants