A tiny, transparent, stateless reverse proxy that lets Claude.ai
(web) connect to the Cala MCP server at https://api.cala.ai/mcp/.
One click pre-fills the repo, Dockerfile build, and port. You only need to paste your
CALA_API_KEY(set it as a secret) before clicking Deploy.
It does exactly one job: inject the X-API-KEY header that Cala requires for
authentication onto every request, while forwarding the
Model Context Protocol traffic (JSON-RPC over
HTTP + Server-Sent Events) faithfully in both directions.
⚠️ This is a prototype / reference implementation. It demonstrates a working pattern for bridging Claude.ai (web) to Cala'sX-API-KEY-authenticated MCP server, and it is intentionally minimal — there is no authentication on the proxy itself, no rate limiting, no per-user identity, no caching, and no retries. Do not treat it as a hardened, multi-tenant production gateway. A robust, officially-supported proxy is more appropriately provided by Cala.ai. See Status & limitations before deploying beyond a small, trusted setting.
Cala is a knowledge layer for AI agents. It turns the chaos of the open internet into structured, verified knowledge that an agent can call as a tool — a graph of typed entities (companies, products, people…) with structured fields, deterministic queries, and full traceability back to the source. Instead of building and maintaining brittle scraping/ingestion pipelines, you call a small set of MCP tools and get clean, typed data your agent can reason over immediately:
| Tool | What it returns |
|---|---|
knowledge_search |
A succinct, sourced natural-language answer (markdown) |
knowledge_query |
Verified knowledge as structured, typed JSON rows |
entity_search |
Entities matched by name (fuzzy) |
entity_introspection |
The field schema for an entity |
retrieve_entity |
Full information for a known entity |
- Verified, traceable answers reduce hallucination. Every fact comes back with sources, so analysts and decision-makers can audit where an answer came from — essential when the output feeds real business decisions (due diligence, market mapping, competitive research).
- One governed key for the whole org, not one per person. The API key lives in a single hosted secret (e.g. Koyeb), injected by this proxy. End users never see or handle it, and it can be rotated centrally — far better for security and compliance than distributing keys into each person's local config.
- Zero per-user setup. Cala's normal Claude integration uses a local
npx mcp-remotebridge that every user must install and configure. With this proxy, an admin adds one URL as a custom connector and every team member on Claude.ai (web) gets Cala instantly — no local tooling, no machine-specific state. - Centralized control point. Because all Cala traffic flows through one hosted service, the org gets a single place for logging, rate-visibility, key rotation, and (if ever needed) revocation — without touching end users.
Claude.ai supports custom connectors — you give it the URL of a remote MCP server and it can call that server's tools. However:
-
Cala authenticates with an
X-API-KEYrequest header. A request tohttps://api.cala.ai/mcp/without a valid key is rejected:POST https://api.cala.ai/mcp/ -> 401 {"detail":"Invalid API key"} -
Claude.ai's custom-connector UI cannot attach arbitrary request headers. There is no field to set
X-API-KEY, so a direct connection can never authenticate. -
Claude.ai connects from Anthropic's servers, not from your machine. The MCP server is reached from Anthropic's infrastructure, so a
localhostproxy is not reachable — the proxy has to be publicly hosted.
This proxy closes that gap. You host it (e.g. on Koyeb),
configure it with your Cala API key, and point Claude.ai at the proxy URL. The
proxy adds the X-API-KEY header to every upstream request transparently, so
Claude.ai never has to know the secret and never sees an auth challenge.
api.cala.ai also advertises an OAuth (Auth0) flow via a 401 +
WWW-Authenticate: Bearer challenge and a
/.well-known/oauth-protected-resource document. If you pointed Claude.ai
directly at Cala, it would detect that challenge and try to run an interactive
OAuth login — which is not what you want for a service-to-service API key.
The proxy sidesteps this entirely: because it injects a valid X-API-KEY it
returns 200 before any auth challenge is produced, and it deliberately returns
404 for /.well-known/* probes. Claude.ai therefore treats the proxy as a
no-auth MCP server and connects straight through.
X-API-KEY cannot be set here
│
┌──────────┐ MCP/JSON-RPC + SSE ┌──────────────┐ + X-API-KEY ┌────────────────────┐
│ Claude.ai │ ──────────────────────▶│ cala-mcp-proxy │ ───────────────▶│ api.cala.ai/mcp/ │
│ (web) │ ◀──────────────────────│ (this repo) │ ◀───────────────│ (Cala MCP server) │
└──────────┘ transparent └──────────────┘ authenticated └────────────────────┘
- Transparent passthrough. Built on Node's native
http/httpswith stream forwarding, so request and response bytes are relayed verbatim. SSE chunks flow through unbuffered — essential for MCP streaming responses. There is no gzip re-encoding and no client-side body timeout that could cut a long-lived stream. - Slow-call resilience. Cala's knowledge tools can take 15–20 s+ before
emitting their result event. During that idle gap the proxy injects inert
: keep-aliveSSE comments (at message boundaries, so frames are never corrupted) and enables TCP keep-alive /TCP_NODELAYon both sockets, so no intermediary drops the connection mid-call. Tunable viaSSE_HEARTBEAT_MS. - Stateless. The proxy stores nothing. The MCP
mcp-session-id(andmcp-protocol-version,last-event-id, etc.) headers are forwarded transparently in both directions. (Cala itself runs stateless and does not issue a session id.) - Observable. One structured JSON log line per request to stdout (method, path, upstream status, duration) — never the API key or message bodies.
- Secure by construction. Any client-supplied
AuthorizationorX-API-KEYheader is stripped and replaced with the configured key — the proxy is the sole authority on auth. Only the single MCP endpoint and/healthare routed; everything else (including OAuth discovery probes) is a404. - Zero runtime dependencies. The entire proxy is one TypeScript file run
natively by Node 24 (no transpile step, no
node_modulesat runtime) — a small surface to audit.
| Method(s) | Path | Behaviour |
|---|---|---|
GET / HEAD |
/health |
Health check for Koyeb / load balancers — 200 {"status":"ok"} |
POST/GET/DELETE |
/mcp |
Proxied to https://api.cala.ai/mcp/ with X-API-KEY injected |
OPTIONS |
/mcp |
CORS preflight — 204 |
| any | other | 404 (no OAuth challenge is advertised) |
| Variable | Required | Default | Description |
|---|---|---|---|
CALA_API_KEY |
yes | — | Cala API key injected as X-API-KEY on every request |
PORT |
no | 3000 |
Port the proxy listens on |
CALA_MCP_URL |
no | https://api.cala.ai/mcp/ |
Upstream MCP endpoint |
MCP_PATH |
no | /mcp |
Local path that maps to the upstream endpoint |
SSE_HEARTBEAT_MS |
no | 15000 |
SSE keep-alive interval during idle gaps; 0 disables |
Copy .env.example to .env and fill in your key for local runs.
Requires Node ≥ 24 (for native TypeScript execution).
cp .env.example .env # then edit CALA_API_KEY
npm install # dev type-defs only; no runtime deps
export CALA_API_KEY=your-key
npm start # -> listening on :3000# Health
curl -s http://localhost:3000/health
# -> {"status":"ok","upstream":"https://api.cala.ai/mcp/"}
# MCP initialize handshake (note: the proxy adds X-API-KEY for you)
curl -i -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'A valid key returns the server's initialize result (as JSON or an SSE stream);
an invalid key returns the upstream's 401 {"detail":"Invalid API key"},
forwarded faithfully.
docker build -t cala-mcp-proxy .
docker run --rm -p 3000:3000 -e CALA_API_KEY=your-key cala-mcp-proxyThe button pre-fills the repository, branch, Dockerfile builder, region, and the
3000;http;/ port mapping. Before clicking Deploy, fill in CALA_API_KEY
(mark it as a secret). After deploy, optionally switch the health check to HTTP
GET /health in the service settings.
- In the Koyeb dashboard: Create Service → GitHub → this repo, and choose Dockerfile as the build method.
- Set the environment variable
CALA_API_KEY(mark it as a secret). - Expose port
3000(HTTP). Koyeb injectsPORT, which the proxy honors. - Set the health check path to
/health(HTTP, port3000). - Deploy. Koyeb gives you a public URL such as
https://cala-mcp-proxy-<org>.koyeb.app.
In Claude.ai → Settings → Connectors → Add custom connector, enter:
https://cala-mcp-proxy-<org>.koyeb.app/mcp
(Note the /mcp path.) No authentication is configured on the Claude.ai side —
the proxy supplies the Cala key for you.
npm run typecheck # strict TypeScript type-check
npm test # offline suite against a mock upstream (no key needed)The offline suite (test/proxy.test.ts, test/config.test.ts) verifies header
injection, stripping of client-supplied auth, faithful status/JSON forwarding,
unbuffered SSE streaming, the SSE keep-alive heartbeat (bridges an idle
gap without corrupting frames), mcp-session-id passthrough, routing/404s, CORS,
and 502 handling on upstream failure — all without a real API key.
A live integration test (test/live.test.ts) runs a real initialize and
tools/list against api.cala.ai through the proxy. It is skipped unless a key
is present:
CALA_API_KEY=your-key npm run test:liveThis is a prototype — a small, auditable reference for the header-injection pattern, not a production-grade gateway. It is well suited to a single trusted team running their own instance with their own key. The limitations below are by design (the proxy has no policy layer); closing them is the natural job of an official, Cala-provided proxy.
Security
- The proxy is unauthenticated. Anyone who knows the public URL can use your Cala key — the URL is effectively a bearer secret. There is no allowlist, shared secret, or basic-auth gate. Keep the URL private, or add one.
- One shared identity. Every user of the connector acts as the same Cala principal — no per-user attribution, scoping, or revocation.
- No rate limiting and permissive CORS — nothing throttles or restricts who can call it.
Reliability / operations
- Cold starts on a free/scale-to-zero instance: the first request after idle can be slow or time out. Use a warm (paid) instance or a keep-alive ping for steady use.
- Key validity is load-bearing. If the key expires/rotates without a
redeploy, the connector returns
401and Claude.ai may fall back to the OAuth flow this proxy is designed to avoid. /healthis liveness-only — it does not probe Cala, so the proxy reports healthy even if the upstream is down.- No retries, single instance, single region.
Protocol / streaming
- The SSE keep-alive protects only the proxy→client leg; it can't prevent an idle timeout between the proxy and Cala.
- No backpressure handling in the SSE forward path (fine for Cala's small payloads; not designed for very large streamed bodies).
- Stateless only, and no server-initiated stream (Cala answers
405toGET, faithfully passed through).
For anything beyond a small, trusted deployment — multi-tenant auth, per-user keys, rate limiting, HA, SLAs — a hardened proxy is more appropriately provided by Cala.ai as part of their service.
If Claude.ai shows a parse error mentioning stop_reason=max_tokens after a Cala
tool call, this is not the proxy truncating data — the proxy delivers Cala's
response complete and byte-accurate (verified). stop_reason and the "via":"cala"
wrapper are Claude.ai / Anthropic-side markers.
The cause is the size of Cala's tool result. knowledge_search inlines the
full text of every source document in its context array — a single answer can be
~7–10k tokens (measured: ~40 KB by default, ~28 KB even with explainability:false
and return_entities:false). When the model generates a response over a result
that large, it can hit its output-token limit and truncate mid-JSON.
Workarounds (client-side — the model chooses the tool and arguments, so steer it by how you phrase the request):
- Prefer
knowledge_queryfor facts — it returns structured rows (~1.5 KB here) instead of inlined documents, and won't truncate. - Ask narrow, specific questions with
knowledge_search(fewer matched sources → smallercontext). Broad "tell me everything about X" prompts are the ones that blow the budget. - Tell the model to be concise / not echo sources.
Recommended upstream fix (Cala): add a parameter to knowledge_search to omit
or cap the inlined source text — e.g. include_context: false, max_sources: N,
or a result token budget — so the tool result fits a conversational client's
generation budget. This is the appropriate place to solve it; the proxy stays a
transparent relay and does not mutate MCP payloads.
This proxy exists to connect Claude.ai to Cala — Cala
provides the MCP server (https://api.cala.ai/mcp/) and the knowledge/entity
tools it exposes. All trademarks and the upstream service belong to Cala.ai. This
repository is an independent, unaffiliated integration utility maintained by
DealExMachina.
Built on the Model Context Protocol.
MIT © 2026 DealExMachina