@@ -1037,17 +1037,49 @@ For more information on mounting applications in Starlette, see the [Starlette d
10371037
10381038For 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+
10421052from 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]:
10621094server = 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+
10731155The 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+
10801174import mcp.server.stdio
10811175import mcp.types as types
10821176from mcp.server.lowlevel import NotificationOptions, Server
@@ -1088,38 +1182,37 @@ server = Server("example-server")
10881182
10891183@server.list_prompts ()
10901184async 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
11221214async 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
11381231if __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+
11441238Caution: The ` uv run mcp run ` and ` uv run mcp dev ` tool doesn't support low-level server.
11451239
11461240#### Structured Output Support
11471241
11481242The 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
11531259import 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
11561263server = Server(" example-server" )
11571264
11581265
11591266@server.list_tools ()
11601267async 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 ()
11851291async 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+
12001334Tools can return data in three ways:
12011335
120213361 . ** Content only** : Return a list of content blocks (default behavior before spec revision 2025-06-18)
0 commit comments