Companion Hub MCP
Companion Hub exposes a Model Context Protocol server that lets an agent manage the appliance β discover apps, install and control them, read logs, inspect configuration, and proxy calls into the apps themselves.
Find your address first
The path is always /api/mcp. What comes before it β written <hub-url> throughout this section β
depends on how you reach the appliance, and 5002 only appears when you are talking to Hubβs
published port directly.
| How you reach Hub | Address |
|---|---|
| On the LAN, by IP or hostname | http://<appliance-ip>:5002/api/mcp |
| Over a VPN such as Tailscale | http://<tailscale-name>:5002/api/mcp |
| By its public URL / tunnel | https://<your-hub-domain>/api/mcp β no port |
| By the applianceβs local Traefik host | http://ci-os-hub.local/api/mcp β no port |
5002 is fixed β unlike Companion Memory, Hub is not an app and its port is
not allocated per install. But it is only in the URL for the first two rows.
The public route enters on 443, is served by the applianceβs Traefik on
port 80, and never mentions 5002; appending it to a tunnel origin is a
connection error, not a fix.
Endpoint
| URL | <hub-url>/api/mcp |
| Transport | Streamable HTTP |
| Protocol versions | 2026-07-28 (stateless) and 2025-11-25 (sessionful) |
| Auth | Authorization: Bearer <key> β nothing else is accepted |
| Enabled | By default, unless MCP_ENABLED=false |
Hub rejects x-api-key. If you have a working setup against Companion Memory
and copy it across, changing only the URL and key, you will get 401 until
you also change the header.
The value injected into apps as HUB_MCP_URL is the complete endpoint β http://ci-os-hub:5002/api/mcp,
already carrying the path. (HUB_URL is the bare origin, http://ci-os-hub:5002.) Both use an internal
Docker hostname that only resolves inside the applianceβs container network, so an app should consume
HUB_MCP_URL as-is rather than appending /api/mcp to it. From your own machine, use the applianceβs
LAN address or a tunnel origin instead.
An earlier version of these docs described an SSE transport and a companion
HUB_MCP_MESSAGES_URL endpoint. Both were removed in the migration to
Streamable HTTP. If you are following an older guide, that is why it does not
work.
Protocol dual-stack
Hub serves two MCP protocol revisions on the same /api/mcp endpoint:
| Revision | Client style | How it works |
|---|---|---|
2026-07-28 | Modern | Stateless per-request exchange β no initialize, no Mcp-Session-Id. MCP SDK v2 clients with versionNegotiation: { mode: 'auto' } attach the per-request _meta envelope automatically. |
2025-11-25 | Legacy | Sessionful Streamable HTTP β POST initialize first, then carry the issued Mcp-Session-Id header on every follow-up POST, GET, and DELETE. This is what most agents use today. |
The Hub inspects each inbound POST and routes legacy traffic to the session registry; modern traffic is handled statelessly. Existing clients that send a plain initialize request continue to work unchanged.
If you are building a new MCP client, prefer the official TypeScript SDK v2 with
versionNegotiation: { mode: 'auto' } so it probes and selects the right era.
Legacy clients using SDK v1 or hand-rolled initialize + session headers remain
fully supported.
Getting a key
From the Hub dashboard, under Settings β Security β or on the appliance host:
cihub api-key create --name "my-client" --capability readFull detail, including how to choose a capability, is on Get Your API Keys.
What the tools can do
Hubβs tools are named hub_* and cover app discovery, lifecycle, configuration, backups, system
information, and app proxying.
Which of them your agent sees depends on your keyβs capability, because tools/list is filtered:
| Capability | Sees |
|---|---|
read | Read-only tools β listing, inspecting, reading logs and config |
write | Plus install, start, stop, restart, reconfigure |
full | Plus uninstall, reset, delete, and bulk operations |
This replaced an older appliance-wide switch that granted destructive access to every key at once. Capability is now per credential, so one agent having it does not mean all of them do.
The two app-proxy tools are gated differently, and the difference matters when
you pick a capability. hub_call_app_api is judged per call from the HTTP
method, so it appears at every capability level β but only GET, HEAD and
OPTIONS count as reads. Every other method needs full, so a write
key is refused for POST/PUT/PATCH/DELETE just as a read key is.
hub_call_app_tool is stricter still: a bridged toolβs effect is opaque to
the Hub, so it is flagged destructive outright and only a full key sees it
at all. Either way, an agent that must do more than read through an app needs
full.
Verify the connection
Do not trust a single successful call. Run the handshake and count the tools:
macOS / Linux / WSL2
BASE="<hub-url>/api/mcp"
KEY="<hub-key>"
# Keep the headers and the body, so you can read the status code and serverInfo.
HDRS=$(mktemp)
curl -s -D "$HDRS" -X POST "$BASE" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H "Authorization: Bearer $KEY" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"verify","version":"1"}}}'
head -1 "$HDRS" # HTTP status β 200 is what you want
SID=$(grep -i '^mcp-session-id:' "$HDRS" | tr -d '\r' | awk '{print $2}')
echo "session: $SID"
curl -s -X POST "$BASE" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H "Authorization: Bearer $KEY" \
-H "mcp-session-id: $SID" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'A successful initialize prints HTTP/1.1 200, returns serverInfo: {name: "ci-hub", ...} in the
body, and issues an Mcp-Session-Id header. tools/list should then return a non-empty tools
array β a full key sees the whole surface, a read key sees fewer by design.
Because these requests send Accept: ..., text/event-stream, the server may
answer with SSE frames (event: message / data: {β¦}) rather than plain
JSON. Strip the framing before piping to jq β for example sed -n 's/^data: //p'. On Hub specifically, an empty session: line means
initialize did not succeed, because Hub always mints a session id; the
status code on the line above tells you why. That inference does not
generalise β the header is optional in the Streamable HTTP spec, so other
servers can answer correctly without one.
Rotating and revoking
Settings β Security lists every key and lets you change a keyβs capability or delete it. Raising a capability asks for confirmation; lowering one does not.
From the CLI, cihub api-key list shows every key with its scopes, capability, and prefix β never the
key itself. The CLI has create and list only, so there is no subcommand for changing or revoking.
The admin routes behind the UI (PATCH /api/api-keys/:id to change a capability,
DELETE /api/api-keys/:id to revoke) are reachable from a script: Hubβs auth middleware accepts the
operator token (ciHubApiKey) as a bearer. The MCP keys minted for clients cannot call them β those
are hashed and authenticate the MCP surface only.
Mint a separate key per client so revoking one does not disturb the others.
Installed marketplace MCP servers
Hub also bridges MCP servers you install from the CI Marketplace (apps with "categories": ["mcp", β¦]).
Those apps run as containers on the appliance; Hub connects to them over stdio (docker exec) or HTTP
(when the catalog entry pins mcp.url) and re-exposes their tools through the same /api/mcp endpoint
alongside the native hub_* tools.
| Layer | What it is |
|---|---|
Hub MCP (/api/mcp) | Your agentβs single entry point β authenticate once with a Bearer key |
| Marketplace MCP app | A catalog container (e.g. n8n-mcp, filesystem-mcp) whose tools Hub proxies |
| Catalog metadata | config.json#mcp (transport, command, env, url) plus form_fields for install |
After installing an MCP app from the store:
- Open the app page β the MCP server card shows bridge status, tool count, and client config.
- Use
hub_list_app_tools(Settings β MCP tool runner, or any MCP client) to discover bridged tools. - Call app tools with
hub_call_app_toolβ names are prefixed<appUrn>__<tool_name>.
Most stdio apps require the app container to be running before bridging. host_docker listings (e.g. github-mcp) spawn an ephemeral docker run on the Hub host instead and bridge when the app is installed, with credentials from app.env.
Install-time configuration comes from form_fields in the marketplace config.json. Hub applies
catalog defaults for optional-only MCP apps (for example n8n-mcp with no credentials filled in).
The catalog keeps mcp.env in sync with those fields so agent clients see the same keys.
HTTP MCP listings must declare mcp.url in the catalog (internal http://service:port/path or a
hosted endpoint). Without it, Hub cannot bridge the server and the app details card shows
Not bridgeable.
See CI-Marketplace MCP catalog architectureΒ for author guidelines (form_fields β mcp.env, compose defaults, field types).