-
Notifications
You must be signed in to change notification settings - Fork 2.1k
README - replace code snippets with examples - add lowlevel to snippets #1150
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1037,17 +1037,49 @@ For more information on mounting applications in Starlette, see the [Starlette d | |
|
||
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: | ||
|
||
<!-- snippet-source examples/snippets/servers/lowlevel/lifespan.py --> | ||
```python | ||
from contextlib import asynccontextmanager | ||
"""Low-level server example showing lifespan management. | ||
|
||
This example demonstrates how to use the lifespan API to manage | ||
server startup and shutdown, including resource initialization | ||
and cleanup. | ||
|
||
Run from the repository root: | ||
uv run examples/snippets/servers/lowlevel/lifespan.py | ||
""" | ||
|
||
from collections.abc import AsyncIterator | ||
from contextlib import asynccontextmanager | ||
|
||
import mcp.server.stdio | ||
import mcp.types as types | ||
from mcp.server.lowlevel import NotificationOptions, Server | ||
from mcp.server.models import InitializationOptions | ||
|
||
|
||
# Mock database class for example | ||
class Database: | ||
"""Mock database class for example.""" | ||
|
||
from fake_database import Database # Replace with your actual DB type | ||
@classmethod | ||
async def connect(cls) -> "Database": | ||
"""Connect to database.""" | ||
print("Database connected") | ||
return cls() | ||
|
||
from mcp.server import Server | ||
async def disconnect(self) -> None: | ||
"""Disconnect from database.""" | ||
print("Database disconnected") | ||
|
||
async def query(self, query_str: str) -> list[dict[str, str]]: | ||
"""Execute a query.""" | ||
# Simulate database query | ||
return [{"id": "1", "name": "Example", "query": query_str}] | ||
|
||
|
||
@asynccontextmanager | ||
async def server_lifespan(server: Server) -> AsyncIterator[dict]: | ||
async def server_lifespan(_server: Server) -> AsyncIterator[dict]: | ||
"""Manage server startup and shutdown lifecycle.""" | ||
# Initialize resources on startup | ||
db = await Database.connect() | ||
|
@@ -1062,21 +1094,83 @@ async def server_lifespan(server: Server) -> AsyncIterator[dict]: | |
server = Server("example-server", lifespan=server_lifespan) | ||
|
||
|
||
# Access lifespan context in handlers | ||
@server.list_tools() | ||
async def handle_list_tools() -> list[types.Tool]: | ||
"""List available tools.""" | ||
return [ | ||
types.Tool( | ||
name="query_db", | ||
description="Query the database", | ||
inputSchema={ | ||
"type": "object", | ||
"properties": {"query": {"type": "string", "description": "SQL query to execute"}}, | ||
"required": ["query"], | ||
}, | ||
) | ||
] | ||
|
||
|
||
@server.call_tool() | ||
async def query_db(name: str, arguments: dict) -> list: | ||
async def query_db(name: str, arguments: dict) -> list[types.TextContent]: | ||
"""Handle database query tool call.""" | ||
if name != "query_db": | ||
raise ValueError(f"Unknown tool: {name}") | ||
|
||
# Access lifespan context | ||
ctx = server.request_context | ||
db = ctx.lifespan_context["db"] | ||
return await db.query(arguments["query"]) | ||
|
||
# Execute query | ||
results = await db.query(arguments["query"]) | ||
|
||
return [types.TextContent(type="text", text=f"Query results: {results}")] | ||
|
||
|
||
async def run(): | ||
"""Run the server with lifespan management.""" | ||
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): | ||
await server.run( | ||
read_stream, | ||
write_stream, | ||
InitializationOptions( | ||
server_name="example-server", | ||
server_version="0.1.0", | ||
capabilities=server.get_capabilities( | ||
notification_options=NotificationOptions(), | ||
experimental_capabilities={}, | ||
), | ||
), | ||
) | ||
|
||
|
||
if __name__ == "__main__": | ||
import asyncio | ||
|
||
asyncio.run(run()) | ||
``` | ||
|
||
_Full example: [examples/snippets/servers/lowlevel/lifespan.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/lowlevel/lifespan.py)_ | ||
<!-- /snippet-source --> | ||
|
||
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 | ||
- Type-safe context passing between lifespan and request handlers | ||
|
||
<!-- snippet-source examples/snippets/servers/lowlevel/basic.py --> | ||
```python | ||
"""Basic low-level server example. | ||
|
||
This example demonstrates the low-level server API with minimal setup, | ||
showing how to implement basic prompts using the raw protocol handlers. | ||
|
||
Run from the repository root: | ||
uv run examples/snippets/servers/lowlevel/basic.py | ||
""" | ||
|
||
import asyncio | ||
|
||
import mcp.server.stdio | ||
import mcp.types as types | ||
from mcp.server.lowlevel import NotificationOptions, Server | ||
|
@@ -1088,38 +1182,37 @@ server = Server("example-server") | |
|
||
@server.list_prompts() | ||
async def handle_list_prompts() -> list[types.Prompt]: | ||
"""List available prompts.""" | ||
return [ | ||
types.Prompt( | ||
name="example-prompt", | ||
description="An example prompt template", | ||
arguments=[ | ||
types.PromptArgument( | ||
name="arg1", description="Example argument", required=True | ||
) | ||
], | ||
arguments=[types.PromptArgument(name="arg1", description="Example argument", required=True)], | ||
) | ||
] | ||
|
||
|
||
@server.get_prompt() | ||
async def handle_get_prompt( | ||
name: str, arguments: dict[str, str] | None | ||
) -> types.GetPromptResult: | ||
async def handle_get_prompt(name: str, arguments: dict[str, str] | None) -> types.GetPromptResult: | ||
"""Get a specific prompt by name.""" | ||
if name != "example-prompt": | ||
raise ValueError(f"Unknown prompt: {name}") | ||
|
||
arg1_value = arguments.get("arg1", "default") if arguments else "default" | ||
|
||
return types.GetPromptResult( | ||
description="Example prompt", | ||
messages=[ | ||
types.PromptMessage( | ||
role="user", | ||
content=types.TextContent(type="text", text="Example prompt text"), | ||
content=types.TextContent(type="text", text=f"Example prompt text with argument: {arg1_value}"), | ||
) | ||
], | ||
) | ||
|
||
|
||
async def run(): | ||
"""Run the basic low-level server.""" | ||
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): | ||
await server.run( | ||
read_stream, | ||
|
@@ -1136,37 +1229,50 @@ async def run(): | |
|
||
|
||
if __name__ == "__main__": | ||
import asyncio | ||
|
||
asyncio.run(run()) | ||
``` | ||
|
||
_Full example: [examples/snippets/servers/lowlevel/basic.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/lowlevel/basic.py)_ | ||
<!-- /snippet-source --> | ||
|
||
Caution: The `uv run mcp run` and `uv run mcp dev` tool doesn't support low-level server. | ||
|
||
#### Structured Output Support | ||
|
||
The low-level server supports structured output for tools, allowing you to return both human-readable content and machine-readable structured data. Tools can define an `outputSchema` to validate their structured output: | ||
|
||
<!-- snippet-source examples/snippets/servers/lowlevel/structured_output.py --> | ||
```python | ||
from types import Any | ||
"""Low-level server example showing structured output support. | ||
|
||
This example demonstrates how to use the low-level server API to return | ||
structured data from tools, with automatic validation against output schemas. | ||
|
||
Run from the repository root: | ||
uv run examples/snippets/servers/lowlevel/structured_output.py | ||
""" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Those module docstring seems to be adding a lot of unnecessary content to the README, given that you can run each of those individually. |
||
|
||
import asyncio | ||
from typing import Any | ||
|
||
import mcp.server.stdio | ||
import mcp.types as types | ||
from mcp.server.lowlevel import Server | ||
from mcp.server.lowlevel import NotificationOptions, Server | ||
from mcp.server.models import InitializationOptions | ||
|
||
server = Server("example-server") | ||
|
||
|
||
@server.list_tools() | ||
async def list_tools() -> list[types.Tool]: | ||
"""List available tools with structured output schemas.""" | ||
return [ | ||
types.Tool( | ||
name="calculate", | ||
description="Perform mathematical calculations", | ||
inputSchema={ | ||
"type": "object", | ||
"properties": { | ||
"expression": {"type": "string", "description": "Math expression"} | ||
}, | ||
"properties": {"expression": {"type": "string", "description": "Math expression"}}, | ||
"required": ["expression"], | ||
}, | ||
outputSchema={ | ||
|
@@ -1183,10 +1289,12 @@ async def list_tools() -> list[types.Tool]: | |
|
||
@server.call_tool() | ||
async def call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]: | ||
"""Handle tool calls with structured output.""" | ||
if name == "calculate": | ||
expression = arguments["expression"] | ||
try: | ||
result = eval(expression) # Use a safe math parser | ||
# WARNING: eval() is dangerous! Use a safe math parser in production | ||
result = eval(expression) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know we have the comment warning people, but maybe we can have an example that doesn't use it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree, maybe something like: from sympy import sympify
...
result = sympify(expression).evalf() (and the extra dep can be added as uv preamble w/ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. just changed an example to weathers, same as we have for high level server API |
||
structured = {"result": result, "expression": expression} | ||
|
||
# low-level server will validate structured output against the tool's | ||
|
@@ -1195,8 +1303,34 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]: | |
return structured | ||
except Exception as e: | ||
raise ValueError(f"Calculation error: {str(e)}") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Out of scope, but this shouldn't be necessary, since the exception is just bubbled up? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And/or, maybe always |
||
else: | ||
raise ValueError(f"Unknown tool: {name}") | ||
|
||
|
||
async def run(): | ||
"""Run the structured output server.""" | ||
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): | ||
await server.run( | ||
read_stream, | ||
write_stream, | ||
InitializationOptions( | ||
server_name="structured-output-example", | ||
server_version="0.1.0", | ||
capabilities=server.get_capabilities( | ||
notification_options=NotificationOptions(), | ||
experimental_capabilities={}, | ||
), | ||
), | ||
) | ||
|
||
|
||
if __name__ == "__main__": | ||
asyncio.run(run()) | ||
``` | ||
|
||
_Full example: [examples/snippets/servers/lowlevel/structured_output.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/lowlevel/structured_output.py)_ | ||
<!-- /snippet-source --> | ||
|
||
Tools can return data in three ways: | ||
|
||
1. **Content only**: Return a list of content blocks (default behavior before spec revision 2025-06-18) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
"""Low-level server examples for MCP Python SDK.""" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
"""Basic low-level server example. | ||
|
||
This example demonstrates the low-level server API with minimal setup, | ||
showing how to implement basic prompts using the raw protocol handlers. | ||
|
||
Run from the repository root: | ||
uv run examples/snippets/servers/lowlevel/basic.py | ||
""" | ||
|
||
import asyncio | ||
|
||
import mcp.server.stdio | ||
import mcp.types as types | ||
from mcp.server.lowlevel import NotificationOptions, Server | ||
from mcp.server.models import InitializationOptions | ||
|
||
# Create a server instance | ||
server = Server("example-server") | ||
|
||
|
||
@server.list_prompts() | ||
async def handle_list_prompts() -> list[types.Prompt]: | ||
"""List available prompts.""" | ||
return [ | ||
types.Prompt( | ||
name="example-prompt", | ||
description="An example prompt template", | ||
arguments=[types.PromptArgument(name="arg1", description="Example argument", required=True)], | ||
) | ||
] | ||
|
||
|
||
@server.get_prompt() | ||
async def handle_get_prompt(name: str, arguments: dict[str, str] | None) -> types.GetPromptResult: | ||
"""Get a specific prompt by name.""" | ||
if name != "example-prompt": | ||
raise ValueError(f"Unknown prompt: {name}") | ||
|
||
arg1_value = arguments.get("arg1", "default") if arguments else "default" | ||
|
||
return types.GetPromptResult( | ||
description="Example prompt", | ||
messages=[ | ||
types.PromptMessage( | ||
role="user", | ||
content=types.TextContent(type="text", text=f"Example prompt text with argument: {arg1_value}"), | ||
) | ||
], | ||
) | ||
|
||
|
||
async def run(): | ||
"""Run the basic low-level server.""" | ||
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): | ||
await server.run( | ||
read_stream, | ||
write_stream, | ||
InitializationOptions( | ||
server_name="example", | ||
server_version="0.1.0", | ||
capabilities=server.get_capabilities( | ||
notification_options=NotificationOptions(), | ||
experimental_capabilities={}, | ||
), | ||
), | ||
) | ||
|
||
|
||
if __name__ == "__main__": | ||
asyncio.run(run()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
meganit:
arg1_value = (arguments or {}).get("arg1", "default")
to say default only once?