diff --git a/README.md b/README.md index 310bb35b..8de0d798 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,28 @@ mcp = FastMCP("My App") # Specify dependencies for deployment and development mcp = FastMCP("My App", dependencies=["pandas", "numpy"]) + +# Add lifespan support for startup/shutdown +@asynccontextmanager +async def app_lifespan(server: FastMCP) -> AsyncIterator[dict]: + """Manage application lifecycle""" + try: + # Initialize on startup + await db.connect() + yield {"db": db} + finally: + # Cleanup on shutdown + await db.disconnect() + +# Pass lifespan to server +mcp = FastMCP("My App", lifespan=app_lifespan) + +# Access lifespan context in tools +@mcp.tool() +def query_db(ctx: Context) -> str: + """Tool that uses initialized resources""" + db = ctx.request_context.lifespan_context["db"] + return db.query() ``` ### Resources @@ -334,7 +356,39 @@ def query_data(sql: str) -> str: ### Low-Level Server -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: +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: + +```python +from contextlib import asynccontextmanager +from typing import AsyncIterator + +@asynccontextmanager +async def server_lifespan(server: Server) -> AsyncIterator[dict]: + """Manage server startup and shutdown lifecycle.""" + try: + # Initialize resources on startup + await db.connect() + yield {"db": db} + finally: + # Clean up on shutdown + await db.disconnect() + +# Pass lifespan to server +server = Server("example-server", lifespan=server_lifespan) + +# Access lifespan context in handlers +@server.call_tool() +async def query_db(name: str, arguments: dict) -> list: + ctx = server.request_context + db = ctx.lifespan_context["db"] + return await db.query(arguments["query"]) +``` + +The lifespan API provides: +- A way to initialize resources when the server starts and clean them up when it stops +- Access to initialized resources through the request context in handlers +- Support for both low-level Server and FastMCP classes +- Type-safe context passing between lifespan and request handlers ```python from mcp.server.lowlevel import Server, NotificationOptions diff --git a/tests/server/fastmcp/test_func_metadata.py b/tests/server/fastmcp/test_func_metadata.py index b68fb902..6461648e 100644 --- a/tests/server/fastmcp/test_func_metadata.py +++ b/tests/server/fastmcp/test_func_metadata.py @@ -236,7 +236,7 @@ async def test_lambda_function(): def test_complex_function_json_schema(): """Test JSON schema generation for complex function arguments. - + Note: Different versions of pydantic output slightly different JSON Schema formats for model fields with defaults. The format changed in 2.9.0: @@ -245,16 +245,16 @@ def test_complex_function_json_schema(): "allOf": [{"$ref": "#/$defs/Model"}], "default": {} } - + 2. Since 2.9.0: { "$ref": "#/$defs/Model", "default": {} } - + Both formats are valid and functionally equivalent. This test accepts either format to ensure compatibility across our supported pydantic versions. - + This change in format does not affect runtime behavior since: 1. Both schemas validate the same way 2. The actual model classes and validation logic are unchanged @@ -262,17 +262,17 @@ def test_complex_function_json_schema(): """ meta = func_metadata(complex_arguments_fn) actual_schema = meta.arg_model.model_json_schema() - + # Create a copy of the actual schema to normalize normalized_schema = actual_schema.copy() - + # Normalize the my_model_a_with_default field to handle both pydantic formats - if 'allOf' in actual_schema['properties']['my_model_a_with_default']: - normalized_schema['properties']['my_model_a_with_default'] = { - '$ref': '#/$defs/SomeInputModelA', - 'default': {} + if "allOf" in actual_schema["properties"]["my_model_a_with_default"]: + normalized_schema["properties"]["my_model_a_with_default"] = { + "$ref": "#/$defs/SomeInputModelA", + "default": {}, } - + assert normalized_schema == { "$defs": { "InnerModel": {