Remote MCP Servers
Register your own MCP servers as agent tools — with HMAC-signed end-user identity headers on every call.
Register a server
Any standard remote MCP server speaks Streamable HTTP — stateless with a legacy-handshake fallback. Register it, and Cencori discovers its tools:
curl -X POST https://cencori.com/v1/mcp/servers \
-H "CENCORI_API_KEY: sk_secret_..." \
-H "Content-Type: application/json" \
-d '{"name":"docs-search","url":"https://mcp.example.com/mcp"}'The response is a discovery result: the server object plus its freshly discovered tools (everything counts as added on first registration). Two things happen here:
- If discovery succeeds, the server is
activeand its tools are snapshotted. - The response carries
identity_secretexactly once. Store it securely — it is never returned again.
If the server requires an OAuth credential or API key, first create a tool connection (see Actions and Approvals) and pass auth_connection_id; Cencori decrypts it only at call time and injects it as an Authorization: Bearer header.
Use the tools
Grant tools in an agent manifest (mcp.servers[].tools), or reference them directly in a run's tools array. Each grant is exposed to the model under a deterministic hosted name:
mcp__<server-id-prefix>__<tool-name>
The model never sees your server id or URL — only hosted names — so a prompt cannot steer it to a different MCP endpoint. On dispatch, Cencori routes the hosted name back to the registered server, enforces the agent's network policy (default-deny egress allowlist) before calling, and reuses the discovery transport.
Write and destructive tools follow the normal approval ladder: they create an action with mcp_server_id in its approval policy, resolved via POST /v1/actions/:id/approve; the approved dispatch calls the same server with the same identity headers.
Signed end-user identity headers
Every tools/call and approved dispatch sends three headers so your server knows which tenant and end user the call is for:
Claims:
{
"project_id": "...",
"tool": "mcp__mcpex1234__search",
"tenant_id": "acme-corp", // your external tenant id (when tenant-scoped)
"external_user_id": "u_42", // your end-user id
"installation_id": "ins_...", // null outside installed agents
"run_id": "run_...", // null for approved actions
"session_id": null,
"turn_number": null,
"iat": 1760000000
}Verify in Node (same envelope as Cencori webhook signatures):
import { createHmac, timingSafeEqual } from 'crypto';
const claims = JSON.parse(Buffer.from(headers['x-cencori-user-context'], 'base64url').toString());
const expected = `sha256=${createHmac('sha256', IDENTITY_SECRET)
.update(`${headers['x-cencori-user-timestamp']}.${headers['x-cencori-user-context']}`)
.digest('hex')}`;
const ok = timingSafeEqual(Buffer.from(expected), Buffer.from(headers['x-cencori-user-signature']));
// Then check Math.abs(now - claims.iat) <= 300 before honoring the context.Treat an unsigned or invalid call as a protocol violation — reject it and do not fall back to unauthenticated behavior.
Spinoffs, snapshots, and hygiene
POST /v1/mcp/servers/:id/refresh-toolsre-discovers and reports added/removed tools; unhealthy servers surfacelast_error.- Manifests snapshot granted tools at publish (and delegate runs pin the effective version), so a tool disappearing mid-run is rejected, not guessed.
- Lost the secret? Re-register the server (a fresh
identity_secretis minted). Server ids use themcp_prefix; all Embedded Agents ids are returned and accepted with their API prefix.

