Architecture
How colabhive-mcp fits between your AI agent and the ColabHive platform.
ColabHive runs the MCP layer in two deployment shapes simultaneously:
- Hosted —
https://mcp.colabhive.com/mcp— multi-tenant, public. - Local stdio —
uvx colabhive-mcp— single-tenant, private to one machine.
The transformation logic, the manifest, the protocol — all identical. Only the transport and tenancy model differ.
Hosted topology (mcp.colabhive.com)
┌────────────────────────────────────────────────────────────────────┐
│ AI Agent client (Claude Desktop / Cursor / n8n) │
│ Authorization: Bearer hive_<their_key> │
└──────────────────────────────┬─────────────────────────────────────┘
│ MCP over HTTP+SSE (POST + GET)
│
┌──────────────▼──────────────┐
│ Cloudflare proxy │
└──────────────┬──────────────┘
│
┌──────────────▼──────────────┐
│ nginx (443 TLS) │
│ SSE-friendly: │
│ • proxy_buffering off │
│ • read_timeout 3600s │
│ • X-Accel-Buffering off │
└──────────────┬──────────────┘
│ http://127.0.0.1:8765
┌──────────────▼──────────────┐
│ colabhive-mcp.service │
│ (systemd, hardened) │
│ ┌────────────────────┐ │
│ │ ServerPool (LRU) │ │
│ │ per SHA(api_key) │ │
│ │ max 64 tenants │ │
│ └────────┬───────────┘ │
│ For each tenant: │
│ • httpx client │
│ • ManifestCache (TTL) │
│ • MCPHandler │
└──────────────┬──────────────┘
│ HTTPS, X-API-Key=tenant's
│
┌──────────────────────▼──────────────────────┐
│ Builder Gateway (api.colabhive.com) │
│ /api/builder/v1/mcp/manifest │
│ /api/builder/v1/actions/{slug}:invoke │
│ /api/builder/v1/invocations/{id} │
└──────────────────────┬──────────────────────┘
│
┌──────────────────────▼──────────────────────┐
│ Orchestrator → Node Runtime (unchanged) │
│ → InferenceServerManager / Specialists / │
│ DockerExecutor │
└──────────────────────────────────────────────┘
Properties:
- Stateless wrt user data. No prompts, no outputs persisted in
mcp.colabhive.com. Caches are metadata (the manifest) only. - Multi-tenant by API key. A
ServerPoolkeyed bySHA-256(api_key)[:16]keeps each tenant's manifest cache warm. LRU evicts to 64 resident tenants by default. No tenant ever sees another's tools. - No service-wide auth. The hosted box has no master key — every request must supply its own.
- TLS terminates at nginx, Cloudflare in front. Backend talks plain HTTP to
127.0.0.1:8765. - SSE knobs explicit. Streaming-protocol-safe nginx config (buffering off, long read timeouts).
Local stdio topology
┌────────────────────────────────────────────────────────────────────┐
│ Your AI Agent │
│ (Claude Desktop / Cursor / n8n) │
└──────────────────────────────┬─────────────────────────────────────┘
│ JSON-RPC over stdin/stdout
┌──────────────▼──────────────┐
│ colabhive-mcp (local) │
│ • single tenant │
│ • COLABHIVE_API_KEY env │
│ • ManifestCache (TTL) │
│ • Filter (allow/deny) │
│ • Sync/async glue │
└──────────────┬──────────────┘
│ HTTPS, X-API-Key
┌──────────────▼──────────────┐
│ Builder Gateway (same) │
└─────────────────────────────┘
Properties:
- One tenant per process. Started by the MCP client; lives only for that session.
- Key in env. Comes from
COLABHIVE_API_KEY(or--api-key). Never sent over a network except toapi.colabhive.com. - Same protocol layer. The only differences are the transport (stdio vs HTTP+SSE) and the auth source (env vs request header).
Request lifecycle
Cold start (first request from a tenant in hosted mode)
1. Client → POST /mcp with header X-API-Key: hive_X
2. server_http: extracts the key
3. ServerPool.get(key) → cache miss → instantiate ColabHiveMCPServer
4. ColabHiveClient initialized (httpx connection pool)
5. ManifestCache GETs /mcp/manifest (~150 ms)
6. MCPHandler.dispatch(initialize) → 200 OK
total: ~200-400 ms wall
tools/list
Client POST → handler → ManifestCache.get()
cache fresh? return immediately.
TTL expired? conditional GET with If-None-Match.
304 → reuse cache.
200 → replace cache.
Return filtered tools to client.
tools/call
Client POST tools/call {name, arguments}
Look up name in cached manifest (404 if absent).
Re-apply filters (allow/deny, side-effects, stability).
POST /actions/{slug}:invoke sync=true.
succeeded → result.
queued → poll /invocations/{id} until terminal or poll_max_wait.
failed → translate to MCP error.
Result → result_to_content_blocks → MCP content[].
The sync/async glue is the main value-add of the wrapper: clients see one synchronous call; ColabHive handles warm/cold paths transparently.
Why a thin wrapper rather than "native" MCP in builder-gateway?
| Reason | Detail |
|---|---|
| Transport mismatch | MCP runs over stdio or SSE. Builder-gateway is request/response HTTP, single port. Stdio in particular requires a process-launched-by-client model that doesn't fit a backend. |
| Local-first option | Users who want their key never to leave their machine need the stdio variant. We share the entire code path with the hosted server — config swaps the transport, nothing else. |
| Edge filtering | --deny-side-effects, --allow-kinds, manifest pinning happen per process / per tenant config. Putting these in builder-gateway would couple per-client policy with server policy. |
| Per-client tuning | Different MCP clients have different quirks. The wrapper is the place to special-case them. |
The manifest layer
Manifests live in two places that must stay in sync:
- DB (source of truth): columns
mcp_*oninference_endpointsplus the JOIN withmodel_configsandtraining_jobs. - Cache (transient): in
colabhive-mcpmemory, refreshed everymanifest_ttlseconds.
The MCP server never invents manifest data. Missing field in DB == missing in manifest. This honors REGLA #1: DB is the only source of truth.
kind is derived in _compute_kind() (in actions.py and inlined in mcp_manifest_lib.py for the routers) — never hardcoded strings. This honors REGLA #0: no engine names hardcoded.
Auth flow
Hosted (live)
Per request:
header → API key extracted (Authorization: Bearer ... or X-API-Key)
no key → 401 with structured JSON-RPC error
key → ServerPool[hash] → tenant server
tenant server uses that key on outbound /api/builder/v1/* calls
No server-side login, no token store, no cookies.
Local stdio (live)
Process start:
COLABHIVE_API_KEY env or --api-key arg → cfg.api_key
ColabHiveClient uses that for every outbound call
Process lives until the MCP client disconnects
OAuth 2.1 device flow (planned, F2)
Will replace API keys with revocable tokens stored in OS keychain. Optional, not bloqueante. See plan in private repo.
Threat model snapshot
| Threat | Mitigation |
|---|---|
| API key exfil from config file | F2 OAuth + keychain (planned) |
| Replay of mutating tool call | F4 HMAC-signed payloads (planned) |
| Cross-tenant data leak | Visibility rules inherited from Actions API; LRU pool isolation; SHA-keyed cache |
| Server crash kills client | systemd auto-restart; structured exit codes |
| Manifest exfil for unauthenticated discovery | /mcp/manifest requires auth; unauth POST returns 401 |
See Security for the full doc.
Performance notes
Manifest size
Order of magnitude: a few hundred endpoints × ~3 KB/manifest ≈ ~1 MB payload.
Mitigations:
- ETag → 304 Not Modified on unchanged manifests (zero body)
- Optional gzip — ~70% reduction
- Client-side filtering keeps the manifest cached but presents a subset to the agent
Cold-path latency
When the agent calls a tool whose underlying model isn't loaded:
Client → MCP server (~5 ms hosted, ~0 stdio)
MCP server → builder-gateway (~30 ms LAN)
→ orchestrator (~10 ms)
→ node-runtime WS (~10 ms)
→ pull image / load weights (~30 s - 5 min — only on real cold)
→ inference (200 ms - 10 s)
The MCP server polls /invocations/{id} until terminal. The agent sees one synchronous tool call.
latencyClass in each manifest tells the agent upfront: slow or batch means "expect long first call".
Observability
Each tenant in hosted mode emits structured JSON logs:
mcp.session.started— client connected, identity, protocol versionmanifest.fetched— duration, byte count, tool count, ETagmcp.tool.invoked— slug, sync, latency_ms, status, errorpool.tenant_added/pool.tenant_evicted— pool capacity events
In F4, these will also surface as Prometheus metrics: mcp_tool_calls_total, mcp_tool_latency_seconds, mcp_errors_total, mcp_tenants_resident.