This page is the map. It names every layer of the broker, states the one rule per layer that is most often got wrong, and links each concept to the document that specifies it and to the source file that implements it. Read it before any other page; then read only the pages the task needs.
If you can reach a running broker, ask it instead: the reserved _broker slot serves broker_guide({ topic? }) and broker_diagnose({ slot? }), generated from the broker’s own source and completed with the deployment’s effective configuration. When they disagree with this page, they win. The same guide pages are published statically under guide/.
mcp-broker puts many MCP servers behind one host and one port: each server occupies a named slot, servers reach their slot from the inside as providers, and any standard MCP client reaches a slot from the outside, alone at /<slot>/mcp or aggregated with every other opted-in provider at /_all/mcp.
MCP clients: Claude, MCP Inspector, SDKs, agents, a browser page
| Streamable HTTP /<slot>/mcp SSE /<slot>/sse
| raw WebSocket /<slot> stdio bridge
v
+---------------------------------------------------------------------------+
| 3 CLIENT EGRESS sessions, origin check, MCP-Protocol-Version |
| 5 EDGE SECURITY OAuth 2.1 resource server, provider identities |
| 6 AUTHORITY hierarchical policy, declarations, authorize, limits, |
| budgets, audited decisions and results |
| 4 RESERVED SLOTS _broker (introspection) _all (aggregate) |
| 2 SLOTS / ROUTING lazy slots, broker-owned request ids, subscriptions |
| 7 TELEMETRY traceparent on every request, broker/telemetry, OTLP |
| 8 DISCOVERY mDNS/DNS-SD (opt-in), RFC 9728 metadata, self-docs |
| 1 PROVIDER INGRESS /provider/<slot> /providers stdio remote .mcpb |
+---------------------------------------------------------------------------+
^
| DirectTransport, MultiplexTransport, LoopbackTransport
| libmcpb (C99), McpBroker (Unreal), Python Provider
MCP servers: browser pages, Node apps, ESP32 boards,
Unreal Engine simulations, stdio tools, remote servers
The broker is one Node process. The whole tunnel is JSON-RPC 2.0; the broker translates framing between transports and rewrites request ids, and otherwise relays MCP as published.
| Term | Meaning |
|---|---|
| slot | A name under which one MCP server is reachable. Created lazily on first reference; never declared. An empty slot answers -32000 Provider "<slot>" not connected |
| provider | An MCP server that publishes into a slot. On the tunnel it is a WebSocket client of the broker, which is why the word “client” is avoided for it |
| client (or consumer) | An MCP client that calls a slot over a standard MCP transport. It never speaks the tunnel protocol |
| dedicated endpoint | ws[s]://<host>/provider/<slot>, one slot per socket, plain JSON-RPC frames, DirectTransport |
| multiplexed endpoint | ws[s]://<host>/providers, N slots per socket, { provider, payload } envelopes, MultiplexTransport |
| upstream | A provider the broker starts or dials itself: stdioUpstreams[], mcpServers[], mcpbBundles[] |
| loopback provider | An MCP server in the broker’s own process, tunnel.registerLoopbackProvider(), no socket |
_broker |
Reserved slot: the broker’s own introspection MCP server. Routes to nothing |
_all |
Reserved slot: union of the tools and prompts of opted-in providers, names prefixed <slot>- |
| declaration | What a provider tells the broker about the resources it governs (broker/authorization/declare). Describes; grants nothing |
| caller reference | params._meta["io.cyanmycelium/caller"], attached by the broker to each request for a declaring provider, so the provider can ask broker/authorize about the real caller |
How an MCP server gets into a slot.
| Way in | Endpoint or key | Implemented by |
|---|---|---|
| Dedicated WebSocket | ws[s]://<host>/provider/<slot> |
DirectTransport (provider package), mcpb_provider_t (C) |
| Multiplexed WebSocket | ws[s]://<host>/providers |
MultiplexTransport (source), mcpb_mux_t (C), the Unreal plugin |
| Child process over stdio | stdioUpstreams[] |
stdio.upstream.ts |
| Remote server the broker dials | mcpServers[] |
remote.upstream.ts |
Signed .mcpb bundle |
mcpbBundles[] |
mcpb.loader.ts |
| Same process | tunnel.registerLoopbackProvider(name, transport) |
LoopbackTransport from @cyanmycelium/mcp-core |
The rule: a provider’s transport and its URL path are one decision. DirectTransport pairs with /provider/<slot>, MultiplexTransport with /providers. /providers/<slot> is neither: it is accepted as an MCP client of a slot literally named providers/<slot>. Since 1.3.0 the broker detects both mismatches on the first frame and closes with 1008; older brokers stay silent.
Joining _all is opt-in: a WebSocket provider sends {"jsonrpc":"2.0","method":"notifications/register","params":{"aggregate":true}} first (the aggregate transport option does it); a stdioUpstreams[] entry needs "aggregate": true; mcpServers[] and mcpbBundles[] join by default.
Specified in: protocol.md, endpoints.md. Router: ws.tunnel.ts. Wire envelope: envelope.ts.
brk-<n>) on the provider side; the client’s own id is restored on the answer. Two clients may both use id 1.providerRequestTimeoutMs (60 s).notifications/resources/updated, which goes only to sessions subscribed to that exact URI. The broker reference-counts subscriptions: N clients cost the provider one resources/subscribe.initialize replayed, then one subscribe per URI.sampling/createMessage, roots/list, elicitation/create) are answered -32601 by the broker, never relayed.Specified in: architecture.md, protocol.md. Source: broker.slots.ts, resource.subscription.registry.ts.
| Transport | Path |
|---|---|
| Streamable HTTP | POST, GET, DELETE /<slot>/mcp |
| Legacy SSE | GET /<slot>/sse, POST /<slot>/messages?sessionId= |
| Raw WebSocket | ws[s]://<host>/<slot> |
| stdio bridge | MCP_BROKER_STDIO_PROVIDER=<slot>; always _all for an MCP host |
Origin header is refused (403 invalid_origin) unless that exact origin is in allowedOrigins. A page served by the broker’s own static mount is not exempt. A request with no Origin (Node, Inspector, SDKs) always passes. WebSocket upgrades are not origin-checked.DELETE /<slot>/mcp keeps its session and subscriptions.tls in the config, MCP_BROKER_TLS_CERT) or a reverse proxy’s; ws:// becomes wss:// and http:// becomes https:// with nothing else changing.Specified in: endpoints.md, guide connect-client, config.md.
_broker serves broker_info, providers_list, provider_status, broker_guide, broker_diagnose and the matching broker:// resources. broker://providers accepts resources/subscribe. It proxies nothing._all exists from startup and answers initialize itself, which makes it the only safe target for a stdio MCP host. Tools and prompts only: every other method, resources/* included, is -32601. Tool names are <slot>-<tool>, capped at 64 characters, hash-suffixed when too long, -2 on collision: never rebuild a name, echo the one tools/list returned. Under authorization the catalog is filtered per caller.Specified in: architecture.md. Source: broker.server.ts, aggregate.server.ts, broker.guides.ts, broker.diagnostics.ts.
All of it is opt-in: with no auth configuration the broker authenticates nobody, which is only appropriate behind a trusted network boundary.
401 with WWW-Authenticate: Bearer resource_metadata=...; the metadata is served at /.well-known/oauth-protected-resource/<slot>/mcp (RFC 9728). auth.dev gives static per-caller tokens for local tests, loopback only by default.providers table, sent as X-Provider-Token or Authorization: Bearer. A browser cannot set those headers, so a browser provider cannot authenticate to the broker directly.1008 while the incumbent answers heartbeats (providerTakeover, default "liveness").Specified in: authorization.md, HOWTO.md (in French). Source: auth/.
The broker can be the authorization decision point for providers that govern their own kind of resource (SCADA first).
authorize(subject, capability, resourcePath) -> allow | deny.broker/authorization/declare): its domain, namespace, capabilities and resources, with optional engineering limits and resource patterns. One owner identity per domain, one declaration per (identity, slot). It grants nothing.broker/authorize, about a caller reference or about itself): one audited decision per check. A resource with limits returns allow-with-constraints, with allowed: false so a provider that ignores obligations refuses rather than oversteps.broker/audit/result): audited next to the decision; unreported results of required capabilities show up in broker_diagnose.Specified in: hierarchical-authorization.md, authorization.md, execution-limits.md, the “Declared authorization” section of AGENTS.md. Source: authorization/, authority/, limits/controller.ts.
traceparent in params._meta; a valid caller value is kept, a missing or malformed one replaced.broker/telemetry, which the broker validates, bounds, queues and exports over OTLP/HTTP. It is never broadcast to clients.correlationId and the W3C traceId are distinct and linked.Specified in: telemetry.md. Source: telemetry/.
_broker (broker_guide, broker_diagnose), and through _all._mcp-broker._tcp.local on named interfaces (mdns in the config). It advertises no credentials, no provider inventory and no version, and grants no trust.@cyanmycelium/mcp-broker.Specified in: mdns in config.md. Source: discovery/mdns.ts.
Providers do not have to be Node. c/libmcpb is a C99 tunnel client (no allocation, a six-function platform port) that speaks exactly the two WebSocket framings; it carries JSON-RPC bytes and never interprets them. It runs on an ESP32 (mcpb_esp), inside Unreal Engine 5 (plugin McpBroker, one multiplexed socket for every simulated device), and under Python through ctypes (mcp_provider.Provider). A simulated pump-1 and a real pump-1 publish the same slot and tool schemas.
Specified in: c/README.md, c/libmcpb/README.md, c/espressif/README.md, c/unreal/README.md, python/README.md.
A browser page publishes a tool.
The page builds an McpServer with DirectTransport("ws://<host>/provider/my-slot", { aggregate: true }), starts it, and closes the transport on pagehide. start() resolving means the socket opened, not that the slot was accepted: a refusal arrives later as a 1008 close. The page needs a bundler, and cannot authenticate as a provider. Runnable: samples/browser-provider.
An agent calls it.
The agent’s MCP client points at http://<host>/my-slot/mcp, or at http://<host>/_all/mcp and calls my-slot-<tool> exactly as tools/list spelled it. A stdio host uses the broker with MCP_BROKER_STDIO_PROVIDER=_all. Runnable: samples/host-config.
A governed write.
A declaring provider receives tools/call with a caller reference, asks broker/authorize with that reference, applies obligations.constraints when the effect is allow-with-constraints, acts, then reports the outcome with broker/audit/result. Tests: startTestBroker() from @cyanmycelium/mcp-broker/testing (testing.md).
_all is not a proxy, not automatic, and carries no resources._broker routes to nothing.start() resolving is not a connection guarantee, on either side._broker tools only; provider tools are relayed as published.