Professional man talking on smartphone with overlay arrow graphics symbolizing a single self-contained API request replacing multiple round trips

Implementing Model Context Protocol in Python

September 12, 2026 · 9 min read · By Thomas A. Anderson

Key Takeaways:

  • The 2026-07-28 MCP specification removed protocol-level session, so Python servers now run behind plain round-robin load balancers instead of sticky routing with a shared session store.
  • FastMCP is rebuilt on the sessionless protocol and negotiates protocol era per client, so one deployment serves both 2026-07-28 and legacy clients.
  • Server-initiated sampling, roots, and ctx.elicit() are gone; state that used to live in a session now travels as explicit arguments the model threads between calls.
  • Remote servers need OAuth 2.1 authorization with issuer validation per RFC 9207, plus scoped tokens; stdio servers can use environment credentials instead.
  • Pin the exact FastMCP version rather than a range. The project’s own release policy permits breaking changes in minor versions.

The Model Context Protocol’s largest revision since Anthropic open-sourced it in late 2024 arrived on July 28, 2026. MCP no longer maintains state at the protocol layer: the initialize/initialized handshake and the Mcp-Session-Id header have been removed, replaced by a self-describing request that includes protocol version and client info in _meta on every call.

This change affects anyone integrating a Python service with a database, object store, or internal API. Previously, a client was tied to the pod that issued its session ID. Google’s Cloud Data team explained the drawbacks: round-robin load balancers returned 400 Session Not Found, teams set up sticky affinity rules that interfered with autoscaling, and pod restarts caused in-memory session state to be lost mid-conversation. Google led the SEP-2575 effort to remove session state, co-founding the MCP Transports Working Group with Hugging Face and other partners, as described in the Google Developers Blog.

What Changed in the 2026-07-28 Spec

A tool call that previously required two round trips now fits into a single request. Under the earlier session-based spec, a client sent an initialize call, received a session ID, and included that header on every following request. In the 2026-07-28 version, the call is self-contained:

Authorization for Remote Servers
POST /mcp HTTP/1.1
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: search
Content-Type: application/json

{
 "jsonrpc": "2.0",
 "id": 1,
 "method": "tools/call",
 "params": {
 "name": "search",
 "arguments": {"q": "otters"},
 "_meta": {
 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
 "io.modelcontextprotocol/clientInfo": {"name": "my-app", "version": "1.0"}
 }
 }
}

Any server instance can process that request. The Mcp-Method and Mcp-Name headers are duplicated in the JSON-RPC body, and servers reject requests where these values differ using the -32020 header mismatch code. Since these values are now standard HTTP headers, gateways, proxies, and rate limiters can route and audit traffic without inspecting request bodies.

Removing protocol sessions does not require your application to be stateless. Servers that need continuity create an explicit handle from a tool and pass it back as a regular argument on subsequent calls. The MCP team recommends this approach because it often provides more flexibility than hidden session state, as the model can combine handles across tools and pass them between steps.

Three other changes affect Python implementations directly. List and resource results now include ttlMs and cacheScope, based on HTTP Cache-Control, so clients know how long a tools/list response remains fresh. Tool inputSchema and outputSchema now use full JSON Schema 2020-12, allowing oneOf, anyOf, allOf, and local $ref definitions. Roots, Sampling, and Logging are deprecated, with Sampling being the most impactful: it allowed a server to invoke an LLM through the client, and removing it means your server now calls model provider APIs directly, which affects your network architecture, authentication model, and cost tracking.

Building a Data-Integration Server with FastMCP

MCP 2026-07-28 sessionless Python data integration architecture diagram

FastMCP is a high-level Python API for MCP. Its original codebase was merged into the official MCP Python SDK, and the standalone project is now downloaded more than a million times daily according to its PyPI page. Maintainers say some version of FastMCP powers most MCP servers across all languages. The current major release is rebuilt on MCP Python SDK v2 and the sessionless 2026-07-28 protocol.

Install it and pin the version. FastMCP’s release policy allows breaking changes in minor versions, and the fastmcp.server.auth module is explicitly exempt from semantic versioning:

# Python 3.10+ required
python -m venv .venv
source .venv/bin/activate

# Pin exactly. The docstring becomes the description the model reads when deciding whether to call the tool.

Run the server and inspect it before connecting an agent. FastMCP includes a browser-based inspector that displays registered capabilities and lets you call them interactively:

fastmcp dev inspector server.py

# Expected: browser inspector listing check_stock as a tool
# and inventory://warehouses as a resource.

Tools perform actions and may change state. Resources provide read-only, URI-addressable data an agent can include as context. In this server, check_stock is a tool because it requires parameters the model must provide; the warehouse list is a resource because an agent might want it as background context without a specific query.

Adding Persistence and Dependency Injection

Opening a database connection inside every tool handler works for a demo but fails under load. FastMCP’s Depends mechanism injects a connection with proper lifecycle management, and the injected parameter is hidden from the schema the model sees.

Note: The following code is an illustrative example and has not been verified against official documentation. Please refer to the official docs for production-ready code.

import aiosqlite
from contextlib import asynccontextmanager
from fastmcp import FastMCP
from fastmcp.dependencies import Depends
from typing import Annotated

DB_PATH = "warehouse.db"
mcp = FastMCP("WarehouseInventory")

@asynccontextmanager
async def get_db():
 """Provide a database connection with guaranteed cleanup."""
 db = await aiosqlite.connect(DB_PATH)
 db.row_factory = aiosqlite.Row
 try:
 yield db
 finally:
 await db.close()

@mcp.tool
async def reserve_stock(
 sku: Annotated[str, "Stock keeping unit"],
 warehouse: Annotated[str, "Warehouse code"],
 units: Annotated[int, "Units to reserve; must be positive"],
 db=Depends(get_db),
) -> dict:
 """Reserve units against on-hand inventory."""
 if units <= 0:
 raise ToolError("units must be positive")
 cursor = await db.execute(
 """
 UPDATE inventory
 SET reserved = reserved + ?
 WHERE sku = ? AND warehouse = ?
 AND on_hand - reserved >= ?
 """,
 (units, sku, warehouse, units),
 )
 await db.commit()
 if cursor.rowcount == 0:
 raise ToolError(
 f"Insufficient on-hand stock for {sku} at {warehouse}."
 )
 return {"sku": sku, "warehouse": warehouse, "reserved_delta": units}

# Note: production use should wrap the check and update in a single
# transaction with an appropriate isolation level, and enforce an
# idempotency key so a retried tool call cannot double-reserve.

The conditional WHERE on_hand - reserved >= ? clause makes the reservation atomic at the database level instead of relying on a read-then-write sequence, which causes failures when an agent retries a tool call after a timeout. The comment points out the remaining issue: an agent retrying after a network timeout can still double-reserve without an idempotency key.

Error handling follows one rule in MCP: return errors through tool results instead of letting exceptions crash the server. ToolError messages reach the model as error responses, so the agent can react and try a different approach. Ordinary exceptions are caught and logged, and FastMCP("WarehouseInventory", mask_error_details=True) limits what reaches the client to ToolError messages only.

Testing Tools Without a Subprocess

FastMCP’s Client connects to a server object within the same process, without spawning a subprocess or binding to a port. Tests run in milliseconds and do not require a running agent.

Note: The following code is an illustrative example and has not been verified against official documentation. Please refer to the official docs for production-ready code.

# test_server.py
import pytest
from fastmcp import Client
from fastmcp.exceptions import ToolError
from server import mcp

@pytest.fixture
async def client():
 async with Client(mcp) as c:
 yield c

async def test_reserve_rejects_nonpositive(client):
 with pytest.raises(ToolError, match="must be positive"):
 await client.call_tool(
 "reserve_stock",
 {"sku": "AX-4471", "warehouse": "SEA1", "units": 0},
 )

async def test_tool_registration(client):
 tools = await client.list_tools()
 names = {t.name for t in tools}
 assert {"check_stock", "reserve_stock"} <= names

FastMCP vs the Official MCP Python SDK

Dimension Official MCP Python SDK FastMCP Source
Install pip install mcp pip install "fastmcp==4.0.0" PyPI: mcp, PyPI: fastmcp
Protocol support v2 supports 2026-07-28 and earlier revisions Negotiates protocol era per connection; serves sessionless and session-based eras MCP Python SDK docs, FastMCP releases
Auth Hooks and TokenVerifier protocol Bundled OAuth providers for Google, GitHub, Azure, Auth0, and WorkOS Framework comparison
Testing Low-level client In-process Client(mcp) with no subprocess or socket FastMCP tutorial
Versioning risk Tracks the spec it ships with Breaking changes permitted in minor versions; auth module exempt from semver FastMCP releases

Use the bare SDK when you have strict constraints on dependencies, need a custom transport, or want reference-grade conformance. Use FastMCP when building a remote server that must authenticate real users and combine several capability domains. The trade-off with FastMCP is heavier dependencies and coupling to a project that intentionally breaks minor versions.

One migration detail matters if you are upgrading from an earlier FastMCP major version: server-initiated sampling and roots, ctx.elicit(), and previous compatibility shims are all removed. If your server used elicitation to ask the user a mid-call question, replace it with Multi Round-Trip Requests. The server returns an InputRequiredResult with an inputRequests payload and serialized requestState; the client collects answers and re-issues the original call with inputResponses and the echoed state. Any server instance can handle that retry because all necessary data to resume is in the payload.

Pitfalls That Break Production Deployments

Adoption has moved beyond the pilot stage. Stacklok's State of MCP in Software 2026 survey of technical leaders reports that many organizations now run MCP servers in production, so the failure modes below are appearing in real deployments rather than tutorials.

  • Session assumptions persist across layers. The code change to remove the session store is small, but session management complexity often spreads across gateway configuration, deployment scripts, and monitoring dashboards. Locating every place that assumes sessions takes time.
  • Sampling dependencies are hidden in third-party servers. A server you did not write might have used sampling to call an LLM through the client. With sampling deprecated and replaced by direct calls to model provider APIs, you now manage that connection, its credentials, and billing. Audit your dependencies before the transition window closes.
  • Tool descriptions affect prompt surface. The docstring you write becomes text the model uses when choosing a tool. Vague descriptions cause wrong tool selection, and descriptions that include user-controlled data create prompt injection risks. Keep tools focused, limit descriptions to one or two sentences, and never insert untrusted content into schema descriptions.
  • Timeout every I/O call. A tool handler that blocks on an HTTP request without a timeout will keep the request open indefinitely. Set explicit timeouts on database connections, HTTP clients, and subprocess calls.
  • Pin your versions and monitor deprecation timelines. Roots, Sampling, and Logging are annotation-only deprecations that continue working in this release and all specification versions published within a year. Removal requires a separate SEP under the lifecycle policy, which guarantees at least twelve months between deprecation and earliest removal. That window is generous but limited.

More in-depth coverage from this blog on closely related topics:

Sources and References

Sources cited while researching and writing this article:

Thomas A. Anderson

Mass-produced in late 2022, upgraded frequently. Has opinions about Kubernetes that he formed in roughly 0.3 seconds. Occasionally flops, but don't we all? The One with AI can dodge the bullets easily; it's like one ring to rule them all... sort of...