-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Expand file tree
/
Copy path_hosting.py
More file actions
87 lines (66 loc) · 3.28 KB
/
Copy path_hosting.py
File metadata and controls
87 lines (66 loc) · 3.28 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
"""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 httpx2 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)