Skip to main content

Serving it over HTTP

One endpoint, many servers. A client connects to /mcp and gets the default toolsets; a client that connects to /mcp?benchmarks gets those as well. The selection is read per connection, the matching server is built once and kept, and two clients asking for the same toolsets share it.

from reactor_mcp_server import build_host, create_mcp_app, load_extensions

app = create_mcp_app(build_host(load_extensions()), path="/mcp")

app is a Starlette application. Serve it with uvicorn, or mount it in one you already have.

What it serves​

RouteWhat it answers
POST /mcpThe protocol, on the server this URL's toolsets select
GET /healthzWhether the process is up, without opening a session
GET /toolsetsWhat this deployment offers and what a URL would activate

/toolsets takes the same query string, so a client can ask what a connection would get:

curl https://mcp.example.com/toolsets?only=spaces
{
"toolsets": [
{"name": "spaces", "title": "Spaces", "description": "…", "default": true, "always": false, "active": true},
{"name": "sandboxes", "title": "Sandboxes", "description": "…", "default": true, "always": false, "active": false}
],
"active": ["spaces"],
"tools": ["find_notebook", "list_spaces", "…"],
"unknown": []
}

It is how a client is told which names it may put in its URL, rather than being sent to read a deployment's source. Both extra routes can be turned off (healthz=None, toolsets_route=None) for a deployment that serves its own.

Running the installed extensions​

A deployment that wants what is installed and no code at all runs the module:

pip install "reactor_mcp_server[server]"
reactor-mcp-server --port 4040 --path /mcp
OptionDefaultWhat it is
--transport / REACTOR_MCP_TRANSPORTstreamable-httpOr stdio, for a client that launches the server
--toolsets / REACTOR_MCP_TOOLSETSeach toolset's own defaultWhat a URL naming no toolset gets, in the URL's spelling: earthdata, only=math,geo. Over stdio, the whole selection
--ignore-query-keynoneA query key the deployment reads for something else; repeatable
--toolsets-tool / REACTOR_MCP_TOOLSETS_TOOLonPut list_server_toolsets on every server; --no-toolsets-tool leaves it off
--host / REACTOR_MCP_HOST0.0.0.0Interface to bind
--port / REACTOR_MCP_PORT4040Port
--path / REACTOR_MCP_PATH/mcpWhere the protocol is served
--name / REACTOR_MCP_NAMEreactor-mcp-serverWhat the server calls itself
--extension / REACTOR_MCP_EXTENSIONSevery installed oneLoad only these, repeatable

Over stdio​

A client that launches the server has no URL to put a toolset in, so the command line selects:

{
"mcpServers": {
"earth": {
"command": "reactor-mcp-server",
"args": ["--transport", "stdio", "--extension", "earthdata", "--toolsets", "only=earthdata"]
}
}
}

Logs go to stderr; stdout is the protocol.

Naming the extensions is what makes a tool list reproducible: the surface a client sees is otherwise whatever happens to be installed beside the server, so an environment carrying one extra extension answers differently from a bare one.

Transport options​

The SDK takes them where the application is built, which the router does — so a deployment that needs sessions says so once and every selection is served the same way:

app = create_mcp_app(host, path="/mcp", app_options={"stateless_http": False})

Another toolset is not another transport.

Two things a host of the SDK's app must get right​

Both are handled by create_mcp_app, and both are worth knowing if you build your own router.

Reach the app with the path the client sent. Mounting the SDK's application strips the prefix and redirects /mcp to /mcp/ — which a client posting JSON-RPC does not follow, so the session never starts and the endpoint looks broken rather than misrouted. ToolsetRouter builds each app for the path it serves and hands the request over unchanged.

Run each app's lifespan. The SDK's application starts its session manager there, and a sub-application's lifespan is not run by the application that routes to it: an app served without one answers every request with "Task group is not initialized". Each lifespan runs in a task of its own, started the first time that set of toolsets is asked for and kept until shutdown — not opened inline in the request that first asked for it, because a cancel scope entered in one task and left in another is an error anyio raises at the end, on shutdown, where nobody is looking for it.