Skip to main content

Architecture

How colabhive-mcp fits between your AI agent and the ColabHive platform.

ColabHive runs the MCP layer in two deployment shapes simultaneously:

  1. Hosted — https://mcp.colabhive.com/mcp — multi-tenant, public.
  2. 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 ServerPool keyed by SHA-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 to api.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?​

ReasonDetail
Transport mismatchMCP 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 optionUsers 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 tuningDifferent 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:

  1. DB (source of truth): columns mcp_* on inference_endpoints plus the JOIN with model_configs and training_jobs.
  2. Cache (transient): in colabhive-mcp memory, refreshed every manifest_ttl seconds.

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

Threat model snapshot​

ThreatMitigation
API key exfil from config fileDedicated keys, filesystem permissions and immediate rotation
Replay of mutating tool callIdempotency keys where documented and host confirmation for mutating tools
Cross-tenant data leakVisibility rules inherited from Actions API; LRU pool isolation; SHA-keyed cache
Server crash kills clientsystemd 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 version
  • manifest.fetched — duration, byte count, tool count, ETag
  • mcp.tool.invoked — slug, sync, latency_ms, status, error
  • pool.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.


See also​