Files
modelcontextprotocol--pytho…/examples/stories/_hosting.py
T
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) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 12:10:27 +08:00

88 lines
3.3 KiB
Python

"""Server-side hosting scaffold for story examples.
A story's ``server.py`` / ``server_lowlevel.py`` imports only from here. The
marked lines touch entry-point APIs that a later release reshapes into
free-function entries; isolating them here keeps story bodies stable.
"""
from __future__ import annotations
import sys
from collections.abc import Callable
from typing import Any, TypeAlias
import anyio
import uvicorn
from starlette.applications import Starlette
from mcp.server.lowlevel import Server
from mcp.server.mcpserver import MCPServer
from mcp.server.stdio import stdio_server
from mcp.server.transport_security import TransportSecuritySettings
AnyServer: TypeAlias = "MCPServer | Server[Any]"
ServerFactory = Callable[[], AnyServer]
AppFactory = Callable[[], Starlette]
NO_DNS_REBIND = TransportSecuritySettings(enable_dns_rebinding_protection=False)
"""Harness servers bind 127.0.0.1 and the in-process httpx client sends no Origin header."""
def argv_after(flag: str, *, default: str | None = None) -> str:
"""Return the argv token following ``flag``, or ``default`` when the flag is absent."""
try:
return sys.argv[sys.argv.index(flag) + 1]
except ValueError:
if default is None:
raise SystemExit(f"missing required {flag}") from None
return default
def asgi_from(server: AnyServer, *, path: str = "/mcp") -> Starlette:
"""Wrap a server instance in its streamable-HTTP ASGI app for in-process driving."""
return server.streamable_http_app( # becomes free fn streamable_http(server, legacy=...)
streamable_http_path=path,
stateless_http=False, # bool folds into a legacy= enum in a later release
transport_security=NO_DNS_REBIND,
)
def run_server_from_args(build_server: ServerFactory) -> None:
"""Entry point for ``if __name__ == "__main__"`` in every ``server*.py``.
Bare argv serves over stdio; ``--http --port N [--path /mcp]`` serves over
uvicorn on 127.0.0.1:N.
"""
server = build_server()
if "--http" in sys.argv:
port = int(argv_after("--port", default="8000"))
path = argv_after("--path", default="/mcp")
anyio.run(_serve_http, server, port, path)
else:
anyio.run(_serve_stdio, server)
async def _serve_stdio(server: AnyServer) -> None:
if isinstance(server, MCPServer):
await server.run_stdio_async() # becomes await serve_stdio(server)
else:
async with stdio_server() as (read, write): # becomes await serve_stdio(server)
await server.run(read, write, server.create_initialization_options())
async def _serve_http(server: AnyServer, port: int, path: str) -> None:
app = asgi_from(server, path=path)
config = uvicorn.Config(app, host="127.0.0.1", port=port, log_level="error")
await uvicorn.Server(config).serve()
def run_app_from_args(build_app: AppFactory) -> None:
"""Entry point for ``if __name__ == "__main__"`` in app-exporting ``server*.py``.
App-exporting stories are HTTP-only; ``--port N`` serves the Starlette app over
uvicorn on 127.0.0.1:N (uvicorn drives the app's own lifespan). No stdio leg.
"""
port = int(argv_after("--port", default="8000"))
config = uvicorn.Config(build_app(), host="127.0.0.1", port=port, log_level="error")
anyio.run(uvicorn.Server(config).serve)