MCP gateway guide
Connect remote MCP servers once, group their tools into toolsets with approvals and limits, and give any MCP client or model one URL to use them.
Concepts
Register the remote MCP servers you use (GitHub, Linear, your own) once, with their credentials stored sealed. Group them into toolsets. Each toolset is itself an MCP server that any MCP client (Claude, ChatGPT, Cursor, VS Code, an agent framework) can connect to with a Yelno API key or with OAuth, and that /v1/responses can use by reference. Every tool call through it is governed (allow and deny lists, read-only mode, approvals, rate limits, kill switches), traced, and optionally priced. Manage all of it in the console under MCP.
Ownership. An organization connection has one shared credential, used for every call. An end_user connection has none: each of your own users connects their own account, and every call uses the caller's account (see End users).
What it is not. No local or stdio servers, no hosted code, no arbitrary agent tool execution. The gateway does not judge what a tool does; it enforces the policy you set around it. For one-off tools on a server you name in the request, and for Yelno's own MCP server at /v1/mcp, see MCP tools and server.
Connect a server
From the directory. The MCP server directory lists remote servers that platform staff verified against each vendor's own documentation. Connecting one takes its URL, transport, auth type and header name from the entry, so the request only needs directory_entry_id. A connection keeps working if its entry is later disabled.
Custom server. Give a server_url (https, no user info, no fragment) and an auth type. Only the Streamable HTTP transport (streamable_http) is supported; the deprecated HTTP+SSE transport is refused with 400 mcp_transport_unsupported. The slug is derived from the name when you leave it out and must be unique in your organization.
- A credential is accepted only for an organization connection with bearer or header auth (single line, up to 8 KiB); otherwise 400 mcp_credential_invalid. It is never returned: reads show a hint such as Bearer ••••3f9a.
- For none, bearer and header organization connections, creating runs a health check straight away. A failed check does not fail the create: the connection comes back unhealthy with status_detail saying why.
- An oauth2 connection starts needs_auth. Authorize it for the organization, or hand out connect links for an end-user connection.
- Registering a connection lets the gateway send data to a new host, so it counts as a data-policy relaxation. If your organization has an approval policy for data_policy.relax, the create (and a PATCH that changes server_url) answers 400 approval_required with error.approval_request_id; once that request is approved, resubmit with X-Yelno-Approval: <approval_request_id>.
- Hosts the platform blocks are refused with 403 mcp_host_blocked. Stored connections do not need the host on the ad-hoc allow-list; that list governs only server_url tools in a request.
Managed OAuth
For an oauth2 connection, Yelno acts as the MCP client and handles the vendor's OAuth for you. Start it with POST …/connections/{id}/oauth/start (the console's Connect button), which returns an authorize_url.
Re-authorizing. When a refresh fails or the server answers 401, the account turns needs_reauth and the mcp.connection.needs_reauth webhook fires. For an organization connection, start OAuth again from the connection's page in the console. For an end-user account, create a new connect link for that user: a link for a user who already has an account replaces it.
End users
With ownership: "end_user", the connection holds no shared credential. Each of your users connects their own account, keyed by an external_user_id you choose: 1 to 256 characters of [A-Za-z0-9._:@|-].
- API-key callers pick the account with the X-Yelno-External-User header (on the toolset endpoint), or with external_user_id on a Responses tool (or the same header). It only selects an account inside your key's organization; it never selects a tenant.
- OAuth callers use their own account: console members who connect from the console are stored as yelno_user:<user id>, and that is the account their OAuth client uses. OAuth callers cannot send X-Yelno-External-User.
- No matching account means the tool error mcp_account_missing, with a connect_hint pointing to the console. The gateway never falls back to the organization's or another user's account.
- In tools/list, an end-user connection the caller has not connected still lists its cached tools, with a note in each description to connect first.
- Set a credential for an end user from the console (PUT …/accounts/{external_user_id}) for bearer or header servers, or send them a connect link.
- An id outside that pattern is refused with 400 mcp_external_user_invalid.
Connect links
A connect link is a hosted page you hand to one of your users so they can connect their own account without a Yelno login. Create one in the console, or from your backend with an API key that has the mcp:connect scope. Links exist only for end-user connections with bearer, header or oauth2 auth; an organization connection has one shared account, which you authorize from the console. Anything else is 400 mcp_connect_link_invalid.
- The token is shown once and stored only as a hash. The link is single use: it is consumed when the account connects.
- The page lives at /mcp/connect/{token}. For OAuth servers it sends the user to the vendor; for bearer or header servers it asks for the value and checks it with tools/list before saving (400 mcp_credential_rejected when the server refuses it).
- After an OAuth sign-in the user is sent to your return_url (https; http://localhost in development) with ?mcp_oauth=connected or ?mcp_oauth=error and an error_code. Without a return_url, they land back on the hosted page, /mcp/connect/{token}?mcp_oauth=connected; your users have no Yelno login, so they are never sent to the console. The hosted page keeps showing connected after the link is spent.
- expires_in_seconds is 60 to 604,800 (7 days), default 3,600. Attempts are limited to 10 per link and 30 per minute per source IP (429 rate_limited).
Toolsets
A toolset groups up to 20 connections (members) under one policy and exposes them as one MCP server at /v1/mcp/toolsets/{id}. Your organization can have up to 50 toolsets (400 mcp_limit_reached beyond that; a 21st member is 400 invalid_request). Each member's tools appear as <namespace>__<tool> (two underscores); the namespace defaults to the connection's slug with dashes turned into underscores and matches [a-z0-9][a-z0-9_-]{0,31}.
Per member. allowed_tools (null for every tool, or a list of upstream names), denied_tools, approval_required (always asks), approval_exempt (never asks, the "always allow" list) and an optional rate_limit_per_minute.
Approval rule, first match wins: the tool is in approval_required, so it needs approval; it is in approval_exempt, so it does not; otherwise the toolset's approval_mode decides. Under destructive, a tool needs approval unless it declares readOnlyHint: true or destructiveHint: false. A tool with no annotations is destructive by the MCP specification's defaults, so it needs approval.
Tool annotations are hints the upstream server declares. They decide read-only mode and the default approval rule, so review a server's tools in the console before you rely on them.
Connect a client
Point any MCP client that speaks Streamable HTTP at the toolset URL, shown on the toolset's page in the console:
Claude and ChatGPT. In claude.ai or Claude Desktop, add a custom connector and paste the toolset URL; in ChatGPT, enable developer mode and add a connector with the URL and OAuth. Both sign in with OAuth: you see Yelno's consent screen, choose allow, and the client gets a token for that one toolset. Claude Desktop can also use an API key through the mcp-remote bridge. Cursor and VS Code take the URL in their config files:
From code, use the Responses API on Yelno with a toolset reference, the Anthropic SDK's MCP connector, or plain JSON-RPC:
API keys. The key must carry mcp:toolset or inference:invoke, belong to the toolset's organization, and match the toolset's project or key scoping when set. Create a dedicated key with only mcp:toolset for clients that need nothing else.
OAuth. The toolset needs oauth_enabled. The person signing in must be a member of the toolset's organization with the key:create permission, and must keep it: the token stops working when they lose it. Tokens are bound to one toolset URL and are accepted nowhere else, not even by the management API. Access tokens last an hour and refresh for up to 30 days. Revoke a client's access from the organization's grants list (POST …/mcp/oauth-grants/{id}/revoke) or your own (POST /api/v1/mcp/oauth/grants/{id}/revoke); deleting a toolset revokes all of its grants.
Use from the Responses API
Instead of naming a server_url, a Responses mcp tool can reference a stored connection or a toolset by id or slug. No credentials appear in the request, and they are never sent to the model provider, logged or stored in the trace.
- connection and toolset are mutually exclusive, and neither can be combined with server_url or headers (400 mcp_reference_conflict). Unknown references are 400 mcp_connection_not_found or mcp_toolset_not_found; a disabled or needs_auth connection is 424 mcp_list_tools_failed.
- With connection, the request's require_approval applies (default "always", as for server_url).
- With toolset, its members, filters, read-only mode and key scoping apply (a key outside the scoping is 400 mcp_toolset_key_not_allowed), and the model sees <server_label>__<namespace>__<tool>. A call needs approval when the toolset's rule or the request's require_approval says so: a request can only tighten.
- Approvals use the Responses round trip (mcp_approval_request, then mcp_approval_response; see MCP tools), not the inbox.
- The external user comes from the tool's external_user_id, else the X-Yelno-External-User header.
- The org host allow-list is not consulted for stored connections; platform host blocks are.
- stream: true is 400 mcp_streaming_unsupported. MCP tools are not offered on /v1/chat/completions; use /v1/responses.
Approvals
When the approval rule says a call on the toolset endpoint needs a human decision, the call is not made. A pending approval appears in the console's approvals inbox, where a member with approval:decide can approve, deny, or approve with "always allow" (which adds the tool to the member's approval_exempt list). The mcp.approval.requested webhook fires; it carries ids, the tool name and the approval URL, never the arguments.
People signed in with OAuth to a modern client (protocol 2026-07-28) that supports form elicitation are asked in place, on a toolset with elicitation_approvals on: the person answers in the client and the call runs. API-key callers are asked in place only when the toolset also turns on elicitation_for_api_keys. It is off by default because a headless automation holding a key could approve its own calls. Declining is the tool error mcp_approval_denied. The request state is signed and expires after 10 minutes.
Every other caller, legacy protocol clients and (by default) API-key callers included, gets a tool error with a link to the inbox. Once someone approves it, call the tool again with the same arguments within 15 minutes and it runs.
- An approval matches the toolset, the namespaced tool, the exact arguments (by SHA-256) and the caller (the API key plus external user, or the OAuth grant).
- Each approval runs at most one call. A pending approval expires after 24 hours.
- The inbox shows the arguments only while your organization captures content for the mcp surface; otherwise it shows their digest.
Safety controls
Network guard and size caps. Every outbound request (MCP calls, OAuth discovery, registration, token and revocation requests, metadata document fetches) refuses private, loopback, link-local and metadata addresses at send time and never follows redirects. Caps: upstream MCP response 2 MB; OAuth documents 256 KiB; client metadata documents 16 KiB; each image, audio or embedded resource 1 MB; structuredContent 1 MB; tool descriptions 4,000 characters; 1,000 tools per connection. An oversized media item is replaced by a short text note, and oversized structuredContent is dropped with a note.
Secret redaction (redact_secrets, on by default) scans text items, embedded resource text and strings inside structuredContent, and replaces each match with [REDACTED:SECRET]. This is the only change the gateway makes to tool output. It looks for:
- provider and platform keys: sk-…, sk-ant-…, sk_live_ and rk_live_, yk_live_ and yk_test_;
- GitHub tokens: ghp_, gho_, ghu_, ghs_, ghr_, github_pat_;
- Slack tokens: xox[abprs]-;
- AWS access key ids AKIA… and ASIA…, Google API keys AIza…;
- JWTs, -----BEGIN … PRIVATE KEY----- blocks and Bearer <token>.
Prompt-injection flagging (flag_prompt_injection, on by default). A deterministic heuristic (instructions to ignore earlier instructions, role-override markers, chat template tokens, hidden-instruction phrasing), plus your guardrail policy when it scans output, checks tool output text. Hits are flagged only, never altered or blocked: in the call's trace flags, in a guardrail event (finding types and counts only) and in the result's _meta["io.yelno/flags"], so your client can decide.
Rate limits apply in four layers; the first one reached refuses the call with mcp_rate_limited and retry_after_seconds. If the shared limiter is unavailable, conservative local limits apply, never none.
Content capture. Tool arguments and outputs are stored only when your organization's data controls capture the mcp surface and zero data retention or a retention ceiling of 0 does not block it. They are then kept sealed for your capture retention. Otherwise only digests, sizes and metadata are kept.
Kill switches. Turn a connection or a toolset off with enabled: false. Platform staff can turn off your organization's gateway, one connection or one toolset, and can block hosts for everyone. Each switch takes effect on the next call, within 5 seconds.
Audit. Every configuration change is in your audit log, with credentials never in the before or after views: mcp_connection.*, mcp_account.connect and revoke, mcp_connect_link.create, mcp_toolset.*, mcp_approval.* and mcp_oauth_grant.*. Platform staff actions are audited too, including mcp_kill_switch.engage and release.
Observability
Trace. Every exchange through the gateway writes a row you can read at GET /api/v1/organizations/{org}/mcp-calls: the toolset, connection, upstream host, namespaced tool, list or call, external user, API key or OAuth client, status, error code, latency, bytes sent to the server and received from it, approval id, charge and flags. Never arguments, outputs or credentials. Filter by connection, toolset, tool, status, surface and time, and page with cursor and next_cursor.
Statistics. GET …/mcp/stats groups calls by connection, toolset, tool or day over up to 93 days (7 by default) with calls, errors, error rate, p50 and p95 latency and bytes received from upstream servers. Errors count failed and refused calls; listings are not counted.
Health. Each connection is checked every 15 minutes, or every 5 minutes while unhealthy. An organization connection (or one with no auth) runs tools/list with its credential; an end-user connection is probed for reachability, where a 401 counts as reachable. Checks are kept for 30 days and shown on the connection's page; the cached tool list refreshes on each check, after an hour (or the server's own cache time), when a call names a tool it lacks, and on demand.
Subscribe a webhook endpoint to these events:
Pricing
A gateway tool call is a tools/call forwarded upstream through a stored connection, from the toolset endpoint or from a Responses connection or toolset reference. Its per-call price is 0 (free) unless platform staff set one for the platform or for your organization. Read the price that applies to you at GET /api/v1/organizations/{org}/mcp/pricing (policy:read); it is a final price.
- When the price is above 0, it is held against your balance before the upstream call. If the hold cannot be placed, the call is not made (insufficient_funds).
- Gateway calls are paid from prepaid credit only, also for organizations on postpaid billing: a call whose price exceeds the available prepaid balance is refused with insufficient_funds.
- The hold is captured only when the server returns a successful result. A failure, timeout, tool error, refusal or approval request releases it, and nothing is charged.
- Model turns in a Responses run are billed as normal inference, at each model's final price.
- Ad-hoc server_url tools are not gateway calls and are never priced. Yelno's own chat tool is charged as an ordinary request.
Troubleshooting
Every route, shape and error code is in the MCP gateway API reference.