mcp-switchboard
Allows n8n to consume the tools from tunnelled MCP servers via HTTP Streamable transport, with scoped URLs to narrow tool listings.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-switchboardwhat tools are available from my home machine?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mcp-switchboard
Run MCP servers on machines behind NAT, and use their tools from anywhere — n8n, an agent, or the built-in web console — without opening a single inbound port.
A client on each machine spawns your local stdio MCP servers and opens one outbound WebSocket to a hub. The hub speaks MCP to each tunnelled server, aggregates every tool into one endpoint, and serves it over Streamable HTTP. It also gives you a console to browse what is connected, call any tool by hand, and read back every call that has ever been made.
┌── your laptop, a NATed box, anywhere ─────────────────┐
│ mcp-switchboard-client │
│ mcp.json → spawns local stdio MCP servers │
│ one outbound WSS connection, all servers multiplexed│
└───────────────────────┬───────────────────────────────┘
│ wss://…/tunnel/v1 (outbound only, token)
┌───────────────────────▼───────────────────────────────┐
│ mcp-switchboard-hub │
│ │
│ :8097 tunnel endpoint ← the only public listener │
│ ──────────────── loopback only ─────────────────── │
│ :8099 /mcp Streamable HTTP ← n8n, agents │
│ / web console │
│ /api/* JSON API │
│ /metrics Prometheus │
│ │
│ → Loki (batched) → SQLite call log │
└───────────────────────────────────────────────────────┘Why this exists
This replaces the reverse-proxy feature of mcp-context-forge, which cannot work as advertised. In every released version (checked 0.9.0 through 1.0.10), its tunnel endpoint accepts connections and then drops everything:
elif msg_type in ("response", "notification"):
# TODO: Route to appropriate MCP client ← responses are logged and discarded# TODO: Implement message queue for SSE delivery ← the SSE endpoint only sends keepalivesNothing registers tunnelled servers into its tool catalog, so the tools never
appear to a consumer. Its protocol also has both peers answer heartbeat with
heartbeat, an infinite loop we measured at ~80 messages/second per session.
Related MCP server: Universal MCP Gateway
Quick start
The hub
NixOS, via the flake:
{
inputs.mcp-switchboard.url = "github:AkosPapp/mcp-switchboard";
# in your host config
imports = [ inputs.mcp-switchboard.nixosModules.default ];
services.mcp-switchboard = {
enable = true;
# Any setting takes a literal or a path to a file holding the value, so
# sops-nix secrets need no special handling.
tunnelToken = "/run/secrets/mcp-switchboard/tunnel-token";
tunnel.host = "0.0.0.0"; # reachable by your clients
private.host = "127.0.0.1"; # console + /mcp stay local
loki.enable = true;
prometheus.register = true;
};
}Or with Docker (note it binds loopback by default, so override the hosts):
docker run -p 8097:8097 -p 8099:8099 \
-e MCP_SWITCHBOARD_TUNNEL_TOKEN=... \
-e MCP_SWITCHBOARD_TUNNEL_HOST=0.0.0.0 \
-e MCP_SWITCHBOARD_PRIVATE_HOST=0.0.0.0 \
-v mcp-switchboard:/var/lib/mcp-switchboard \
ghcr.io/akospapp/mcp-switchboard-hubThe client
On any machine with MCP servers, next to an mcp.json:
curl -fsSL https://akospapp.github.io/mcp-switchboard/install.sh | sh -s -- \
--hub-url wss://switchboard.example.com --token "$TOKEN"or, if you already have uv:
uvx mcp-switchboard-client --hub-url wss://switchboard.example.com --token "$TOKEN"The installer only makes sure npx and uvx exist (via nix shell if you have
Nix, otherwise per-user installs with no sudo) and then hands off to uvx.
mcp.json
The usual shape, as used by Claude Desktop, Cursor and VS Code:
{
"mcpServers": {
"git": { "command": "uvx", "args": ["mcp-server-git", "--repository", "."] },
"fetch": { "command": "uvx", "args": ["mcp-server-fetch"] }
}
}An mcp.json is optional: with none in the current directory the client tunnels only the
built-in harness. The client also adds a built-in harness server (files, search, git, shell, background
processes) by default, confined to the directory you start the client in; pass
--no-harness to leave it out. It gives a shell to anyone who can reach /mcp, so read
the security model first.
An entry may carry a "project" to group it (a top-level "project" sets the default
for every entry without one). Servers in a project are exposed under an extra scope, see
Connecting n8n.
Server names are unique within one client (a duplicate is rejected), so two
projects that both want an lsp need distinct names such as lsp-nix and lsp-web.
FastMCP-style entries are also recognised — any entry containing a top-level
source key is launched with fastmcp run instead. Only stdio transport is
tunnelled, since the wire protocol bridges stdin/stdout.
Connecting n8n
Point n8n's MCP Client Tool node (transport: HTTP Streamable) at the hub. The same tools are served at three scopes, so you can narrow the list instead of scrolling one enormous flat one:
URL | What it lists | Tool names |
| everything, every machine |
|
| one machine |
|
| one project on a machine |
|
| one server |
|
| one server within a project |
|
A server's project, when it has one, also appears in the broader scopes' names
(legion5__nix__lsp__hover at /mcp). The console's Endpoints panel lists the URLs
for whatever is currently connected, with example tool names, and — when
MCP_SWITCHBOARD_PUBLIC_URL is set — a copyable client-install command.
Every tool is tagged with its origin three ways, because different clients
surface different fields: in the name (above), in the title
(git_status · git @ legion5), and at the front of the description
([legion5 · git] …) — that last one matters because it is what an LLM reads
when choosing a tool. The machine-readable _meta carries host, server,
connectionId and upstreamName too.
The console shows each tool's fully-resolved exposed name, so there is never a guess about what n8n will see.
Two listeners, on purpose
The tunnel endpoint is the only thing that needs to face the internet, so it is
the only thing on the public listener, and it always requires a bearer token.
The console, the API, /mcp and /metrics live on a separate listener that
binds loopback and is unauthenticated by default — not because auth was
skipped, but because not being reachable is a stronger guarantee than a shared
secret. If you do expose it, set MCP_SWITCHBOARD_PRIVATE_TOKEN.
Configuration
Everything is configured with MCP_SWITCHBOARD_* environment variables, read
from the process environment and optionally from a .env file. See
.env.example for the full list.
Any value that starts with / and points at an existing regular file is replaced
by that file's contents. So secrets are passed as paths, with no separate
*_FILE variables:
MCP_SWITCHBOARD_TUNNEL_TOKEN=literal-value
MCP_SWITCHBOARD_TUNNEL_TOKEN=/run/secrets/mcp-switchboard/tunnel-tokenDirectories are never substituted, so path-valued settings like
MCP_SWITCHBOARD_DATA_DIR behave normally. A setting marked secret that points
at a missing file fails at startup rather than silently authenticating with the
literal string /run/secrets/....
Observability
/metrics exposes mcpsb_connections_active, mcpsb_servers{state},
mcpsb_tools_total{label,server}, mcpsb_tool_calls_total{label,server,tool,status,source},
mcpsb_tool_call_duration_seconds, mcpsb_tunnel_frames_total{direction} and
mcpsb_loki_dropped_total.
With MCP_SWITCHBOARD_LOKI_ENABLED=true, each tool call and lifecycle event is
batched and pushed to Loki. The queue is bounded and drops (counting the drops)
rather than ever blocking or failing a tool call if Loki is down.
Every call — from the console and from MCP consumers alike — is also written to a
SQLite log with its full arguments and result, browsable in the console and
subject to RETENTION_DAYS / MAX_ROWS.
Layout
hub/ the service: tunnel, MCP endpoint, console, API, exporters
client/ the tunnel client, deliberately dependency-light (no MCP dep)
servers/ first-party MCP servers (harness: filesystem, git and shell tools, on by default in the client)
nix/ NixOS module, uv2nix package set, VM test
tests/ end-to-end test: real client, real MCP server, real consumer
docs/ PROTOCOL.md, the tunnel wire formatDevelopment
nix develop # or: uv sync
pytest hub/tests client/tests servers/harness/tests -q # unit
pytest tests -q # end to end (spawns a real client process)
nix build .#checks.x86_64-linux.vm -L # NixOS VM test, needs KVMThe client and hub each carry byte-identical copies of protocol.py and
envconf.py so the client needs no dependency on the hub; a test fails if they
drift.
See spec.md for the behavioural specification.
Licence
MIT.
This server cannot be deployed
Maintenance
Related MCP Connectors
Host your MCP tool over streamable HTTP in one command.
Secure tunneling, reverse proxy and remote access for local applications.
Remote MCP server exposing SMI Aware tools, resources, and skills over Streamable HTTP.
Cloud-hosted MCP server for URnetwork VPN and Proxy
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceExposes any stdio-based MCP server to the internet via HTTP/SSE transport, enabling remote agents to access MCP tools over a network.4 npmMIT
- FlicenseNot gradedqualityCmaintenanceA self-hosted MCP gateway that aggregates all your MCP servers behind a single Streamable HTTP endpoint, with automatic registry discovery (19,000+ servers), on-demand Docker provisioning, multi-device support via SSH, OAuth2 PKCE authentication, and a workflow engine for saving and replaying multi-step tool sequences.-
- AlicenseNot gradedqualityBmaintenanceEnables stdio-only MCP clients to connect to Agent Community's hosted Streamable HTTP MCP server for accessing hosted agents and tools.14 npmMIT
- AlicenseNot gradedqualityAmaintenanceEnables MCP clients to connect to a local workspace over a public tunnel and lets them run shell commands and transfer files bidirectionally.262 npm21GPL 3.0