Skip to Content

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 HubAddress
On the LAN, by IP or hostnamehttp://<appliance-ip>:5002/api/mcp
Over a VPN such as Tailscalehttp://<tailscale-name>:5002/api/mcp
By its public URL / tunnelhttps://<your-hub-domain>/api/mcp β€” no port
By the appliance’s local Traefik hosthttp://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
TransportStreamable HTTP
Protocol versions2026-07-28 (stateless) and 2025-11-25 (sessionful)
AuthAuthorization: Bearer <key> β€” nothing else is accepted
EnabledBy 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:

RevisionClient styleHow it works
2026-07-28ModernStateless 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-25LegacySessionful 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 read

Full 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:

CapabilitySees
readRead-only tools β€” listing, inspecting, reading logs and config
writePlus install, start, stop, restart, reconfigure
fullPlus 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:

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.

LayerWhat it is
Hub MCP (/api/mcp)Your agent’s single entry point β€” authenticate once with a Bearer key
Marketplace MCP appA catalog container (e.g. n8n-mcp, filesystem-mcp) whose tools Hub proxies
Catalog metadataconfig.json#mcp (transport, command, env, url) plus form_fields for install

After installing an MCP app from the store:

  1. Open the app page β€” the MCP server card shows bridge status, tool count, and client config.
  2. Use hub_list_app_tools (Settings β†’ MCP tool runner, or any MCP client) to discover bridged tools.
  3. 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).

Last updated on