Skip to content

Commit 91d9271

Browse files
committed
docs: update README with lifespan examples and usage
Add comprehensive documentation for lifespan support: - Add usage examples for both Server and FastMPC classes - Document startup/shutdown patterns - Show context access in tools and handlers - Clean up spacing in test files 🤖 Generated with Claude CLI. Co-Authored-By: Claude <[email protected]>
1 parent 1c4a896 commit 91d9271

File tree

2 files changed

+66
-12
lines changed

2 files changed

+66
-12
lines changed

README.md

+55-1
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,28 @@ mcp = FastMCP("My App")
135135

136136
# Specify dependencies for deployment and development
137137
mcp = FastMCP("My App", dependencies=["pandas", "numpy"])
138+
139+
# Add lifespan support for startup/shutdown
140+
@asynccontextmanager
141+
async def app_lifespan(server: FastMCP) -> AsyncIterator[dict]:
142+
"""Manage application lifecycle"""
143+
try:
144+
# Initialize on startup
145+
await db.connect()
146+
yield {"db": db}
147+
finally:
148+
# Cleanup on shutdown
149+
await db.disconnect()
150+
151+
# Pass lifespan to server
152+
mcp = FastMCP("My App", lifespan=app_lifespan)
153+
154+
# Access lifespan context in tools
155+
@mcp.tool()
156+
def query_db(ctx: Context) -> str:
157+
"""Tool that uses initialized resources"""
158+
db = ctx.request_context.lifespan_context["db"]
159+
return db.query()
138160
```
139161

140162
### Resources
@@ -334,7 +356,39 @@ def query_data(sql: str) -> str:
334356

335357
### Low-Level Server
336358

337-
For more control, you can use the low-level server implementation directly. This gives you full access to the protocol and allows you to customize every aspect of your server:
359+
For more control, you can use the low-level server implementation directly. This gives you full access to the protocol and allows you to customize every aspect of your server, including lifecycle management through the lifespan API:
360+
361+
```python
362+
from contextlib import asynccontextmanager
363+
from typing import AsyncIterator
364+
365+
@asynccontextmanager
366+
async def server_lifespan(server: Server) -> AsyncIterator[dict]:
367+
"""Manage server startup and shutdown lifecycle."""
368+
try:
369+
# Initialize resources on startup
370+
await db.connect()
371+
yield {"db": db}
372+
finally:
373+
# Clean up on shutdown
374+
await db.disconnect()
375+
376+
# Pass lifespan to server
377+
server = Server("example-server", lifespan=server_lifespan)
378+
379+
# Access lifespan context in handlers
380+
@server.call_tool()
381+
async def query_db(name: str, arguments: dict) -> list:
382+
ctx = server.request_context
383+
db = ctx.lifespan_context["db"]
384+
return await db.query(arguments["query"])
385+
```
386+
387+
The lifespan API provides:
388+
- A way to initialize resources when the server starts and clean them up when it stops
389+
- Access to initialized resources through the request context in handlers
390+
- Support for both low-level Server and FastMCP classes
391+
- Type-safe context passing between lifespan and request handlers
338392

339393
```python
340394
from mcp.server.lowlevel import Server, NotificationOptions

tests/server/fastmcp/test_func_metadata.py

+11-11
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ async def check_call(args):
236236

237237
def test_complex_function_json_schema():
238238
"""Test JSON schema generation for complex function arguments.
239-
239+
240240
Note: Different versions of pydantic output slightly different
241241
JSON Schema formats for model fields with defaults. The format changed in 2.9.0:
242242
@@ -245,34 +245,34 @@ def test_complex_function_json_schema():
245245
"allOf": [{"$ref": "#/$defs/Model"}],
246246
"default": {}
247247
}
248-
248+
249249
2. Since 2.9.0:
250250
{
251251
"$ref": "#/$defs/Model",
252252
"default": {}
253253
}
254-
254+
255255
Both formats are valid and functionally equivalent. This test accepts either format
256256
to ensure compatibility across our supported pydantic versions.
257-
257+
258258
This change in format does not affect runtime behavior since:
259259
1. Both schemas validate the same way
260260
2. The actual model classes and validation logic are unchanged
261261
3. func_metadata uses model_validate/model_dump, not the schema directly
262262
"""
263263
meta = func_metadata(complex_arguments_fn)
264264
actual_schema = meta.arg_model.model_json_schema()
265-
265+
266266
# Create a copy of the actual schema to normalize
267267
normalized_schema = actual_schema.copy()
268-
268+
269269
# Normalize the my_model_a_with_default field to handle both pydantic formats
270-
if 'allOf' in actual_schema['properties']['my_model_a_with_default']:
271-
normalized_schema['properties']['my_model_a_with_default'] = {
272-
'$ref': '#/$defs/SomeInputModelA',
273-
'default': {}
270+
if "allOf" in actual_schema["properties"]["my_model_a_with_default"]:
271+
normalized_schema["properties"]["my_model_a_with_default"] = {
272+
"$ref": "#/$defs/SomeInputModelA",
273+
"default": {},
274274
}
275-
275+
276276
assert normalized_schema == {
277277
"$defs": {
278278
"InnerModel": {

0 commit comments

Comments
 (0)