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
88 lines
3.3 KiB
Python
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)
|