Files
wehub-resource-sync 49b9bb6724
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:10:27 +08:00

161 lines
5.3 KiB
Python

"""SSE Polling Demo Server
Demonstrates the SSE polling pattern with close_sse_stream() for long-running tasks.
Features demonstrated:
- Priming events (automatic with EventStore)
- Server-initiated stream close via close_sse_stream callback
- Client auto-reconnect with Last-Event-ID
- Progress notifications during long-running tasks
Run with:
uv run mcp-sse-polling-demo --port 3000
"""
import logging
import anyio
import click
import mcp_types as types
import uvicorn
from mcp.server import Server, ServerRequestContext
from .event_store import InMemoryEventStore
logger = logging.getLogger(__name__)
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=(
"Process a batch of items with periodic checkpoints. "
"Demonstrates SSE polling where server closes stream periodically."
),
input_schema={
"type": "object",
"properties": {
"items": {
"type": "integer",
"description": "Number of items to process (1-100)",
"default": 10,
},
"checkpoint_every": {
"type": "integer",
"description": "Close stream after this many items (1-20)",
"default": 3,
},
},
},
)
]
)
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( # pyright: ignore[reportDeprecated]
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( # pyright: ignore[reportDeprecated]
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( # pyright: ignore[reportDeprecated]
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,
)
starlette_app = app.streamable_http_app(
event_store=InMemoryEventStore(),
retry_interval=retry_interval,
debug=True,
)
logger.info(f"SSE Polling Demo server starting on port {port}")
logger.info("Try: POST /mcp with tools/call for 'process_batch'")
uvicorn.run(starlette_app, host="127.0.0.1", port=port)
return 0
if __name__ == "__main__":
main()