mcp-combiner
An MCP aggregator — fronts multiple MCP servers behind a single Streamable HTTP endpoint, so
one connection exposes every backend server's tools. Built on
FastMCP. Shareable across clients (via sharedserver), it
powers the mcp-companion Neovim plugin and the
mcp-combiner Claude Code plugin, and works standalone with any MCP client.
PyPI package · command · import package:
mcp-combiner/mcp-combiner/mcp_combiner.
⚠️ Renamed from
mcp-bridge. If you ran an earlier build:
- command/import are now
mcp-combiner/mcp_combiner; reinstall:uv tool uninstall mcp-bridgethenuv tool install …(see Install below).- config env vars
MCP_BRIDGE_*→MCP_COMBINER_*(andMCP_COMPANION_COMBINER_URL→MCP_COMPANION_COMBINER_URL).- OAuth token storage moved to
~/.cache/mcp-combiner/— you'll re-authenticate each MCP server once (old tokens under~/.cache/mcp-companion/are no longer read).
Install
Needs only uv — uvx fetches and runs it, no venv to manage:
uvx mcp-combiner --help # once published to PyPI
# before PyPI (or to track main) — the package lives in the combiner/ subdirectory:
uvx --from "git+https://github.com/georgeharker/mcp-companion#subdirectory=combiner" mcp-combiner
Or install it: uv pip install mcp-combiner (PyPI), or from the repo subdir
uv pip install "git+https://github.com/georgeharker/mcp-companion#subdirectory=combiner".
Usage
mcp-combiner --config /path/to/servers.json --port 9741
Interactive resource host (mcp-app widgets)
The combiner serves interactive widget resources itself: open
/ui/<token>/?resource=<uri> and it renders a sandboxed host page (provider
HTML on a second loopback origin — port+1 — never same-origin with the
capability-holding page), streams tool data to the widget over SSE, and gates
widget-initiated tool calls through the same permission pipeline as agent
calls.
A tool result whose tool definition binds a widget resource
(meta.ui.resourceUri, e.g. todoist's find-tasks-by-date) holds the agent's
call in flight while you interact: the found data streams into the widget,
your actions run as tool calls through the combiner, and Done resolves the
call with a summary of the interaction. The hold budget is 50s by default
(MCP_COMBINER_UI_HOLD_TIMEOUT).
Retrieve what happened inside any widget with the meta-tools:
combiner__ui_sessions— open/recent widget sessions per chatcombiner__ui_messages— the prompts, contexts, and tool calls the widget made
Security: /ui/* is bearer-free (the path token is the capability) while
/mcp and the control routes keep the bearer gate; the sandbox denies
connect-src unless the widget's resource metadata declares domains. For
testing, the mockserver serves a spec-speaking widget at
ui://mock/mock/widget (run it with --transport http and point a
mock server entry at it). Design details:
docs/designs/interactive-resource-host.md.
Inbound authentication
By default the /mcp endpoint is unauthenticated — fine on loopback, but a
hole the moment you bind beyond 127.0.0.1 (--host) or share the box. Set a
bearer token and every request to /mcp must present Authorization: Bearer <token>:
MCP_COMBINER_AUTH_TOKEN=$(cat ~/secrets/combiner-token) \
mcp-combiner --config … --port 9741
# or, daemon-side:
mcp-combiner --config … --auth-token-file ~/secrets/combiner-token
- Unset (both) → endpoint stays open (default; nothing changes).
--auth-token-filewins over the env var; a blank/unreadable file → stays open.- Scope:
/mcpand the control routes that mutate the running server —/sessions*(session filters) and/handover*(restart handover)./healthstays open for liveness probes. Because the control routes are gated, the combiner's own callers present the token too: themcp-combinerctl and the Neovim host's REST client both readMCP_COMBINER_AUTH_TOKEN. - A missing/wrong token gets a plain
401— noWWW-Authenticate: Bearer, so standards clients surface an honest auth error instead of falling into OAuth / Dynamic Client Registration. Clients present the token pre-emptively.
The companion clients pick the token up from the same MCP_COMBINER_AUTH_TOKEN
env var (Claude Code, OpenCode, Pi, and the Neovim host) — provision it once in
your environment (or via each client's documented hook) and it flows to all of them.
Claude Code caveat. Claude Code strips secret-looking env-var names (
*TOKEN*/*KEY*/*SECRET*/…) from theheadersHelpersubprocess it spawns, so that helper cannot readMCP_COMBINER_AUTH_TOKENfrom its own environment. The Claude plugin works around it: theSessionStarthook (which runs with the full, unredacted environment) relays the token to the helper via a mode-600cc-bearer-<pid>file under$XDG_STATE_HOME/mcp-companion/, written before the session-id file and removed onSessionEnd. It's written once per SessionStart and persists across combiner reconnects, so the helper re-reads it without the hook re-running. The daemon is unaffected — it too is launched by that hook, not by a headersHelper. (mcp-companion ≥ 0.13.3.)
Reusing the gate on other FastMCP servers
The middleware lives in mcp_combiner/inbound_auth.py and is self-contained
(stdlib + starlette only), so a sibling FastMCP server (e.g. cribsheet,
svg-mcp) can vendor the file and gate its own /mcp. Each server names its own
env var — set them to the same value for one shared secret, or distinct values for
per-service isolation:
| server | env var |
|---|---|
| mcp-combiner | MCP_COMBINER_AUTH_TOKEN |
| cribsheet | CRIBSHEET_AUTH_TOKEN |
| svg-mcp | SVG_MCP_AUTH_TOKEN |
from inbound_auth import BearerAuthMiddleware, resolve_auth_token
token = resolve_auth_token("CRIBSHEET_AUTH_TOKEN") # or a --auth-token-file
if token:
app.add_middleware(
BearerAuthMiddleware, token=token,
is_protected=lambda path: path != "/health", # plain servers: gate all but health
)
The combiner, in turn, presents the backend's token when it connects — add it to
that server's servers.json entry: {"auth": {"bearer": "${CRIBSHEET_AUTH_TOKEN}"}}
(or a headers map). See Authentication → Bearer token
in the top-level README.
Development
uv sync
pytest
Metadata
Release files for mcp-combiner 0.16.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| mcp_combiner-0.16.0.tar.gz | 414.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| mcp_combiner-0.16.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 682.9 kB
Release files / mcp_combiner-0.16.0.tar.gz
| Download URL | mcp_combiner-0.16.0.tar.gz |
|---|---|
| Size | 414.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
79703b7a831c33b022710674ea88648ced7b01c0bdf77be3daa294763c2e38ae
|
|
BLAKE2b-256 checksum How to use checksums |
cd817ece2f1f9560798cf57412aceabd5b6a5d6947e943ca0a05def3982e39c9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.
Transparency logRelease files / mcp_combiner-0.16.0-py3-none-any.whl
| Download URL | mcp_combiner-0.16.0-py3-none-any.whl |
|---|---|
| Size | 268.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
db370ce9b4779759a935d5478c1c6622151c5bb83669be421025a3b336a7c91f
|
|
BLAKE2b-256 checksum How to use checksums |
42fce8a82c6a66b3513b815169da5f86a0731425137d14b334bc83b0dea01010
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.
Transparency log