Custom MCP cookbook
Need an integration Leash doesn't ship — Intercom, an in-house MCP server, a third-party MCP we haven't pre-built? Register the URL and bearer token on the dashboard, fetch the config from your app, and connect. The bearer never lives in your app code.
When to use this
- The integration speaks MCP (most modern internal tools do, plus Anthropic's and Cloudflare's server registries).
- Otherwise — if it's plain OAuth — use Custom OAuth providers instead.
1. Register the MCP server on the dashboard
On the org page (/dashboard/organization), the “Custom MCP servers” card asks for:
- A slug your app uses to look it up (e.g.
intercom,acme-tools) - The MCP URL (must be HTTPS)
- Auth:
nonefor public MCPs, orbearerwith a token (encrypted at rest with your org's KMS key)
Intercom example
For Intercom's remote MCP, slug = intercom, URL = https://mcp.intercom.com/sse, auth = bearer with your Intercom access token.
2. Fetch the config from your app
GET /api/integrations/mcp-config/<slug> on the platform returns { url, headers } with the bearer Authorization already attached. Plug those two values into whichever MCP client your app uses — Leash isn't in the request path between your app and the MCP.
import { Client } from '@modelcontextprotocol/sdk/client/index.js'import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'export async function GET(req: Request) {// 1. Pull the URL + Authorization header from Leashconst cfgRes = await fetch('https://leash.build/api/integrations/mcp-config/intercom', {headers: {'X-API-Key': process.env.LEASH_API_KEY!,cookie: req.headers.get('cookie') ?? '',},})if (!cfgRes.ok) {return Response.json({ error: 'mcp_config_unavailable' }, { status: cfgRes.status })}const { data: cfg } = await cfgRes.json() as { data: { url: string; headers: Record<string, string> } }// 2. Connect with the official MCP clientconst transport = new StreamableHTTPClientTransport(new URL(cfg.url), {requestInit: { headers: cfg.headers },})const mcp = new Client({ name: 'sync-intercom', version: '1.0.0' })await mcp.connect(transport)// 3. Call toolsconst result = await mcp.callTool({name: 'list_conversations',arguments: { state: 'open', limit: 50 },})return Response.json({ conversations: result.content })}
3. Inject secrets your MCP server needs
For self-hosted MCP servers — say, an internal Python service you built — the deployed Leash app may need extra env vars (a webhook signing secret, a database URL the MCP itself can't reach). Add them to the org's secret sources and declare them in your repo's .env.example:
LEASH_API_KEY=INTERCOM_MCP_URL=INTERCOM_TOKEN=
The MCP URL and bearer token registered on the dashboard are different from these — they're fetched through /api/integrations/mcp-config/<slug> at runtime so they rotate without redeploys. Use plain env vars only for things the MCP server itself consumes.
How rotation behaves
Rotating a value in your source updates Leash immediately, but when running app code sees the new value depends on how it reads the secret:
| Read mechanism | Rotation behavior |
|---|---|
process.env.X | Bake-time. The value is mounted into the Cloud Run revision at deploy. Re-deploy (or hit “sync now”) to pick up a new value. |
await leash.env.get('X') | Runtime fetch. The next call after rotation gets the new value (per-instance cache, ~60s); { fresh: true } bypasses the cache. |
| Custom MCP bearer | Per-call resolve. The token is fetched fresh via getCustomMcpConfig on every call — picks up the new value on the next call. |
Failure modes
- HTTP 404 with
code: 'unknown_mcp_server'— slug not registered (or revoked). Check the dashboard. - HTTP 402 — Custom MCP requires Growth+ or higher.
- HTTP 401 — missing or invalid
LEASH_API_KEY. - The platform does not validate that the upstream MCP is up. If it's down, your MCP client surfaces the error directly.
Reference: SDK / Custom MCP covers the response shape and the design rationale (config-handoff instead of proxying). Error handling lists every LeashError code.