The host
McpHost owns a reactor platform, the extensions registered on it, and the
servers built from them. Everything interesting happens here rather than in
the extensions — which toolsets are on, which extensions of a tool apply, what
a client ends up seeing — because that is the part that changes per request
while the extensions stay the same.
from reactor_mcp_server import build_host, load_extensions
host = build_host(load_extensions(), name="my-server")
built = host.build()
build_host registers the extensions and starts the platform. load_extensions()
reads the entry-point group; a list of instances works just as well, which is
what a test does.
Building a server
from reactor_mcp_server import parse_selection
built = host.build(parse_selection("only=spaces"))
built.server # the MCPServer to serve
built.toolsets # frozenset of the toolsets that are on
built.tool_names # what a client will list
built.unknown # names the URL used that nobody declared
A BuiltServer carries what it was built from, because "which tools am I
getting and why" is the question a hosted endpoint is asked most, and
answering it from the built server alone means reading the SDK's registry and
guessing.
Servers are cached per selection, keyed on the set of active toolsets: ?a&b
and ?b&a are one server, and so are ?a and ?toolsets=a. Building one
means walking every contribution and wrapping every handler, so two clients
asking for the same toolsets share the result. forget_built() drops the
cache — after enabling or disabling a plugin. build drops it itself when the
URL it reads wakes or stands down an extension: what is offered has changed,
so no server built before may be served again, and the router makes new
applications for new connections while the old ones finish their sessions.
What a deployment decides
The server a selection is served by
McpHost.build makes an MCPServer. A deployment that serves a subclass —
one carrying CORS, an authentication middleware, the token verifier an
operator configured — says so:
def make_server(name, instructions):
server = MyServerWithCORS(name=name, instructions=instructions)
server._token_verifier = verifier
return server
host = build_host(extensions, name="my-server", server_factory=make_server)
The instructions it is given are the host's own followed by those of every
active toolset (host.instructions_for(active)), so a server built for
?benchmarks says how to use benchmarks and one built without it does not.
A selection is served by the server built for it, so that subclass has to be
what build makes rather than something wrapped around it afterwards. A built
server made without the verifier would be an unauthenticated door beside an
authenticated one, at the same URL.
What else is on it
on_server(server) is the escape hatch, and deliberately a narrow one.
Everything an extension offers goes through the contribution points, where a
host can read it without running anything; this is for what an extension has
to do to a built server — take off a tool that was registered outside the
contribution model, add something the SDK exposes that a spec does not
describe:
class Spaces(McpExtension):
def on_server(self, server) -> None:
for name in ("list_files", "list_kernels", "connect_to_jupyter"):
server.remove_tool(name)
Called once per built server, after the tools, resources and prompts are on it, in contribution order. A server is built per set of toolsets, so an extension whose toolset is not active is not called.
Reading what is on offer
Without building anything:
host.declared_toolsets() # every toolset, once each
host.offered_tools() # every tool, extensions applied
host.offered_tools(["spaces"]) # only that toolset's
host.active_for(selection) # the toolsets a selection turns on
host.describe(selection) # what /toolsets and list_server_toolsets answer
When an extension misbehaves
One plugin never breaks the rest, and the failure is never silent:
- a tool whose extension raises is left out rather than served half-extended — an extension that narrows a tool and fails to would otherwise widen it;
- a resource or prompt that fails to register costs that one;
- an
on_serverthat raises costs that extension's action; - an extension that fails to load at all costs that extension.
Each is logged with the plugin that caused it.