@cyanmycelium/mcp-broker
model context protocol · websocket broker · v0.1
New mcp-broker × Model Hardware Standard: securing agent-to-hardware, today

@cyanmycelium/mcp-broker

Routes MCP clients to multiple Model Context Protocol providers through a single host. WebSocket, Streamable HTTP, SSE, and stdio transports on both sides.

statusv0.1 npmv0.1.0 licenseApache-2.0 node≥ 20.11 dotnetplanned
View on GitHub
01 - Motivation

One MCP server is rarely enough

Real-world MCP deployments don't consist of a single isolated server. An organization typically needs to expose, behind one address:

  1. Industrial assets - PLCs, SCADA, machine telemetry - wrapped as MCP servers, each speaking its own protocol behind the wrapper.
  2. Sensors and data sources the agent can query on demand, often living on different networks with different latencies.
  3. Agent hosts - micro-containers, headless engines, RPA bots - reachable as tools rather than monolithic services.
  4. Browser-side providers (3D scenes, web apps) that cannot listen on a port and need an external broker to relay JSON-RPC frames.
  5. One client, many providers - Claude, MCP Inspector, custom agents should reach any of them through a single URL instead of juggling N configurations.
@cyanmycelium/mcp-broker is the relay layer that lets all of those connect to a central point and be reached by MCP clients without each client having to know every backend.
02 - Architecture

Three roles, one process

The broker sits between two populations and translates framing where needed. Each provider gets an isolated slot with its own pending requests and notification streams.

Each slot is created lazily the first time any client references a provider name - even before that provider's WebSocket connects. While disconnected, the broker responds with a JSON-RPC error on the client's transport.

03 - Transports

Asymmetric by design

A provider speaks WebSocket; a client speaks SSE on the same slot. The broker translates framing without the participants knowing about each other.

3.1Provider side (inbound)

Transport Endpoint Use case
Dedicated WS ws[s]://broker/provider/<name> One provider, one socket. The default for in-process MCP servers.
Multiplexed WS ws[s]://broker/providers One socket carries N providers via { provider, payload } envelopes. For micro-frontend or many-scene browser apps.
Stdio upstream broker spawns child process The broker manages the lifecycle of a local stdio MCP server. Configured at startup.

3.2Client side (outbound)

Transport Endpoint Use case
Streamable HTTP POST/GET /<name>/mcp MCP 2025-03-26. MCP Inspector and modern SDKs.
Raw WebSocket ws[s]://broker/<name> Custom MCP clients, lowest framing overhead.
Legacy SSE GET /<name>/sse + POST /<name>/messages Older Claude transport. Still widely deployed.
Stdio bridge broker reads stdin, writes stdout Claude Desktop wrapping the broker as a stdio MCP server pointing at one provider.
04 - Quick start

From zero to a connected client

No installation step. The broker is a CLI; npx runs the latest published version.

Shell
$ npx @cyanmycelium/mcp-broker

⚙️  mcp-broker started
────────────────────────────────────────────────────────────────
📡  Provider WebSocket   ws://localhost:3000/provider/<name>
🔌  MCP (Streamable HTTP) http://localhost:3000/<name>/mcp
📺  Legacy SSE            http://localhost:3000/<name>/sse
────────────────────────────────────────────────────────────────

Then point any MCP provider at the WS endpoint, and any MCP client at the HTTP one:

TypeScript · provider.ts
// Inside any MCP server (uses @cyanmycelium/mcp-core or any SDK).
import { McpServerBuilder, DirectTransport } from '@cyanmycelium/mcp-core/server'

const server = new McpServerBuilder()
  .withName('my-provider')          // the slot name on the broker
  .withTransport(new DirectTransport(
    'ws://localhost:3000/provider/my-provider'
  ))
  .build()

await server.start()

The MCP client (Claude Desktop, MCP Inspector, custom) now reaches your tools at http://localhost:3000/my-provider/mcp. The provider can run anywhere - browser, container, edge - as long as it can open a WebSocket back to the broker.

05 - Self-introspection

The broker is itself an MCP server

When the broker starts, it registers itself under the reserved slot _broker. Any MCP client can connect to http://<host>/_broker/mcp and discover the broker's own state through standard MCP tools - no out-of-band documentation, no admin protocol to learn.

Tool What it returns
broker_info Identity: name, version, uptime, host, port, TLS status, configured URL paths.
providers_list Every provider slot (connected and disconnected): name, transport kind, client count, pending request count.
provider_status Detail of one slot identified by name.

Resources at broker://info, broker://providers, and the URI template broker://providers/{name} mirror the same data for clients that prefer resources/read to tools/call.

Agent perspective
// Calling _broker like any other MCP provider.

 tools/call { name: "broker_info" }
← {
  name:          "@cyanmycelium/mcp-broker",
  version:       "0.1.0",
  uptimeSeconds: 128,
  port:          3000,
  tls:           false
}

 tools/call { name: "providers_list" }
← {
  count: 3,
  providers: [
    { name: "_broker",       transport: "loopback", connected: true },
    { name: "babylon-scene", transport: "ws",       connected: true },
    { name: "scene-2",       transport: "ws",       connected: true }
  ]
}
Agents always know what they are talking to and where they can route, without any out-of-band manifest.
06 - Grammar layer

Tool descriptions tuned per client, per locale

Tool, property, resource, and resource-template descriptions live in JSON files - not in the code. The broker ships a matrix of grammars indexed by (userAgent, locale), and the session resolver picks the right combination when a client connects.

en fr zh
default packaged packaged packaged
claude packaged packaged default:zh
gpt default:en default:fr default:zh

Each filled cell is one JSON file at grammars/<userAgent>/<locale>.json. Missing cells cascade: claude:zhdefault:zhclaude:endefault:en. Add a new locale or a new user-agent family by dropping a file. No code change.

6.1Shape of a grammar file

JSON · grammars/default/fr.json
{
  "tools": {
    "broker_info": {
      "description": "Retourne le nom, la version, l'uptime, ..."
    },
    "provider_status": {
      "description": "Retourne l'état d'un emplacement de provider...",
      "properties": {
        "name": "Nom exact du provider (sensible à la casse)."
      }
    }
  },
  "resources": {
    "broker://info": {
      "name":        "Informations du broker",
      "description": "Identité, version, uptime, configuration d'écoute."
    }
  },
  "templates": {
    "broker://providers/{name}": {
      "name":        "Provider du broker",
      "description": "Snapshot d'un emplacement de provider..."
    }
  }
}

6.2Local overrides without forking

Need to retune a Claude-French description for your organization? Drop a file in .mcp-broker/grammars/claude/fr.json with only the entries you want to change:

JSON · .mcp-broker/grammars/claude/fr.json
{
  "tools": {
    "broker_info": {
      "description": "Description sur-mesure pour Claude en français, propre à ton org."
    }
  }
}

The broker merges this on top of the packaged claude:fr grammar. Entries you don't override fall through to the packaged values. Translators edit JSON; developers edit code; the two never collide.

07 - Programmatic API

Embed the broker in your own process

Useful when the broker is one component of a larger Node service (gateway, fleet manager, dev harness) and you want to wire stdio upstreams or static mounts from code.

TypeScript
import { WsTunnelBuilder } from '@cyanmycelium/mcp-broker'

const broker = new WsTunnelBuilder()
  .withPort(3000)
  .withHost('0.0.0.0')

  // Spawn a local stdio MCP server as a provider slot.
  .withStdioUpstream('fs', 'npx', ['-y', '@modelcontextprotocol/server-filesystem', '/data'])

  // Optional: serve a dev harness or admin UI.
  .withStaticMount('/', './public')

  // TLS in two lines.
  .withTlsFiles('./certs/cert.pem', './certs/key.pem')

  .build()

await broker.start()

Every builder method is also reachable via an environment variable when running the broker as a CLI (see next section). Pick the surface that suits the deployment.

08 - Configuration

One folder, or one env var per knob

Two sources, env vars always win over the file. The file is the static baseline; env vars are deploy-specific overrides.

8.1JSON config file (recommended)

Drop a .mcp-broker/ folder next to where you launch the broker. Paths inside config.json are resolved against the folder, so it stays self-contained - certs, www, and grammar overrides all live next to the config.

Layout
.mcp-broker/
├── config.json       ← broker configuration
├── certs/            ← TLS material (optional)
├── grammars/         ← local grammar overrides (optional)
└── www/              ← static dev harness (optional)
JSON · .mcp-broker/config.json
{
  "port": 3001,
  "locale": "fr",
  "tls": {
    "cert": "certs/cert.pem",
    "key":  "certs/key.pem"
  },
  "www": {
    "open": false,
    "mounts": [{ "urlPrefix": "/", "dir": "www" }]
  },
  "stdioUpstreams": [
    { "name": "fs", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/data"] }
  ]
}

Drop additional grammars/<userAgent>/<locale>.json files to override packaged tool/resource descriptions for your organization - no fork required. Full reference in node/docs/config.md.

8.2Environment variables

Variable Default Notes
MCP_BROKER_CONFIG./.mcp-broker/config.jsonPath to a JSON config file
MCP_BROKER_PORT3000TCP port
MCP_BROKER_HOST0.0.0.0Interface to bind
MCP_BROKER_LOCALEenBCP-47 tag fed to the broker grammar resolver
MCP_BROKER_PROVIDER_PATH/providerProvider WS prefix
MCP_BROKER_CLIENT_PATH/Raw WS client prefix
MCP_BROKER_MCP_PATH/mcpStreamable HTTP suffix
MCP_BROKER_WWW_DIR(unset)Shortcut: mount this directory at /
MCP_BROKER_BUNDLE_DIR(unset)Shortcut: mount this directory at /bundle
MCP_BROKER_OPEN(unset)1 to auto-open browser on startup
MCP_BROKER_TLS_CERT(unset)Path to a PEM certificate. Enables HTTPS/WSS
MCP_BROKER_TLS_KEY(unset)Path to a PEM private key. Enables HTTPS/WSS
MCP_BROKER_PROTOCOLautohttp forces plain, https forces TLS
MCP_BROKER_STDIO_PROVIDER(unset)Bridge stdin/stdout to this provider (Claude Desktop)

Env-var paths are resolved against process.cwd(); config-file paths are resolved against the config file's directory. Two different reference points by design - env is the deploy override, the file is the self-contained bundle.

09 - Install

Install

Shell
# Run without installing
$ npx @cyanmycelium/mcp-broker

# Or install globally
$ npm install -g @cyanmycelium/mcp-broker
$ mcp-broker

# Or as a dependency of a larger Node service
$ npm install @cyanmycelium/mcp-broker

Requires Node 20.11+. Apache-2.0.

Copied to clipboard