Skip to content

Commit d6a51f7

Browse files
committed
add lowlevel to snippets
1 parent 6566c08 commit d6a51f7

File tree

5 files changed

+420
-24
lines changed

5 files changed

+420
-24
lines changed

README.md

Lines changed: 158 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,17 +1037,49 @@ For more information on mounting applications in Starlette, see the [Starlette d
10371037

10381038
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:
10391039

1040+
<!-- snippet-source examples/snippets/servers/lowlevel/lifespan.py -->
10401041
```python
1041-
from contextlib import asynccontextmanager
1042+
"""Low-level server example showing lifespan management.
1043+
1044+
This example demonstrates how to use the lifespan API to manage
1045+
server startup and shutdown, including resource initialization
1046+
and cleanup.
1047+
1048+
Run from the repository root:
1049+
uv run examples/snippets/servers/lowlevel/lifespan.py
1050+
"""
1051+
10421052
from collections.abc import AsyncIterator
1053+
from contextlib import asynccontextmanager
1054+
1055+
import mcp.server.stdio
1056+
import mcp.types as types
1057+
from mcp.server.lowlevel import NotificationOptions, Server
1058+
from mcp.server.models import InitializationOptions
1059+
1060+
1061+
# Mock database class for example
1062+
class Database:
1063+
"""Mock database class for example."""
10431064

1044-
from fake_database import Database # Replace with your actual DB type
1065+
@classmethod
1066+
async def connect(cls) -> "Database":
1067+
"""Connect to database."""
1068+
print("Database connected")
1069+
return cls()
10451070

1046-
from mcp.server import Server
1071+
async def disconnect(self) -> None:
1072+
"""Disconnect from database."""
1073+
print("Database disconnected")
1074+
1075+
async def query(self, query_str: str) -> list[dict[str, str]]:
1076+
"""Execute a query."""
1077+
# Simulate database query
1078+
return [{"id": "1", "name": "Example", "query": query_str}]
10471079

10481080

10491081
@asynccontextmanager
1050-
async def server_lifespan(server: Server) -> AsyncIterator[dict]:
1082+
async def server_lifespan(_server: Server) -> AsyncIterator[dict]:
10511083
"""Manage server startup and shutdown lifecycle."""
10521084
# Initialize resources on startup
10531085
db = await Database.connect()
@@ -1062,21 +1094,83 @@ async def server_lifespan(server: Server) -> AsyncIterator[dict]:
10621094
server = Server("example-server", lifespan=server_lifespan)
10631095

10641096

1065-
# Access lifespan context in handlers
1097+
@server.list_tools()
1098+
async def handle_list_tools() -> list[types.Tool]:
1099+
"""List available tools."""
1100+
return [
1101+
types.Tool(
1102+
name="query_db",
1103+
description="Query the database",
1104+
inputSchema={
1105+
"type": "object",
1106+
"properties": {"query": {"type": "string", "description": "SQL query to execute"}},
1107+
"required": ["query"],
1108+
},
1109+
)
1110+
]
1111+
1112+
10661113
@server.call_tool()
1067-
async def query_db(name: str, arguments: dict) -> list:
1114+
async def query_db(name: str, arguments: dict) -> list[types.TextContent]:
1115+
"""Handle database query tool call."""
1116+
if name != "query_db":
1117+
raise ValueError(f"Unknown tool: {name}")
1118+
1119+
# Access lifespan context
10681120
ctx = server.request_context
10691121
db = ctx.lifespan_context["db"]
1070-
return await db.query(arguments["query"])
1122+
1123+
# Execute query
1124+
results = await db.query(arguments["query"])
1125+
1126+
return [types.TextContent(type="text", text=f"Query results: {results}")]
1127+
1128+
1129+
async def run():
1130+
"""Run the server with lifespan management."""
1131+
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
1132+
await server.run(
1133+
read_stream,
1134+
write_stream,
1135+
InitializationOptions(
1136+
server_name="example-server",
1137+
server_version="0.1.0",
1138+
capabilities=server.get_capabilities(
1139+
notification_options=NotificationOptions(),
1140+
experimental_capabilities={},
1141+
),
1142+
),
1143+
)
1144+
1145+
1146+
if __name__ == "__main__":
1147+
import asyncio
1148+
1149+
asyncio.run(run())
10711150
```
10721151

1152+
_Full example: [examples/snippets/servers/lowlevel/lifespan.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/lowlevel/lifespan.py)_
1153+
<!-- /snippet-source -->
1154+
10731155
The lifespan API provides:
10741156

10751157
- A way to initialize resources when the server starts and clean them up when it stops
10761158
- Access to initialized resources through the request context in handlers
10771159
- Type-safe context passing between lifespan and request handlers
10781160

1161+
<!-- snippet-source examples/snippets/servers/lowlevel/basic.py -->
10791162
```python
1163+
"""Basic low-level server example.
1164+
1165+
This example demonstrates the low-level server API with minimal setup,
1166+
showing how to implement basic prompts using the raw protocol handlers.
1167+
1168+
Run from the repository root:
1169+
uv run examples/snippets/servers/lowlevel/basic.py
1170+
"""
1171+
1172+
import asyncio
1173+
10801174
import mcp.server.stdio
10811175
import mcp.types as types
10821176
from mcp.server.lowlevel import NotificationOptions, Server
@@ -1088,38 +1182,37 @@ server = Server("example-server")
10881182

10891183
@server.list_prompts()
10901184
async def handle_list_prompts() -> list[types.Prompt]:
1185+
"""List available prompts."""
10911186
return [
10921187
types.Prompt(
10931188
name="example-prompt",
10941189
description="An example prompt template",
1095-
arguments=[
1096-
types.PromptArgument(
1097-
name="arg1", description="Example argument", required=True
1098-
)
1099-
],
1190+
arguments=[types.PromptArgument(name="arg1", description="Example argument", required=True)],
11001191
)
11011192
]
11021193

11031194

11041195
@server.get_prompt()
1105-
async def handle_get_prompt(
1106-
name: str, arguments: dict[str, str] | None
1107-
) -> types.GetPromptResult:
1196+
async def handle_get_prompt(name: str, arguments: dict[str, str] | None) -> types.GetPromptResult:
1197+
"""Get a specific prompt by name."""
11081198
if name != "example-prompt":
11091199
raise ValueError(f"Unknown prompt: {name}")
11101200

1201+
arg1_value = arguments.get("arg1", "default") if arguments else "default"
1202+
11111203
return types.GetPromptResult(
11121204
description="Example prompt",
11131205
messages=[
11141206
types.PromptMessage(
11151207
role="user",
1116-
content=types.TextContent(type="text", text="Example prompt text"),
1208+
content=types.TextContent(type="text", text=f"Example prompt text with argument: {arg1_value}"),
11171209
)
11181210
],
11191211
)
11201212

11211213

11221214
async def run():
1215+
"""Run the basic low-level server."""
11231216
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
11241217
await server.run(
11251218
read_stream,
@@ -1136,37 +1229,50 @@ async def run():
11361229

11371230

11381231
if __name__ == "__main__":
1139-
import asyncio
1140-
11411232
asyncio.run(run())
11421233
```
11431234

1235+
_Full example: [examples/snippets/servers/lowlevel/basic.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/lowlevel/basic.py)_
1236+
<!-- /snippet-source -->
1237+
11441238
Caution: The `uv run mcp run` and `uv run mcp dev` tool doesn't support low-level server.
11451239

11461240
#### Structured Output Support
11471241

11481242
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:
11491243

1244+
<!-- snippet-source examples/snippets/servers/lowlevel/structured_output.py -->
11501245
```python
1151-
from types import Any
1246+
"""Low-level server example showing structured output support.
11521247
1248+
This example demonstrates how to use the low-level server API to return
1249+
structured data from tools, with automatic validation against output schemas.
1250+
1251+
Run from the repository root:
1252+
uv run examples/snippets/servers/lowlevel/structured_output.py
1253+
"""
1254+
1255+
import asyncio
1256+
from typing import Any
1257+
1258+
import mcp.server.stdio
11531259
import mcp.types as types
1154-
from mcp.server.lowlevel import Server
1260+
from mcp.server.lowlevel import NotificationOptions, Server
1261+
from mcp.server.models import InitializationOptions
11551262

11561263
server = Server("example-server")
11571264

11581265

11591266
@server.list_tools()
11601267
async def list_tools() -> list[types.Tool]:
1268+
"""List available tools with structured output schemas."""
11611269
return [
11621270
types.Tool(
11631271
name="calculate",
11641272
description="Perform mathematical calculations",
11651273
inputSchema={
11661274
"type": "object",
1167-
"properties": {
1168-
"expression": {"type": "string", "description": "Math expression"}
1169-
},
1275+
"properties": {"expression": {"type": "string", "description": "Math expression"}},
11701276
"required": ["expression"],
11711277
},
11721278
outputSchema={
@@ -1183,10 +1289,12 @@ async def list_tools() -> list[types.Tool]:
11831289

11841290
@server.call_tool()
11851291
async def call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]:
1292+
"""Handle tool calls with structured output."""
11861293
if name == "calculate":
11871294
expression = arguments["expression"]
11881295
try:
1189-
result = eval(expression) # Use a safe math parser
1296+
# WARNING: eval() is dangerous! Use a safe math parser in production
1297+
result = eval(expression)
11901298
structured = {"result": result, "expression": expression}
11911299

11921300
# 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]:
11951303
return structured
11961304
except Exception as e:
11971305
raise ValueError(f"Calculation error: {str(e)}")
1306+
else:
1307+
raise ValueError(f"Unknown tool: {name}")
1308+
1309+
1310+
async def run():
1311+
"""Run the structured output server."""
1312+
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
1313+
await server.run(
1314+
read_stream,
1315+
write_stream,
1316+
InitializationOptions(
1317+
server_name="structured-output-example",
1318+
server_version="0.1.0",
1319+
capabilities=server.get_capabilities(
1320+
notification_options=NotificationOptions(),
1321+
experimental_capabilities={},
1322+
),
1323+
),
1324+
)
1325+
1326+
1327+
if __name__ == "__main__":
1328+
asyncio.run(run())
11981329
```
11991330

1331+
_Full example: [examples/snippets/servers/lowlevel/structured_output.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/lowlevel/structured_output.py)_
1332+
<!-- /snippet-source -->
1333+
12001334
Tools can return data in three ways:
12011335

12021336
1. **Content only**: Return a list of content blocks (default behavior before spec revision 2025-06-18)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Low-level server examples for MCP Python SDK."""
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""Basic low-level server example.
2+
3+
This example demonstrates the low-level server API with minimal setup,
4+
showing how to implement basic prompts using the raw protocol handlers.
5+
6+
Run from the repository root:
7+
uv run examples/snippets/servers/lowlevel/basic.py
8+
"""
9+
10+
import asyncio
11+
12+
import mcp.server.stdio
13+
import mcp.types as types
14+
from mcp.server.lowlevel import NotificationOptions, Server
15+
from mcp.server.models import InitializationOptions
16+
17+
# Create a server instance
18+
server = Server("example-server")
19+
20+
21+
@server.list_prompts()
22+
async def handle_list_prompts() -> list[types.Prompt]:
23+
"""List available prompts."""
24+
return [
25+
types.Prompt(
26+
name="example-prompt",
27+
description="An example prompt template",
28+
arguments=[types.PromptArgument(name="arg1", description="Example argument", required=True)],
29+
)
30+
]
31+
32+
33+
@server.get_prompt()
34+
async def handle_get_prompt(name: str, arguments: dict[str, str] | None) -> types.GetPromptResult:
35+
"""Get a specific prompt by name."""
36+
if name != "example-prompt":
37+
raise ValueError(f"Unknown prompt: {name}")
38+
39+
arg1_value = arguments.get("arg1", "default") if arguments else "default"
40+
41+
return types.GetPromptResult(
42+
description="Example prompt",
43+
messages=[
44+
types.PromptMessage(
45+
role="user",
46+
content=types.TextContent(type="text", text=f"Example prompt text with argument: {arg1_value}"),
47+
)
48+
],
49+
)
50+
51+
52+
async def run():
53+
"""Run the basic low-level server."""
54+
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
55+
await server.run(
56+
read_stream,
57+
write_stream,
58+
InitializationOptions(
59+
server_name="example",
60+
server_version="0.1.0",
61+
capabilities=server.get_capabilities(
62+
notification_options=NotificationOptions(),
63+
experimental_capabilities={},
64+
),
65+
),
66+
)
67+
68+
69+
if __name__ == "__main__":
70+
asyncio.run(run())

0 commit comments

Comments
 (0)