Skip to content

Commit 712a160

Browse files
fix: update MCPServer lifespan example for API consistency
ROOT CAUSE: The MCPServer lifespan example was showing the dual-lifespan API (server_lifespan + session_lifespan) which is only for the lowlevel Server API. MCPServer uses a single 'lifespan' parameter mapped to session_lifespan internally. CHANGES: - Reverted lifespan_example.py to use single 'lifespan' parameter - Updated context access to use session_lifespan_context (correct for MCPServer) - Regenerated README.v2.md code snippets via update_readme_snippets.py IMPACT: - Fixes CI failure in README snippet validation - Documents correct MCPServer API usage - Lowlevel Server API (with dual lifespans) remains documented separately Github-Issue:#2113
1 parent 710746f commit 712a160

File tree

2 files changed

+103
-60
lines changed

2 files changed

+103
-60
lines changed

README.v2.md

Lines changed: 102 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -222,12 +222,11 @@ The MCPServer server is your core interface to the MCP protocol. It handles conn
222222

223223
<!-- snippet-source examples/snippets/servers/lifespan_example.py -->
224224
```python
225-
"""Example showing lifespan support with server and session scopes."""
225+
"""Example showing lifespan support for startup/shutdown with strong typing."""
226226

227227
from collections.abc import AsyncIterator
228228
from contextlib import asynccontextmanager
229229
from dataclasses import dataclass
230-
from typing import TypedDict
231230

232231
from mcp.server.mcpserver import Context, MCPServer
233232

@@ -236,75 +235,48 @@ from mcp.server.mcpserver import Context, MCPServer
236235
class Database:
237236
"""Mock database class for example."""
238237

239-
connections: int = 0
240-
241238
@classmethod
242239
async def connect(cls) -> "Database":
243-
"""Connect to database (runs once at server startup)."""
244-
cls.connections += 1
240+
"""Connect to database."""
245241
return cls()
246242

247243
async def disconnect(self) -> None:
248244
"""Disconnect from database."""
249-
cls.connections -= 1
245+
pass
250246

251247
def query(self) -> str:
252248
"""Execute a query."""
253249
return "Query result"
254250

255251

256-
class ServerContext(TypedDict):
257-
"""Server-level context (shared across all clients)."""
252+
@dataclass
253+
class AppContext:
254+
"""Application context with typed dependencies."""
258255

259256
db: Database
260257

261258

262-
class SessionContext(TypedDict):
263-
"""Session-level context (per-client connection)."""
264-
265-
session_id: str
266-
267-
268259
@asynccontextmanager
269-
async def server_lifespan(server: MCPServer) -> AsyncIterator[ServerContext]:
270-
"""Manage server-level lifecycle (runs once at startup).
271-
272-
Use for: database pools, ML models, shared caches, global config.
273-
"""
274-
# Initialize on startup (ONCE for all clients)
260+
async def app_lifespan(server: MCPServer) -> AsyncIterator[AppContext]:
261+
"""Manage application lifecycle with type-safe context."""
262+
# Initialize on startup
275263
db = await Database.connect()
276264
try:
277-
yield ServerContext(db=db)
265+
yield AppContext(db=db)
278266
finally:
279267
# Cleanup on shutdown
280268
await db.disconnect()
281269

282270

283-
@asynccontextmanager
284-
async def session_lifespan(server: MCPServer) -> AsyncIterator[SessionContext]:
285-
"""Manage session-level lifecycle (runs per-client connection).
286-
287-
Use for: user auth, per-client state, session IDs.
288-
"""
289-
# Initialize per-client (runs FOR EACH CLIENT)
290-
session_id = "unique-session-id"
291-
try:
292-
yield SessionContext(session_id=session_id)
293-
finally:
294-
pass # Cleanup per-client resources
295-
296-
297-
# Pass both lifespans to server
298-
mcp = MCPServer("My App", lifespan=server_lifespan) # MCPServer uses server lifespan
271+
# Pass lifespan to server
272+
mcp = MCPServer("My App", lifespan=app_lifespan)
299273

300274

301-
# Access type-safe contexts in tools
275+
# Access type-safe lifespan context in tools
302276
@mcp.tool()
303-
def query_db(ctx: Context) -> str:
277+
def query_db(ctx: Context[AppContext]) -> str:
304278
"""Tool that uses initialized resources."""
305-
# Access server-level context (shared across all clients)
306-
server_ctx: ServerContext = ctx.request_context.session_lifespan_context
307-
db = server_ctx.db
279+
db = ctx.request_context.session_lifespan_context.db
308280
return db.query()
309281
```
310282

@@ -1684,6 +1656,7 @@ uv run examples/snippets/servers/lowlevel/lifespan.py
16841656
from collections.abc import AsyncIterator
16851657
from contextlib import asynccontextmanager
16861658
from typing import TypedDict
1659+
from uuid import uuid4
16871660

16881661
import mcp.server.stdio
16891662
from mcp import types
@@ -1694,71 +1667,141 @@ from mcp.server import Server, ServerRequestContext
16941667
class Database:
16951668
"""Mock database class for example."""
16961669

1670+
connections: int = 0
1671+
16971672
@classmethod
16981673
async def connect(cls) -> "Database":
16991674
"""Connect to database."""
1700-
print("Database connected")
1675+
cls.connections += 1
1676+
print(f"Database connected (total connections: {cls.connections})")
17011677
return cls()
17021678

17031679
async def disconnect(self) -> None:
17041680
"""Disconnect from database."""
1705-
print("Database disconnected")
1681+
self.connections -= 1
1682+
print(f"Database disconnected (total connections: {self.connections})")
17061683

17071684
async def query(self, query_str: str) -> list[dict[str, str]]:
17081685
"""Execute a query."""
17091686
# Simulate database query
17101687
return [{"id": "1", "name": "Example", "query": query_str}]
17111688

17121689

1713-
class AppContext(TypedDict):
1690+
class ServerContext(TypedDict):
1691+
"""Server-level context (shared across all clients)."""
1692+
17141693
db: Database
17151694

17161695

1696+
class SessionContext(TypedDict):
1697+
"""Session-level context (per-client connection)."""
1698+
1699+
session_id: str
1700+
1701+
17171702
@asynccontextmanager
1718-
async def server_lifespan(_server: Server[AppContext]) -> AsyncIterator[AppContext]:
1719-
"""Manage server startup and shutdown lifecycle."""
1703+
async def server_lifespan(_server: Server) -> AsyncIterator[ServerContext]:
1704+
"""Manage server startup and shutdown lifecycle.
1705+
1706+
This runs ONCE when the server process starts, before any clients connect.
1707+
Use this for resources that should be shared across all client connections:
1708+
- Database connection pools
1709+
- Machine learning models
1710+
- Shared caches
1711+
- Global configuration
1712+
"""
1713+
print("[SERVER LIFESPAN] Starting server...")
17201714
db = await Database.connect()
17211715
try:
1716+
print("[SERVER LIFESPAN] Server started, database connected")
17221717
yield {"db": db}
17231718
finally:
17241719
await db.disconnect()
1720+
print("[SERVER LIFESPAN] Server stopped, database disconnected")
1721+
1722+
1723+
@asynccontextmanager
1724+
async def session_lifespan(_server: Server) -> AsyncIterator[SessionContext]:
1725+
"""Manage per-client session lifecycle.
1726+
1727+
This runs FOR EACH CLIENT that connects to the server.
1728+
Use this for resources that are specific to a single client connection:
1729+
- User authentication context
1730+
- Per-client transaction state
1731+
- Client-specific caches
1732+
- Session identifiers
1733+
"""
1734+
session_id = str(uuid4())
1735+
print(f"[SESSION LIFESPAN] Session {session_id} started")
1736+
try:
1737+
yield {"session_id": session_id}
1738+
finally:
1739+
print(f"[SESSION LIFESPAN] Session {session_id} stopped")
17251740

17261741

17271742
async def handle_list_tools(
1728-
ctx: ServerRequestContext[AppContext], params: types.PaginatedRequestParams | None
1743+
ctx: ServerRequestContext[ServerContext, SessionContext],
1744+
params: types.PaginatedRequestParams | None,
17291745
) -> types.ListToolsResult:
17301746
"""List available tools."""
17311747
return types.ListToolsResult(
17321748
tools=[
17331749
types.Tool(
17341750
name="query_db",
1735-
description="Query the database",
1751+
description="Query the database (uses shared server connection)",
17361752
input_schema={
17371753
"type": "object",
17381754
"properties": {"query": {"type": "string", "description": "SQL query to execute"}},
17391755
"required": ["query"],
17401756
},
1741-
)
1757+
),
1758+
types.Tool(
1759+
name="get_session_info",
1760+
description="Get information about the current session",
1761+
input_schema={
1762+
"type": "object",
1763+
"properties": {},
1764+
},
1765+
),
17421766
]
17431767
)
17441768

17451769

17461770
async def handle_call_tool(
1747-
ctx: ServerRequestContext[AppContext], params: types.CallToolRequestParams
1771+
ctx: ServerRequestContext[ServerContext, SessionContext],
1772+
params: types.CallToolRequestParams,
17481773
) -> types.CallToolResult:
1749-
"""Handle database query tool call."""
1750-
if params.name != "query_db":
1751-
raise ValueError(f"Unknown tool: {params.name}")
1774+
"""Handle tool calls."""
1775+
if params.name == "query_db":
1776+
# Access server-level resource (shared database connection)
1777+
db = ctx.server_lifespan_context["db"]
1778+
results = await db.query((params.arguments or {})["query"])
1779+
1780+
return types.CallToolResult(
1781+
content=[
1782+
types.TextContent(
1783+
type="text",
1784+
text=f"Query results (session {ctx.session_lifespan_context['session_id']}): {results}",
1785+
)
1786+
]
1787+
)
17521788

1753-
db = ctx.lifespan_context["db"]
1754-
results = await db.query((params.arguments or {})["query"])
1789+
if params.name == "get_session_info":
1790+
# Access session-level resource (session ID)
1791+
session_id = ctx.session_lifespan_context["session_id"]
17551792

1756-
return types.CallToolResult(content=[types.TextContent(type="text", text=f"Query results: {results}")])
1793+
return types.CallToolResult(
1794+
content=[types.TextContent(type="text", text=f"Your session ID: {session_id}")]
1795+
)
1796+
1797+
raise ValueError(f"Unknown tool: {params.name}")
17571798

17581799

1800+
# Create server with BOTH server and session lifespans
17591801
server = Server(
17601802
"example-server",
1761-
lifespan=server_lifespan,
1803+
server_lifespan=server_lifespan, # Runs once at server startup
1804+
session_lifespan=session_lifespan, # Runs per-client connection
17621805
on_list_tools=handle_list_tools,
17631806
on_call_tool=handle_call_tool,
17641807
)

examples/snippets/servers/lifespan_example.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,5 +52,5 @@ async def app_lifespan(server: MCPServer) -> AsyncIterator[AppContext]:
5252
@mcp.tool()
5353
def query_db(ctx: Context[AppContext]) -> str:
5454
"""Tool that uses initialized resources."""
55-
db = ctx.request_context.lifespan_context.db
55+
db = ctx.request_context.session_lifespan_context.db
5656
return db.query()

0 commit comments

Comments
 (0)