@@ -1037,17 +1037,49 @@ For more information on mounting applications in Starlette, see the [Starlette d
1037
1037
1038
1038
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:
1039
1039
1040
+ <!-- snippet-source examples/snippets/servers/lowlevel/lifespan.py -->
1040
1041
``` 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
+
1042
1052
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."""
1043
1064
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 ()
1045
1070
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}]
1047
1079
1048
1080
1049
1081
@asynccontextmanager
1050
- async def server_lifespan (server : Server) -> AsyncIterator[dict ]:
1082
+ async def server_lifespan (_server : Server) -> AsyncIterator[dict ]:
1051
1083
""" Manage server startup and shutdown lifecycle."""
1052
1084
# Initialize resources on startup
1053
1085
db = await Database.connect()
@@ -1062,21 +1094,83 @@ async def server_lifespan(server: Server) -> AsyncIterator[dict]:
1062
1094
server = Server(" example-server" , lifespan = server_lifespan)
1063
1095
1064
1096
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
+
1066
1113
@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
1068
1120
ctx = server.request_context
1069
1121
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())
1071
1150
```
1072
1151
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
+
1073
1155
The lifespan API provides:
1074
1156
1075
1157
- A way to initialize resources when the server starts and clean them up when it stops
1076
1158
- Access to initialized resources through the request context in handlers
1077
1159
- Type-safe context passing between lifespan and request handlers
1078
1160
1161
+ <!-- snippet-source examples/snippets/servers/lowlevel/basic.py -->
1079
1162
``` 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
+
1080
1174
import mcp.server.stdio
1081
1175
import mcp.types as types
1082
1176
from mcp.server.lowlevel import NotificationOptions, Server
@@ -1088,38 +1182,37 @@ server = Server("example-server")
1088
1182
1089
1183
@server.list_prompts ()
1090
1184
async def handle_list_prompts () -> list[types.Prompt]:
1185
+ """ List available prompts."""
1091
1186
return [
1092
1187
types.Prompt(
1093
1188
name = " example-prompt" ,
1094
1189
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 )],
1100
1191
)
1101
1192
]
1102
1193
1103
1194
1104
1195
@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."""
1108
1198
if name != " example-prompt" :
1109
1199
raise ValueError (f " Unknown prompt: { name} " )
1110
1200
1201
+ arg1_value = arguments.get(" arg1" , " default" ) if arguments else " default"
1202
+
1111
1203
return types.GetPromptResult(
1112
1204
description = " Example prompt" ,
1113
1205
messages = [
1114
1206
types.PromptMessage(
1115
1207
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 } " ),
1117
1209
)
1118
1210
],
1119
1211
)
1120
1212
1121
1213
1122
1214
async def run ():
1215
+ """ Run the basic low-level server."""
1123
1216
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
1124
1217
await server.run(
1125
1218
read_stream,
@@ -1136,37 +1229,50 @@ async def run():
1136
1229
1137
1230
1138
1231
if __name__ == " __main__" :
1139
- import asyncio
1140
-
1141
1232
asyncio.run(run())
1142
1233
```
1143
1234
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
+
1144
1238
Caution: The ` uv run mcp run ` and ` uv run mcp dev ` tool doesn't support low-level server.
1145
1239
1146
1240
#### Structured Output Support
1147
1241
1148
1242
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:
1149
1243
1244
+ <!-- snippet-source examples/snippets/servers/lowlevel/structured_output.py -->
1150
1245
``` python
1151
- from types import Any
1246
+ """ Low-level server example showing structured output support.
1152
1247
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
1153
1259
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
1155
1262
1156
1263
server = Server(" example-server" )
1157
1264
1158
1265
1159
1266
@server.list_tools ()
1160
1267
async def list_tools () -> list[types.Tool]:
1268
+ """ List available tools with structured output schemas."""
1161
1269
return [
1162
1270
types.Tool(
1163
1271
name = " calculate" ,
1164
1272
description = " Perform mathematical calculations" ,
1165
1273
inputSchema = {
1166
1274
" type" : " object" ,
1167
- " properties" : {
1168
- " expression" : {" type" : " string" , " description" : " Math expression" }
1169
- },
1275
+ " properties" : {" expression" : {" type" : " string" , " description" : " Math expression" }},
1170
1276
" required" : [" expression" ],
1171
1277
},
1172
1278
outputSchema = {
@@ -1183,10 +1289,12 @@ async def list_tools() -> list[types.Tool]:
1183
1289
1184
1290
@server.call_tool ()
1185
1291
async def call_tool (name : str , arguments : dict[str , Any]) -> dict[str , Any]:
1292
+ """ Handle tool calls with structured output."""
1186
1293
if name == " calculate" :
1187
1294
expression = arguments[" expression" ]
1188
1295
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)
1190
1298
structured = {" result" : result, " expression" : expression}
1191
1299
1192
1300
# 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]:
1195
1303
return structured
1196
1304
except Exception as e:
1197
1305
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())
1198
1329
```
1199
1330
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
+
1200
1334
Tools can return data in three ways:
1201
1335
1202
1336
1 . ** Content only** : Return a list of content blocks (default behavior before spec revision 2025-06-18)
0 commit comments