refactor: replace lowlevel Server decorators with on_* constructor kwargs (#1985)

This commit is contained in:
Max Isbey 2026-02-12 15:55:54 +00:00 • committed by GitHub
commit 0a22a9dc33
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 3563 additions and 5481 deletions

View file

@ -10,6 +10,7 @@ import json
import logging
import click
from mcp.server import ServerRequestContext
from mcp.server.mcpserver import Context, MCPServer
from mcp.server.mcpserver.prompts.base import UserMessage
from mcp.server.session import ServerSession
@ -20,13 +21,17 @@ from mcp.types import (
CompletionArgument,
CompletionContext,
EmbeddedResource,
EmptyResult,
ImageContent,
JSONRPCMessage,
PromptReference,
ResourceTemplateReference,
SamplingMessage,
SetLevelRequestParams,
SubscribeRequestParams,
TextContent,
TextResourceContents,
UnsubscribeRequestParams,
)
from pydantic import BaseModel, Field
@ -393,28 +398,29 @@ def test_prompt_with_image() -> list[UserMessage]:
# Custom request handlers
# TODO(felix): Add public APIs to MCPServer for subscribe_resource, unsubscribe_resource,
# and set_logging_level to avoid accessing protected _lowlevel_server attribute.
@mcp._lowlevel_server.set_logging_level() # pyright: ignore[reportPrivateUsage]
async def handle_set_logging_level(level: str) -> None:
async def handle_set_logging_level(ctx: ServerRequestContext, params: SetLevelRequestParams) -> EmptyResult:
"""Handle logging level changes"""
logger.info(f"Log level set to: {level}")
# In a real implementation, you would adjust the logging level here
# For conformance testing, we just acknowledge the request
logger.info(f"Log level set to: {params.level}")
return EmptyResult()
async def handle_subscribe(uri: str) -> None:
async def handle_subscribe(ctx: ServerRequestContext, params: SubscribeRequestParams) -> EmptyResult:
"""Handle resource subscription"""
resource_subscriptions.add(str(uri))
logger.info(f"Subscribed to resource: {uri}")
resource_subscriptions.add(str(params.uri))
logger.info(f"Subscribed to resource: {params.uri}")
return EmptyResult()
async def handle_unsubscribe(uri: str) -> None:
async def handle_unsubscribe(ctx: ServerRequestContext, params: UnsubscribeRequestParams) -> EmptyResult:
"""Handle resource unsubscription"""
resource_subscriptions.discard(str(uri))
logger.info(f"Unsubscribed from resource: {uri}")
resource_subscriptions.discard(str(params.uri))
logger.info(f"Unsubscribed from resource: {params.uri}")
return EmptyResult()
mcp._lowlevel_server.subscribe_resource()(handle_subscribe) # pyright: ignore[reportPrivateUsage]
mcp._lowlevel_server.unsubscribe_resource()(handle_unsubscribe) # pyright: ignore[reportPrivateUsage]
mcp._lowlevel_server._add_request_handler("logging/setLevel", handle_set_logging_level) # pyright: ignore[reportPrivateUsage]
mcp._lowlevel_server._add_request_handler("resources/subscribe", handle_subscribe) # pyright: ignore[reportPrivateUsage]
mcp._lowlevel_server._add_request_handler("resources/unsubscribe", handle_unsubscribe) # pyright: ignore[reportPrivateUsage]
@mcp.completion()

View file

@ -1,17 +1,19 @@
"""Simple MCP server demonstrating pagination for tools, resources, and prompts.
This example shows how to use the paginated decorators to handle large lists
of items that need to be split across multiple pages.
This example shows how to implement pagination with the low-level server API
to handle large lists of items that need to be split across multiple pages.
"""
from typing import Any
from typing import TypeVar
import anyio
import click
from mcp import types
from mcp.server.lowlevel import Server
from mcp.server import Server, ServerRequestContext
from starlette.requests import Request
T = TypeVar("T")
# Sample data - in real scenarios, this might come from a database
SAMPLE_TOOLS = [
types.Tool(
@ -44,6 +46,102 @@ SAMPLE_PROMPTS = [
]
def _paginate(cursor: str | None, items: list[T], page_size: int) -> tuple[list[T], str | None]:
"""Helper to paginate a list of items given a cursor."""
if cursor is not None:
try:
start_idx = int(cursor)
except (ValueError, TypeError):
return [], None
else:
start_idx = 0
page = items[start_idx : start_idx + page_size]
next_cursor = str(start_idx + page_size) if start_idx + page_size < len(items) else None
return page, next_cursor
# Paginated list_tools - returns 5 tools per page
async def handle_list_tools(
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
) -> types.ListToolsResult:
cursor = params.cursor if params is not None else None
page, next_cursor = _paginate(cursor, SAMPLE_TOOLS, page_size=5)
return types.ListToolsResult(tools=page, next_cursor=next_cursor)
# Paginated list_resources - returns 10 resources per page
async def handle_list_resources(
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
) -> types.ListResourcesResult:
cursor = params.cursor if params is not None else None
page, next_cursor = _paginate(cursor, SAMPLE_RESOURCES, page_size=10)
return types.ListResourcesResult(resources=page, next_cursor=next_cursor)
# Paginated list_prompts - returns 7 prompts per page
async def handle_list_prompts(
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
) -> types.ListPromptsResult:
cursor = params.cursor if params is not None else None
page, next_cursor = _paginate(cursor, SAMPLE_PROMPTS, page_size=7)
return types.ListPromptsResult(prompts=page, next_cursor=next_cursor)
async def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.CallToolResult:
# Find the tool in our sample data
tool = next((t for t in SAMPLE_TOOLS if t.name == params.name), None)
if not tool:
raise ValueError(f"Unknown tool: {params.name}")
return types.CallToolResult(
content=[
types.TextContent(
type="text",
text=f"Called tool '{params.name}' with arguments: {params.arguments}",
)
]
)
async def handle_read_resource(
ctx: ServerRequestContext, params: types.ReadResourceRequestParams
) -> types.ReadResourceResult:
resource = next((r for r in SAMPLE_RESOURCES if r.uri == str(params.uri)), None)
if not resource:
raise ValueError(f"Unknown resource: {params.uri}")
return types.ReadResourceResult(
contents=[
types.TextResourceContents(
uri=str(params.uri),
text=f"Content of {resource.name}: This is sample content for the resource.",
mime_type="text/plain",
)
]
)
async def handle_get_prompt(ctx: ServerRequestContext, params: types.GetPromptRequestParams) -> types.GetPromptResult:
prompt = next((p for p in SAMPLE_PROMPTS if p.name == params.name), None)
if not prompt:
raise ValueError(f"Unknown prompt: {params.name}")
message_text = f"This is the prompt '{params.name}'"
if params.arguments:
message_text += f" with arguments: {params.arguments}"
return types.GetPromptResult(
description=prompt.description,
messages=[
types.PromptMessage(
role="user",
content=types.TextContent(type="text", text=message_text),
)
],
)
@click.command()
@click.option("--port", default=8000, help="Port to listen on for SSE")
@click.option(
@ -53,142 +151,15 @@ SAMPLE_PROMPTS = [
help="Transport type",
)
def main(port: int, transport: str) -> int:
app = Server("mcp-simple-pagination")
# Paginated list_tools - returns 5 tools per page
@app.list_tools()
async def list_tools_paginated(request: types.ListToolsRequest) -> types.ListToolsResult:
page_size = 5
cursor = request.params.cursor if request.params is not None else None
if cursor is None:
# First page
start_idx = 0
else:
# Parse cursor to get the start index
try:
start_idx = int(cursor)
except (ValueError, TypeError):
# Invalid cursor, return empty
return types.ListToolsResult(tools=[], next_cursor=None)
# Get the page of tools
page_tools = SAMPLE_TOOLS[start_idx : start_idx + page_size]
# Determine if there are more pages
next_cursor = None
if start_idx + page_size < len(SAMPLE_TOOLS):
next_cursor = str(start_idx + page_size)
return types.ListToolsResult(tools=page_tools, next_cursor=next_cursor)
# Paginated list_resources - returns 10 resources per page
@app.list_resources()
async def list_resources_paginated(
request: types.ListResourcesRequest,
) -> types.ListResourcesResult:
page_size = 10
cursor = request.params.cursor if request.params is not None else None
if cursor is None:
# First page
start_idx = 0
else:
# Parse cursor to get the start index
try:
start_idx = int(cursor)
except (ValueError, TypeError):
# Invalid cursor, return empty
return types.ListResourcesResult(resources=[], next_cursor=None)
# Get the page of resources
page_resources = SAMPLE_RESOURCES[start_idx : start_idx + page_size]
# Determine if there are more pages
next_cursor = None
if start_idx + page_size < len(SAMPLE_RESOURCES):
next_cursor = str(start_idx + page_size)
return types.ListResourcesResult(resources=page_resources, next_cursor=next_cursor)
# Paginated list_prompts - returns 7 prompts per page
@app.list_prompts()
async def list_prompts_paginated(
request: types.ListPromptsRequest,
) -> types.ListPromptsResult:
page_size = 7
cursor = request.params.cursor if request.params is not None else None
if cursor is None:
# First page
start_idx = 0
else:
# Parse cursor to get the start index
try:
start_idx = int(cursor)
except (ValueError, TypeError):
# Invalid cursor, return empty
return types.ListPromptsResult(prompts=[], next_cursor=None)
# Get the page of prompts
page_prompts = SAMPLE_PROMPTS[start_idx : start_idx + page_size]
# Determine if there are more pages
next_cursor = None
if start_idx + page_size < len(SAMPLE_PROMPTS):
next_cursor = str(start_idx + page_size)
return types.ListPromptsResult(prompts=page_prompts, next_cursor=next_cursor)
# Implement call_tool handler
@app.call_tool()
async def call_tool(name: str, arguments: dict[str, Any]) -> list[types.ContentBlock]:
# Find the tool in our sample data
tool = next((t for t in SAMPLE_TOOLS if t.name == name), None)
if not tool:
raise ValueError(f"Unknown tool: {name}")
# Simple mock response
return [
types.TextContent(
type="text",
text=f"Called tool '{name}' with arguments: {arguments}",
)
]
# Implement read_resource handler
@app.read_resource()
async def read_resource(uri: str) -> str:
# Find the resource in our sample data
resource = next((r for r in SAMPLE_RESOURCES if r.uri == uri), None)
if not resource:
raise ValueError(f"Unknown resource: {uri}")
# Return a simple string - the decorator will convert it to TextResourceContents
return f"Content of {resource.name}: This is sample content for the resource."
# Implement get_prompt handler
@app.get_prompt()
async def get_prompt(name: str, arguments: dict[str, str] | None) -> types.GetPromptResult:
# Find the prompt in our sample data
prompt = next((p for p in SAMPLE_PROMPTS if p.name == name), None)
if not prompt:
raise ValueError(f"Unknown prompt: {name}")
# Simple mock response
message_text = f"This is the prompt '{name}'"
if arguments:
message_text += f" with arguments: {arguments}"
return types.GetPromptResult(
description=prompt.description,
messages=[
types.PromptMessage(
role="user",
content=types.TextContent(type="text", text=message_text),
)
],
)
app = Server(
"mcp-simple-pagination",
on_list_tools=handle_list_tools,
on_list_resources=handle_list_resources,
on_list_prompts=handle_list_prompts,
on_call_tool=handle_call_tool,
on_read_resource=handle_read_resource,
on_get_prompt=handle_get_prompt,
)
if transport == "sse":
from mcp.server.sse import SseServerTransport

View file

@ -1,7 +1,7 @@
import anyio
import click
from mcp import types
from mcp.server.lowlevel import Server
from mcp.server import Server, ServerRequestContext
from starlette.requests import Request
@ -30,20 +30,11 @@ def create_messages(context: str | None = None, topic: str | None = None) -> lis
return messages
@click.command()
@click.option("--port", default=8000, help="Port to listen on for SSE")
@click.option(
"--transport",
type=click.Choice(["stdio", "sse"]),
default="stdio",
help="Transport type",
)
def main(port: int, transport: str) -> int:
app = Server("mcp-simple-prompt")
@app.list_prompts()
async def list_prompts() -> list[types.Prompt]:
return [
async def handle_list_prompts(
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
) -> types.ListPromptsResult:
return types.ListPromptsResult(
prompts=[
types.Prompt(
name="simple",
title="Simple Assistant Prompt",
@ -62,19 +53,35 @@ def main(port: int, transport: str) -> int:
],
)
]
)
@app.get_prompt()
async def get_prompt(name: str, arguments: dict[str, str] | None = None) -> types.GetPromptResult:
if name != "simple":
raise ValueError(f"Unknown prompt: {name}")
if arguments is None:
arguments = {}
async def handle_get_prompt(ctx: ServerRequestContext, params: types.GetPromptRequestParams) -> types.GetPromptResult:
if params.name != "simple":
raise ValueError(f"Unknown prompt: {params.name}")
return types.GetPromptResult(
messages=create_messages(context=arguments.get("context"), topic=arguments.get("topic")),
description="A simple prompt with optional context and topic arguments",
)
arguments = params.arguments or {}
return types.GetPromptResult(
messages=create_messages(context=arguments.get("context"), topic=arguments.get("topic")),
description="A simple prompt with optional context and topic arguments",
)
@click.command()
@click.option("--port", default=8000, help="Port to listen on for SSE")
@click.option(
"--transport",
type=click.Choice(["stdio", "sse"]),
default="stdio",
help="Transport type",
)
def main(port: int, transport: str) -> int:
app = Server(
"mcp-simple-prompt",
on_list_prompts=handle_list_prompts,
on_get_prompt=handle_get_prompt,
)
if transport == "sse":
from mcp.server.sse import SseServerTransport

View file

@ -1,8 +1,9 @@
from urllib.parse import urlparse
import anyio
import click
from mcp import types
from mcp.server.lowlevel import Server
from mcp.server.lowlevel.helper_types import ReadResourceContents
from mcp.server import Server, ServerRequestContext
from starlette.requests import Request
SAMPLE_RESOURCES = {
@ -21,20 +22,11 @@ SAMPLE_RESOURCES = {
}
@click.command()
@click.option("--port", default=8000, help="Port to listen on for SSE")
@click.option(
"--transport",
type=click.Choice(["stdio", "sse"]),
default="stdio",
help="Transport type",
)
def main(port: int, transport: str) -> int:
app = Server("mcp-simple-resource")
@app.list_resources()
async def list_resources() -> list[types.Resource]:
return [
async def handle_list_resources(
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
) -> types.ListResourcesResult:
return types.ListResourcesResult(
resources=[
types.Resource(
uri=f"file:///{name}.txt",
name=name,
@ -44,20 +36,45 @@ def main(port: int, transport: str) -> int:
)
for name in SAMPLE_RESOURCES.keys()
]
)
@app.read_resource()
async def read_resource(uri: str):
from urllib.parse import urlparse
parsed = urlparse(uri)
if not parsed.path:
raise ValueError(f"Invalid resource path: {uri}")
name = parsed.path.replace(".txt", "").lstrip("/")
async def handle_read_resource(
ctx: ServerRequestContext, params: types.ReadResourceRequestParams
) -> types.ReadResourceResult:
parsed = urlparse(str(params.uri))
if not parsed.path:
raise ValueError(f"Invalid resource path: {params.uri}")
name = parsed.path.replace(".txt", "").lstrip("/")
if name not in SAMPLE_RESOURCES:
raise ValueError(f"Unknown resource: {uri}")
if name not in SAMPLE_RESOURCES:
raise ValueError(f"Unknown resource: {params.uri}")
return [ReadResourceContents(content=SAMPLE_RESOURCES[name]["content"], mime_type="text/plain")]
return types.ReadResourceResult(
contents=[
types.TextResourceContents(
uri=str(params.uri),
text=SAMPLE_RESOURCES[name]["content"],
mime_type="text/plain",
)
]
)
@click.command()
@click.option("--port", default=8000, help="Port to listen on for SSE")
@click.option(
"--transport",
type=click.Choice(["stdio", "sse"]),
default="stdio",
help="Transport type",
)
def main(port: int, transport: str) -> int:
app = Server(
"mcp-simple-resource",
on_list_resources=handle_list_resources,
on_read_resource=handle_read_resource,
)
if transport == "sse":
from mcp.server.sse import SseServerTransport

View file

@ -1,13 +1,12 @@
import contextlib
import logging
from collections.abc import AsyncIterator
from typing import Any
import anyio
import click
import uvicorn
from mcp import types
from mcp.server.lowlevel import Server
from mcp.server import Server, ServerRequestContext
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
from starlette.applications import Starlette
from starlette.middleware.cors import CORSMiddleware
@ -17,6 +16,64 @@ from starlette.types import Receive, Scope, Send
logger = logging.getLogger(__name__)
async def handle_list_tools(
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
) -> types.ListToolsResult:
return types.ListToolsResult(
tools=[
types.Tool(
name="start-notification-stream",
description=("Sends a stream of notifications with configurable count and interval"),
input_schema={
"type": "object",
"required": ["interval", "count", "caller"],
"properties": {
"interval": {
"type": "number",
"description": "Interval between notifications in seconds",
},
"count": {
"type": "number",
"description": "Number of notifications to send",
},
"caller": {
"type": "string",
"description": ("Identifier of the caller to include in notifications"),
},
},
},
)
]
)
async def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.CallToolResult:
arguments = params.arguments or {}
interval = arguments.get("interval", 1.0)
count = arguments.get("count", 5)
caller = arguments.get("caller", "unknown")
# Send the specified number of notifications with the given interval
for i in range(count):
await ctx.session.send_log_message(
level="info",
data=f"Notification {i + 1}/{count} from caller: {caller}",
logger="notification_stream",
related_request_id=ctx.request_id,
)
if i < count - 1: # Don't wait after the last notification
await anyio.sleep(interval)
return types.CallToolResult(
content=[
types.TextContent(
type="text",
text=(f"Sent {count} notifications with {interval}s interval for caller: {caller}"),
)
]
)
@click.command()
@click.option("--port", default=3000, help="Port to listen on for HTTP")
@click.option(
@ -41,59 +98,11 @@ def main(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
app = Server("mcp-streamable-http-stateless-demo")
@app.call_tool()
async def call_tool(name: str, arguments: dict[str, Any]) -> list[types.ContentBlock]:
ctx = app.request_context
interval = arguments.get("interval", 1.0)
count = arguments.get("count", 5)
caller = arguments.get("caller", "unknown")
# Send the specified number of notifications with the given interval
for i in range(count):
await ctx.session.send_log_message(
level="info",
data=f"Notification {i + 1}/{count} from caller: {caller}",
logger="notification_stream",
related_request_id=ctx.request_id,
)
if i < count - 1: # Don't wait after the last notification
await anyio.sleep(interval)
return [
types.TextContent(
type="text",
text=(f"Sent {count} notifications with {interval}s interval for caller: {caller}"),
)
]
@app.list_tools()
async def list_tools() -> list[types.Tool]:
return [
types.Tool(
name="start-notification-stream",
description=("Sends a stream of notifications with configurable count and interval"),
input_schema={
"type": "object",
"required": ["interval", "count", "caller"],
"properties": {
"interval": {
"type": "number",
"description": "Interval between notifications in seconds",
},
"count": {
"type": "number",
"description": "Number of notifications to send",
},
"caller": {
"type": "string",
"description": ("Identifier of the caller to include in notifications"),
},
},
},
)
]
app = Server(
"mcp-streamable-http-stateless-demo",
on_list_tools=handle_list_tools,
on_call_tool=handle_call_tool,
)
# Create the session manager with true stateless mode
session_manager = StreamableHTTPSessionManager(

View file

@ -1,12 +1,11 @@
import contextlib
import logging
from collections.abc import AsyncIterator
from typing import Any
import anyio
import click
from mcp import types
from mcp.server.lowlevel import Server
from mcp.server import Server, ServerRequestContext
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
from starlette.applications import Starlette
from starlette.middleware.cors import CORSMiddleware
@ -19,6 +18,75 @@ from .event_store import InMemoryEventStore
logger = logging.getLogger(__name__)
async def handle_list_tools(
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
) -> types.ListToolsResult:
return types.ListToolsResult(
tools=[
types.Tool(
name="start-notification-stream",
description="Sends a stream of notifications with configurable count and interval",
input_schema={
"type": "object",
"required": ["interval", "count", "caller"],
"properties": {
"interval": {
"type": "number",
"description": "Interval between notifications in seconds",
},
"count": {
"type": "number",
"description": "Number of notifications to send",
},
"caller": {
"type": "string",
"description": "Identifier of the caller to include in notifications",
},
},
},
)
]
)
async def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.CallToolResult:
arguments = params.arguments or {}
interval = arguments.get("interval", 1.0)
count = arguments.get("count", 5)
caller = arguments.get("caller", "unknown")
# Send the specified number of notifications with the given interval
for i in range(count):
# Include more detailed message for resumability demonstration
notification_msg = f"[{i + 1}/{count}] Event from '{caller}' - Use Last-Event-ID to resume if disconnected"
await ctx.session.send_log_message(
level="info",
data=notification_msg,
logger="notification_stream",
# Associates this notification with the original request
# Ensures notifications are sent to the correct response stream
# Without this, notifications will either go to:
# - a standalone SSE stream (if GET request is supported)
# - nowhere (if GET request isn't supported)
related_request_id=ctx.request_id,
)
logger.debug(f"Sent notification {i + 1}/{count} for caller: {caller}")
if i < count - 1: # Don't wait after the last notification
await anyio.sleep(interval)
# This will send a resource notification through standalone SSE
# established by GET request
await ctx.session.send_resource_updated(uri="http:///test_resource")
return types.CallToolResult(
content=[
types.TextContent(
type="text",
text=(f"Sent {count} notifications with {interval}s interval for caller: {caller}"),
)
]
)
@click.command()
@click.option("--port", default=3000, help="Port to listen on for HTTP")
@click.option(
@ -43,70 +111,11 @@ def main(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
app = Server("mcp-streamable-http-demo")
@app.call_tool()
async def call_tool(name: str, arguments: dict[str, Any]) -> list[types.ContentBlock]:
ctx = app.request_context
interval = arguments.get("interval", 1.0)
count = arguments.get("count", 5)
caller = arguments.get("caller", "unknown")
# Send the specified number of notifications with the given interval
for i in range(count):
# Include more detailed message for resumability demonstration
notification_msg = f"[{i + 1}/{count}] Event from '{caller}' - Use Last-Event-ID to resume if disconnected"
await ctx.session.send_log_message(
level="info",
data=notification_msg,
logger="notification_stream",
# Associates this notification with the original request
# Ensures notifications are sent to the correct response stream
# Without this, notifications will either go to:
# - a standalone SSE stream (if GET request is supported)
# - nowhere (if GET request isn't supported)
related_request_id=ctx.request_id,
)
logger.debug(f"Sent notification {i + 1}/{count} for caller: {caller}")
if i < count - 1: # Don't wait after the last notification
await anyio.sleep(interval)
# This will send a resource notificaiton though standalone SSE
# established by GET request
await ctx.session.send_resource_updated(uri="http:///test_resource")
return [
types.TextContent(
type="text",
text=(f"Sent {count} notifications with {interval}s interval for caller: {caller}"),
)
]
@app.list_tools()
async def list_tools() -> list[types.Tool]:
return [
types.Tool(
name="start-notification-stream",
description=("Sends a stream of notifications with configurable count and interval"),
input_schema={
"type": "object",
"required": ["interval", "count", "caller"],
"properties": {
"interval": {
"type": "number",
"description": "Interval between notifications in seconds",
},
"count": {
"type": "number",
"description": "Number of notifications to send",
},
"caller": {
"type": "string",
"description": ("Identifier of the caller to include in notifications"),
},
},
},
)
]
app = Server(
"mcp-streamable-http-demo",
on_list_tools=handle_list_tools,
on_call_tool=handle_call_tool,
)
# Create event store for resumability
# The InMemoryEventStore enables resumability support for StreamableHTTP transport.

View file

@ -13,42 +13,39 @@ from typing import Any
import click
import uvicorn
from mcp import types
from mcp.server import Server, ServerRequestContext
from mcp.server.experimental.task_context import ServerTaskContext
from mcp.server.lowlevel import Server
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
from starlette.applications import Starlette
from starlette.routing import Mount
server = Server("simple-task-interactive")
# Enable task support - this auto-registers all handlers
server.experimental.enable_tasks()
async def handle_list_tools(
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
) -> types.ListToolsResult:
return types.ListToolsResult(
tools=[
types.Tool(
name="confirm_delete",
description="Asks for confirmation before deleting (demonstrates elicitation)",
input_schema={
"type": "object",
"properties": {"filename": {"type": "string"}},
},
execution=types.ToolExecution(task_support=types.TASK_REQUIRED),
),
types.Tool(
name="write_haiku",
description="Asks LLM to write a haiku (demonstrates sampling)",
input_schema={"type": "object", "properties": {"topic": {"type": "string"}}},
execution=types.ToolExecution(task_support=types.TASK_REQUIRED),
),
]
)
@server.list_tools()
async def list_tools() -> list[types.Tool]:
return [
types.Tool(
name="confirm_delete",
description="Asks for confirmation before deleting (demonstrates elicitation)",
input_schema={
"type": "object",
"properties": {"filename": {"type": "string"}},
},
execution=types.ToolExecution(task_support=types.TASK_REQUIRED),
),
types.Tool(
name="write_haiku",
description="Asks LLM to write a haiku (demonstrates sampling)",
input_schema={"type": "object", "properties": {"topic": {"type": "string"}}},
execution=types.ToolExecution(task_support=types.TASK_REQUIRED),
),
]
async def handle_confirm_delete(arguments: dict[str, Any]) -> types.CreateTaskResult:
async def handle_confirm_delete(ctx: ServerRequestContext, arguments: dict[str, Any]) -> types.CreateTaskResult:
"""Handle the confirm_delete tool - demonstrates elicitation."""
ctx = server.request_context
ctx.experimental.validate_task_mode(types.TASK_REQUIRED)
filename = arguments.get("filename", "unknown.txt")
@ -80,9 +77,8 @@ async def handle_confirm_delete(arguments: dict[str, Any]) -> types.CreateTaskRe
return await ctx.experimental.run_task(work)
async def handle_write_haiku(arguments: dict[str, Any]) -> types.CreateTaskResult:
async def handle_write_haiku(ctx: ServerRequestContext, arguments: dict[str, Any]) -> types.CreateTaskResult:
"""Handle the write_haiku tool - demonstrates sampling."""
ctx = server.request_context
ctx.experimental.validate_task_mode(types.TASK_REQUIRED)
topic = arguments.get("topic", "nature")
@ -111,18 +107,31 @@ async def handle_write_haiku(arguments: dict[str, Any]) -> types.CreateTaskResul
return await ctx.experimental.run_task(work)
@server.call_tool()
async def handle_call_tool(name: str, arguments: dict[str, Any]) -> types.CallToolResult | types.CreateTaskResult:
async def handle_call_tool(
ctx: ServerRequestContext, params: types.CallToolRequestParams
) -> types.CallToolResult | types.CreateTaskResult:
"""Dispatch tool calls to their handlers."""
if name == "confirm_delete":
return await handle_confirm_delete(arguments)
elif name == "write_haiku":
return await handle_write_haiku(arguments)
else:
return types.CallToolResult(
content=[types.TextContent(type="text", text=f"Unknown tool: {name}")],
is_error=True,
)
arguments = params.arguments or {}
if params.name == "confirm_delete":
return await handle_confirm_delete(ctx, arguments)
elif params.name == "write_haiku":
return await handle_write_haiku(ctx, arguments)
return types.CallToolResult(
content=[types.TextContent(type="text", text=f"Unknown tool: {params.name}")],
is_error=True,
)
server = Server(
"simple-task-interactive",
on_list_tools=handle_list_tools,
on_call_tool=handle_call_tool,
)
# Enable task support - this auto-registers all handlers
server.experimental.enable_tasks()
def create_app(session_manager: StreamableHTTPSessionManager) -> Starlette:

View file

@ -2,68 +2,70 @@
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from typing import Any
import anyio
import click
import uvicorn
from mcp import types
from mcp.server import Server, ServerRequestContext
from mcp.server.experimental.task_context import ServerTaskContext
from mcp.server.lowlevel import Server
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
from starlette.applications import Starlette
from starlette.routing import Mount
server = Server("simple-task-server")
async def handle_list_tools(
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
) -> types.ListToolsResult:
return types.ListToolsResult(
tools=[
types.Tool(
name="long_running_task",
description="A task that takes a few seconds to complete with status updates",
input_schema={"type": "object", "properties": {}},
execution=types.ToolExecution(task_support=types.TASK_REQUIRED),
)
]
)
async def handle_call_tool(
ctx: ServerRequestContext, params: types.CallToolRequestParams
) -> types.CallToolResult | types.CreateTaskResult:
"""Dispatch tool calls to their handlers."""
if params.name == "long_running_task":
ctx.experimental.validate_task_mode(types.TASK_REQUIRED)
async def work(task: ServerTaskContext) -> types.CallToolResult:
await task.update_status("Starting work...")
await anyio.sleep(1)
await task.update_status("Processing step 1...")
await anyio.sleep(1)
await task.update_status("Processing step 2...")
await anyio.sleep(1)
return types.CallToolResult(content=[types.TextContent(type="text", text="Task completed!")])
return await ctx.experimental.run_task(work)
return types.CallToolResult(
content=[types.TextContent(type="text", text=f"Unknown tool: {params.name}")],
is_error=True,
)
server = Server(
"simple-task-server",
on_list_tools=handle_list_tools,
on_call_tool=handle_call_tool,
)
# One-line setup: auto-registers get_task, get_task_result, list_tasks, cancel_task
server.experimental.enable_tasks()
@server.list_tools()
async def list_tools() -> list[types.Tool]:
return [
types.Tool(
name="long_running_task",
description="A task that takes a few seconds to complete with status updates",
input_schema={"type": "object", "properties": {}},
execution=types.ToolExecution(task_support=types.TASK_REQUIRED),
)
]
async def handle_long_running_task(arguments: dict[str, Any]) -> types.CreateTaskResult:
"""Handle the long_running_task tool - demonstrates status updates."""
ctx = server.request_context
ctx.experimental.validate_task_mode(types.TASK_REQUIRED)
async def work(task: ServerTaskContext) -> types.CallToolResult:
await task.update_status("Starting work...")
await anyio.sleep(1)
await task.update_status("Processing step 1...")
await anyio.sleep(1)
await task.update_status("Processing step 2...")
await anyio.sleep(1)
return types.CallToolResult(content=[types.TextContent(type="text", text="Task completed!")])
return await ctx.experimental.run_task(work)
@server.call_tool()
async def handle_call_tool(name: str, arguments: dict[str, Any]) -> types.CallToolResult | types.CreateTaskResult:
"""Dispatch tool calls to their handlers."""
if name == "long_running_task":
return await handle_long_running_task(arguments)
else:
return types.CallToolResult(
content=[types.TextContent(type="text", text=f"Unknown tool: {name}")],
is_error=True,
)
@click.command()
@click.option("--port", default=8000, help="Port to listen on")
def main(port: int) -> int:

View file

@ -1,9 +1,7 @@
from typing import Any
import anyio
import click
from mcp import types
from mcp.server.lowlevel import Server
from mcp.server import Server, ServerRequestContext
from mcp.shared._httpx_utils import create_mcp_http_client
from starlette.requests import Request
@ -18,28 +16,11 @@ async def fetch_website(
return [types.TextContent(type="text", text=response.text)]
@click.command()
@click.option("--port", default=8000, help="Port to listen on for SSE")
@click.option(
"--transport",
type=click.Choice(["stdio", "sse"]),
default="stdio",
help="Transport type",
)
def main(port: int, transport: str) -> int:
app = Server("mcp-website-fetcher")
@app.call_tool()
async def fetch_tool(name: str, arguments: dict[str, Any]) -> list[types.ContentBlock]:
if name != "fetch":
raise ValueError(f"Unknown tool: {name}")
if "url" not in arguments:
raise ValueError("Missing required argument 'url'")
return await fetch_website(arguments["url"])
@app.list_tools()
async def list_tools() -> list[types.Tool]:
return [
async def handle_list_tools(
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
) -> types.ListToolsResult:
return types.ListToolsResult(
tools=[
types.Tool(
name="fetch",
title="Website Fetcher",
@ -56,6 +37,33 @@ def main(port: int, transport: str) -> int:
},
)
]
)
async def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.CallToolResult:
if params.name != "fetch":
raise ValueError(f"Unknown tool: {params.name}")
arguments = params.arguments or {}
if "url" not in arguments:
raise ValueError("Missing required argument 'url'")
content = await fetch_website(arguments["url"])
return types.CallToolResult(content=content)
@click.command()
@click.option("--port", default=8000, help="Port to listen on for SSE")
@click.option(
"--transport",
type=click.Choice(["stdio", "sse"]),
default="stdio",
help="Transport type",
)
def main(port: int, transport: str) -> int:
app = Server(
"mcp-website-fetcher",
on_list_tools=handle_list_tools,
on_call_tool=handle_call_tool,
)
if transport == "sse":
from mcp.server.sse import SseServerTransport

View file

@ -15,12 +15,11 @@ Run with:
import contextlib
import logging
from collections.abc import AsyncIterator
from typing import Any
import anyio
import click
from mcp import types
from mcp.server.lowlevel import Server
from mcp.server import Server, ServerRequestContext
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
from starlette.applications import Starlette
from starlette.routing import Mount
@ -31,88 +30,12 @@ from .event_store import InMemoryEventStore
logger = logging.getLogger(__name__)
@click.command()
@click.option("--port", default=3000, help="Port to listen on")
@click.option(
"--log-level",
default="INFO",
help="Logging level (DEBUG, INFO, WARNING, ERROR)",
)
@click.option(
"--retry-interval",
default=100,
help="SSE retry interval in milliseconds (sent to client)",
)
def main(port: int, log_level: str, retry_interval: int) -> int:
"""Run the SSE Polling Demo server."""
logging.basicConfig(
level=getattr(logging, log_level.upper()),
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
# Create the lowlevel server
app = Server("sse-polling-demo")
@app.call_tool()
async def call_tool(name: str, arguments: dict[str, Any]) -> list[types.ContentBlock]:
"""Handle tool calls."""
ctx = app.request_context
if name == "process_batch":
items = arguments.get("items", 10)
checkpoint_every = arguments.get("checkpoint_every", 3)
if items < 1 or items > 100:
return [types.TextContent(type="text", text="Error: items must be between 1 and 100")]
if checkpoint_every < 1 or checkpoint_every > 20:
return [types.TextContent(type="text", text="Error: checkpoint_every must be between 1 and 20")]
await ctx.session.send_log_message(
level="info",
data=f"Starting batch processing of {items} items...",
logger="process_batch",
related_request_id=ctx.request_id,
)
for i in range(1, items + 1):
# Simulate work
await anyio.sleep(0.5)
# Report progress
await ctx.session.send_log_message(
level="info",
data=f"[{i}/{items}] Processing item {i}",
logger="process_batch",
related_request_id=ctx.request_id,
)
# Checkpoint: close stream to trigger client reconnect
if i % checkpoint_every == 0 and i < items:
await ctx.session.send_log_message(
level="info",
data=f"Checkpoint at item {i} - closing SSE stream for polling",
logger="process_batch",
related_request_id=ctx.request_id,
)
if ctx.close_sse_stream:
logger.info(f"Closing SSE stream at checkpoint {i}")
await ctx.close_sse_stream()
# Wait for client to reconnect (must be > retry_interval of 100ms)
await anyio.sleep(0.2)
return [
types.TextContent(
type="text",
text=f"Successfully processed {items} items with checkpoints every {checkpoint_every} items",
)
]
return [types.TextContent(type="text", text=f"Unknown tool: {name}")]
@app.list_tools()
async def list_tools() -> list[types.Tool]:
"""List available tools."""
return [
async def handle_list_tools(
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
) -> types.ListToolsResult:
"""List available tools."""
return types.ListToolsResult(
tools=[
types.Tool(
name="process_batch",
description=(
@ -136,6 +59,95 @@ def main(port: int, log_level: str, retry_interval: int) -> int:
},
)
]
)
async def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.CallToolResult:
"""Handle tool calls."""
arguments = params.arguments or {}
if params.name == "process_batch":
items = arguments.get("items", 10)
checkpoint_every = arguments.get("checkpoint_every", 3)
if items < 1 or items > 100:
return types.CallToolResult(
content=[types.TextContent(type="text", text="Error: items must be between 1 and 100")]
)
if checkpoint_every < 1 or checkpoint_every > 20:
return types.CallToolResult(
content=[types.TextContent(type="text", text="Error: checkpoint_every must be between 1 and 20")]
)
await ctx.session.send_log_message(
level="info",
data=f"Starting batch processing of {items} items...",
logger="process_batch",
related_request_id=ctx.request_id,
)
for i in range(1, items + 1):
# Simulate work
await anyio.sleep(0.5)
# Report progress
await ctx.session.send_log_message(
level="info",
data=f"[{i}/{items}] Processing item {i}",
logger="process_batch",
related_request_id=ctx.request_id,
)
# Checkpoint: close stream to trigger client reconnect
if i % checkpoint_every == 0 and i < items:
await ctx.session.send_log_message(
level="info",
data=f"Checkpoint at item {i} - closing SSE stream for polling",
logger="process_batch",
related_request_id=ctx.request_id,
)
if ctx.close_sse_stream:
logger.info(f"Closing SSE stream at checkpoint {i}")
await ctx.close_sse_stream()
# Wait for client to reconnect (must be > retry_interval of 100ms)
await anyio.sleep(0.2)
return types.CallToolResult(
content=[
types.TextContent(
type="text",
text=f"Successfully processed {items} items with checkpoints every {checkpoint_every} items",
)
]
)
return types.CallToolResult(content=[types.TextContent(type="text", text=f"Unknown tool: {params.name}")])
@click.command()
@click.option("--port", default=3000, help="Port to listen on")
@click.option(
"--log-level",
default="INFO",
help="Logging level (DEBUG, INFO, WARNING, ERROR)",
)
@click.option(
"--retry-interval",
default=100,
help="SSE retry interval in milliseconds (sent to client)",
)
def main(port: int, log_level: str, retry_interval: int) -> int:
"""Run the SSE Polling Demo server."""
logging.basicConfig(
level=getattr(logging, log_level.upper()),
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
app = Server(
"sse-polling-demo",
on_list_tools=handle_list_tools,
on_call_tool=handle_call_tool,
)
# Create event store for resumability
event_store = InMemoryEventStore()

View file

@ -2,60 +2,54 @@
"""Example low-level MCP server demonstrating structured output support.
This example shows how to use the low-level server API to return
structured data from tools, with automatic validation against output
schemas.
structured data from tools.
"""
import asyncio
import json
import random
from datetime import datetime
from typing import Any
import mcp.server.stdio
from mcp import types
from mcp.server.lowlevel import NotificationOptions, Server
from mcp.server.models import InitializationOptions
# Create low-level server instance
server = Server("structured-output-lowlevel-example")
from mcp.server import Server, ServerRequestContext
@server.list_tools()
async def list_tools() -> list[types.Tool]:
async def handle_list_tools(
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
) -> types.ListToolsResult:
"""List available tools with their schemas."""
return [
types.Tool(
name="get_weather",
description="Get weather information (simulated)",
input_schema={
"type": "object",
"properties": {"city": {"type": "string", "description": "City name"}},
"required": ["city"],
},
output_schema={
"type": "object",
"properties": {
"temperature": {"type": "number"},
"conditions": {"type": "string"},
"humidity": {"type": "integer", "minimum": 0, "maximum": 100},
"wind_speed": {"type": "number"},
"timestamp": {"type": "string", "format": "date-time"},
return types.ListToolsResult(
tools=[
types.Tool(
name="get_weather",
description="Get weather information (simulated)",
input_schema={
"type": "object",
"properties": {"city": {"type": "string", "description": "City name"}},
"required": ["city"],
},
"required": ["temperature", "conditions", "humidity", "wind_speed", "timestamp"],
},
),
]
output_schema={
"type": "object",
"properties": {
"temperature": {"type": "number"},
"conditions": {"type": "string"},
"humidity": {"type": "integer", "minimum": 0, "maximum": 100},
"wind_speed": {"type": "number"},
"timestamp": {"type": "string", "format": "date-time"},
},
"required": ["temperature", "conditions", "humidity", "wind_speed", "timestamp"],
},
),
]
)
@server.call_tool()
async def call_tool(name: str, arguments: dict[str, Any]) -> Any:
async def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.CallToolResult:
"""Handle tool call with structured output."""
if name == "get_weather":
# city = arguments["city"] # Would be used with real weather API
if params.name == "get_weather":
# Simulate weather data (in production, call a real weather API)
import random
weather_conditions = ["sunny", "cloudy", "rainy", "partly cloudy", "foggy"]
weather_data = {
@ -66,12 +60,19 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> Any:
"timestamp": datetime.now().isoformat(),
}
# Return structured data only
# The low-level server will serialize this to JSON content automatically
return weather_data
return types.CallToolResult(
content=[types.TextContent(type="text", text=json.dumps(weather_data, indent=2))],
structured_content=weather_data,
)
else:
raise ValueError(f"Unknown tool: {name}")
raise ValueError(f"Unknown tool: {params.name}")
server = Server(
"structured-output-lowlevel-example",
on_list_tools=handle_list_tools,
on_call_tool=handle_call_tool,
)
async def run():
@ -80,14 +81,7 @@ async def run():
await server.run(
read_stream,
write_stream,
InitializationOptions(
server_name="structured-output-lowlevel-example",
server_version="0.1.0",
capabilities=server.get_capabilities(
notification_options=NotificationOptions(),
experimental_capabilities={},
),
),
server.create_initialization_options(),
)

View file

@ -6,32 +6,30 @@ import asyncio
import mcp.server.stdio
from mcp import types
from mcp.server.lowlevel import NotificationOptions, Server
from mcp.server.models import InitializationOptions
# Create a server instance
server = Server("example-server")
from mcp.server import Server, ServerRequestContext
@server.list_prompts()
async def handle_list_prompts() -> list[types.Prompt]:
async def handle_list_prompts(
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
) -> types.ListPromptsResult:
"""List available prompts."""
return [
types.Prompt(
name="example-prompt",
description="An example prompt template",
arguments=[types.PromptArgument(name="arg1", description="Example argument", required=True)],
)
]
return types.ListPromptsResult(
prompts=[
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:
async def handle_get_prompt(ctx: ServerRequestContext, params: types.GetPromptRequestParams) -> types.GetPromptResult:
"""Get a specific prompt by name."""
if name != "example-prompt":
raise ValueError(f"Unknown prompt: {name}")
if params.name != "example-prompt":
raise ValueError(f"Unknown prompt: {params.name}")
arg1_value = (arguments or {}).get("arg1", "default")
arg1_value = (params.arguments or {}).get("arg1", "default")
return types.GetPromptResult(
description="Example prompt",
@ -44,20 +42,20 @@ async def handle_get_prompt(name: str, arguments: dict[str, str] | None) -> type
)
server = Server(
"example-server",
on_list_prompts=handle_list_prompts,
on_get_prompt=handle_get_prompt,
)
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={},
),
),
server.create_initialization_options(),
)

View file

@ -3,44 +3,49 @@ uv run examples/snippets/servers/lowlevel/direct_call_tool_result.py
"""
import asyncio
from typing import Any
import mcp.server.stdio
from mcp import types
from mcp.server.lowlevel import NotificationOptions, Server
from mcp.server.models import InitializationOptions
server = Server("example-server")
from mcp.server import Server, ServerRequestContext
@server.list_tools()
async def list_tools() -> list[types.Tool]:
async def handle_list_tools(
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
) -> types.ListToolsResult:
"""List available tools."""
return [
types.Tool(
name="advanced_tool",
description="Tool with full control including _meta field",
input_schema={
"type": "object",
"properties": {"message": {"type": "string"}},
"required": ["message"],
},
)
]
return types.ListToolsResult(
tools=[
types.Tool(
name="advanced_tool",
description="Tool with full control including _meta field",
input_schema={
"type": "object",
"properties": {"message": {"type": "string"}},
"required": ["message"],
},
)
]
)
@server.call_tool()
async def handle_call_tool(name: str, arguments: dict[str, Any]) -> types.CallToolResult:
async def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.CallToolResult:
"""Handle tool calls by returning CallToolResult directly."""
if name == "advanced_tool":
message = str(arguments.get("message", ""))
if params.name == "advanced_tool":
message = (params.arguments or {}).get("message", "")
return types.CallToolResult(
content=[types.TextContent(type="text", text=f"Processed: {message}")],
structured_content={"result": "success", "message": message},
_meta={"hidden": "data for client applications only"},
)
raise ValueError(f"Unknown tool: {name}")
raise ValueError(f"Unknown tool: {params.name}")
server = Server(
"example-server",
on_list_tools=handle_list_tools,
on_call_tool=handle_call_tool,
)
async def run():
@ -49,14 +54,7 @@ async def run():
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={},
),
),
server.create_initialization_options(),
)

View file

@ -4,12 +4,11 @@ uv run examples/snippets/servers/lowlevel/lifespan.py
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from typing import Any
from typing import TypedDict
import mcp.server.stdio
from mcp import types
from mcp.server.lowlevel import NotificationOptions, Server
from mcp.server.models import InitializationOptions
from mcp.server import Server, ServerRequestContext
# Mock database class for example
@ -32,52 +31,58 @@ class Database:
return [{"id": "1", "name": "Example", "query": query_str}]
class AppContext(TypedDict):
db: Database
@asynccontextmanager
async def server_lifespan(_server: Server) -> AsyncIterator[dict[str, Any]]:
async def server_lifespan(_server: Server[AppContext]) -> AsyncIterator[AppContext]:
"""Manage server startup and shutdown lifecycle."""
# Initialize resources on startup
db = await Database.connect()
try:
yield {"db": db}
finally:
# Clean up on shutdown
await db.disconnect()
# Pass lifespan to server
server = Server("example-server", lifespan=server_lifespan)
@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
async def handle_list_tools(
ctx: ServerRequestContext[AppContext], params: types.PaginatedRequestParams | None
) -> types.ListToolsResult:
"""List available tools."""
return [
types.Tool(
name="query_db",
description="Query the database",
input_schema={
"type": "object",
"properties": {"query": {"type": "string", "description": "SQL query to execute"}},
"required": ["query"],
},
)
]
return types.ListToolsResult(
tools=[
types.Tool(
name="query_db",
description="Query the database",
input_schema={
"type": "object",
"properties": {"query": {"type": "string", "description": "SQL query to execute"}},
"required": ["query"],
},
)
]
)
@server.call_tool()
async def query_db(name: str, arguments: dict[str, Any]) -> list[types.TextContent]:
async def handle_call_tool(
ctx: ServerRequestContext[AppContext], params: types.CallToolRequestParams
) -> types.CallToolResult:
"""Handle database query tool call."""
if name != "query_db":
raise ValueError(f"Unknown tool: {name}")
if params.name != "query_db":
raise ValueError(f"Unknown tool: {params.name}")
# Access lifespan context
ctx = server.request_context
db = ctx.lifespan_context["db"]
results = await db.query((params.arguments or {})["query"])
# Execute query
results = await db.query(arguments["query"])
return types.CallToolResult(content=[types.TextContent(type="text", text=f"Query results: {results}")])
return [types.TextContent(type="text", text=f"Query results: {results}")]
server = Server(
"example-server",
lifespan=server_lifespan,
on_list_tools=handle_list_tools,
on_call_tool=handle_call_tool,
)
async def run():
@ -86,14 +91,7 @@ async def run():
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={},
),
),
server.create_initialization_options(),
)

View file

@ -3,62 +3,67 @@ uv run examples/snippets/servers/lowlevel/structured_output.py
"""
import asyncio
from typing import Any
import json
import mcp.server.stdio
from mcp import types
from mcp.server.lowlevel import NotificationOptions, Server
from mcp.server.models import InitializationOptions
server = Server("example-server")
from mcp.server import Server, ServerRequestContext
@server.list_tools()
async def list_tools() -> list[types.Tool]:
async def handle_list_tools(
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
) -> types.ListToolsResult:
"""List available tools with structured output schemas."""
return [
types.Tool(
name="get_weather",
description="Get current weather for a city",
input_schema={
"type": "object",
"properties": {"city": {"type": "string", "description": "City name"}},
"required": ["city"],
},
output_schema={
"type": "object",
"properties": {
"temperature": {"type": "number", "description": "Temperature in Celsius"},
"condition": {"type": "string", "description": "Weather condition"},
"humidity": {"type": "number", "description": "Humidity percentage"},
"city": {"type": "string", "description": "City name"},
return types.ListToolsResult(
tools=[
types.Tool(
name="get_weather",
description="Get current weather for a city",
input_schema={
"type": "object",
"properties": {"city": {"type": "string", "description": "City name"}},
"required": ["city"],
},
"required": ["temperature", "condition", "humidity", "city"],
},
)
]
output_schema={
"type": "object",
"properties": {
"temperature": {"type": "number", "description": "Temperature in Celsius"},
"condition": {"type": "string", "description": "Weather condition"},
"humidity": {"type": "number", "description": "Humidity percentage"},
"city": {"type": "string", "description": "City name"},
},
"required": ["temperature", "condition", "humidity", "city"],
},
)
]
)
@server.call_tool()
async def call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]:
async def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.CallToolResult:
"""Handle tool calls with structured output."""
if name == "get_weather":
city = arguments["city"]
if params.name == "get_weather":
city = (params.arguments or {})["city"]
# Simulated weather data - in production, call a weather API
weather_data = {
"temperature": 22.5,
"condition": "partly cloudy",
"humidity": 65,
"city": city, # Include the requested city
"city": city,
}
# low-level server will validate structured output against the tool's
# output schema, and additionally serialize it into a TextContent block
# for backwards compatibility with pre-2025-06-18 clients.
return weather_data
else:
raise ValueError(f"Unknown tool: {name}")
return types.CallToolResult(
content=[types.TextContent(type="text", text=json.dumps(weather_data, indent=2))],
structured_content=weather_data,
)
raise ValueError(f"Unknown tool: {params.name}")
server = Server(
"example-server",
on_list_tools=handle_list_tools,
on_call_tool=handle_call_tool,
)
async def run():
@ -67,14 +72,7 @@ async def run():
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={},
),
),
server.create_initialization_options(),
)

View file

@ -1,22 +1,20 @@
"""Example of implementing pagination with MCP server decorators."""
"""Example of implementing pagination with the low-level MCP server."""
from mcp import types
from mcp.server.lowlevel import Server
# Initialize the server
server = Server("paginated-server")
from mcp.server import Server, ServerRequestContext
# Sample data to paginate
ITEMS = [f"Item {i}" for i in range(1, 101)] # 100 items
@server.list_resources()
async def list_resources_paginated(request: types.ListResourcesRequest) -> types.ListResourcesResult:
async def handle_list_resources(
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
) -> types.ListResourcesResult:
"""List resources with pagination support."""
page_size = 10
# Extract cursor from request params
cursor = request.params.cursor if request.params is not None else None
cursor = params.cursor if params is not None else None
# Parse cursor to get offset
start = 0 if cursor is None else int(cursor)
@ -32,3 +30,6 @@ async def list_resources_paginated(request: types.ListResourcesRequest) -> types
next_cursor = str(end) if end < len(ITEMS) else None
return types.ListResourcesResult(resources=page_items, next_cursor=next_cursor)
server = Server("paginated-server", on_list_resources=handle_list_resources)