Every MCP gateway route: directory, connections, accounts and connect links, toolsets and their MCP endpoint, approvals, the trace and OAuth for MCP clients.
Management routes under /api/v1/organizations/{org} take a console session token and the permission shown; unknown ids answer 404, never 403. Routes under /v1 take an API key with the scope shown. The toolset endpoint also accepts Yelno MCP OAuth tokens. Errors use the gateway's error envelope, except the OAuth protocol endpoints (RFC 6749 and RFC 7591 shapes) and the MCP endpoint (JSON-RPC). Platform staff have their own routes under /api/v1/admin/mcp for the directory, host blocks, kill switches and pricing (setting a per-call price, or ending an organization's override with DELETE /api/v1/admin/mcp/pricing/{organization_id}?reason=); they are not listed here.
GET/api/v1/mcp/oauth/authorizesessionConsent payload for the consent screen.
POST/api/v1/mcp/oauth/authorizesessionAllow or deny; returns redirect_to.
POST/api/v1/mcp/oauth/tokenpublic clientAuthorization code and refresh token grants.
POST/api/v1/mcp/oauth/revokepublic clientRevoke a token (always 200).
GET/api/v1/mcp/oauth/grantssessionYour own grants.
POST/api/v1/mcp/oauth/grants/{id}/revokesessionRevoke one of your own grants.
Two browser pages complete the picture: the hosted connect page at /mcp/connect/{token} and the OAuth consent screen at /mcp/oauth/authorize. Ids and amounts in the examples below are illustrative. The walk-through is in the MCP gateway guide.
GET /api/v1/mcp/directory is public and returns {data: DirectoryEntryOut[]}, active entries only, filtered by ?category= and ?q=. GET /api/v1/mcp/directory/{id_or_slug} returns one, or 404 when it is disabled or unknown. Each entry was verified against the vendor's own documentation (source_url, verified_at). To connect one, create a connection with its directory_entry_id.
DirectoryEntryOut
{ "id": "mcpd_…", "slug": "linear", "name": "Linear", "publisher": "Linear",
"description": "Issues, projects and cycles in your Linear workspace.",
"category": "productivity", // developer | productivity | payments | data | search | knowledge// | design | crm | support | cloud | automation | ai"icon_slug": "linear",
"server_url": "https://mcp.linear.app/mcp",
"transport": "streamable_http", // the only transport supported"auth_type": "oauth2", // none | bearer | header | oauth2"auth_header_name": null, // for auth_type header, e.g. "x-api-key""alt_auth": ["bearer"], // other credential kinds the server accepts"client_registration": "auto", // oauth2: auto | preregistered (needs a client id and secret)"has_platform_client": false, // staff stored an OAuth client for this entry"docs_url": "https://linear.app/docs/mcp",
"notes": "…",
"requires_vendor_approval": false, // the vendor only admits approved clients"state": "active",
"verified_at": "…", "source_url": "https://linear.app/docs/mcp" }
// 400 when an approval policy covers data_policy.relax
{ "error": { "type": "invalid_request", "code": "approval_required",
"param": "approval_request_id", "message": "…",
"approval_request_id": "apr_…" } }
// once approved, send the same request again with
X-Yelno-Approval: <approval_request_id>
Give directory_entry_id or server_url. With an entry, URL, transport, auth type and header name come from it.
Only the Streamable HTTP transport is supported: transport: "sse" is 400 mcp_transport_unsupported.
Registering a connection lets the gateway send data to a new host, so it is a data-policy relaxation. Under an enabled data_policy.relax approval policy, create (and a PATCH that changes server_url) answers 400 approval_required with error.approval_request_id, and applies once resubmitted with X-Yelno-Approval. A blocked host is 403 mcp_host_blocked.
credential is valid only for an organization connection with bearer ({token}) or header ({header_name, value}) auth; values are single-line and up to 8 KiB; the header name must be an HTTP token and not a transport header (Host, Content-*, Mcp-*, X-Yelno-Hop, …).
For none, bearer and header organization connections the create runs a health check within call_timeout_seconds; a failure returns the connection unhealthy, not an error. oauth2 connections start needs_auth.
Up to 100 connections per organization (400 mcp_limit_reached). Other errors: mcp_server_url_invalid, mcp_slug_taken, mcp_directory_entry_unavailable, version_conflict. Changes are audited as mcp_connection.create, update and delete, with credential_set: true|false instead of any secret.
PATCH …/connections/{id} accepts name, description, slug, enabled, rate_limit_per_minute, call_timeout_seconds, server_url (a data-policy relaxation, like create), credential, oauth, version and reason. DELETE answers {id, deleted: true}: it revokes upstream OAuth tokens (best effort), deletes accounts, flows and links, and removes the connection from every toolset. POST …/check runs a health check and returns a HealthCheckOut; POST …/refresh-tools returns {tools_count, tools_refreshed_at}.
POST …/connections/{id}/oauth/start
{ "external_user_id": "user-42", // optional: an end user's account"for_me": false, // true: connect your own account (stored as yelno_user:<id>)"return_to": "…" } // optional; a console page (else 400); default /console/mcp/connections/{id}
→ { "flow_id": "mcpf_…", "authorize_url": "https://…", "expires_at": "…" }
return_to must be a page of the console's origin, else 400. The browser returns from the vendor to GET /api/v1/mcp/oauth/callback, which redirects to return_to with ?mcp_oauth=connected|error&connection={id}&error_code=…. For a connect link it redirects to the link's return_url, or without one to the hosted page /mcp/connect/{token}, never to the console. The vendor's error text is never echoed. GET /api/v1/mcp/oauth/client-metadata.json is the client metadata document Yelno presents to vendors that support it (client_name: "Yelno MCP Gateway", public client, authorization_code and refresh_token grants).
GET …/connections/{id}/accounts lists ConnectedAccountOut rows (?external_user_id=). PUT …/connections/{id}/accounts/{external_user_id} with {"credential": {…}} sets an end user's bearer or header credential. DELETE …/connections/{id}/accounts/{account_id} revokes one account, with upstream revocation where possible. External user ids are 1 to 256 characters of [A-Za-z0-9._:@|-] (400 mcp_external_user_invalid otherwise).
The token is shown once; the link is single use and consumed when the account connects. A link for a user who already has an account replaces that account. Links are valid only for end-user connections with bearer, header or oauth2 auth; an organization connection's single account is authorized from the console (400 mcp_connect_link_invalid otherwise). GET …/connections/{id}/connect-links lists links still pending.
Hosted connect routes (no sign-in; the token is the authority)
Unknown tokens answer 404. The credential route checks the value with tools/list first (400 mcp_credential_rejected). Attempts are limited to 10 per link and 30 per minute per source IP (429 rate_limited). After an OAuth sign-in the user lands on the link's return_url with ?mcp_oauth=connected|error (and error_code), or on the hosted page itself when there is none. The hosted page reads state: "connected" even after the link is spent. Connections are audited as mcp_account.connect with via: "connect_link".
POST …/mcp/toolsets takes the writable fields above plus reason. PATCH …/mcp/toolsets/{id} takes any writable field; members replaces the whole list, and version is checked (400 version_conflict). DELETE revokes the toolset's OAuth grants and tokens. Up to 50 toolsets per organization (400 mcp_limit_reached) and 20 members per toolset (400 invalid_request, param: "members"). Other errors: mcp_namespace_invalid, mcp_namespace_taken, mcp_connection_not_found, mcp_slug_taken, mcp_slug_invalid, version_conflict. Audited as mcp_toolset.create, update and delete.
Approval rule per tool, first match wins: approval_required, then approval_exempt, then approval_mode (always, never, or destructive: approval unless readOnlyHint: true or destructiveHint: false). Tools are exposed as <namespace>__<tool>; a name outside [A-Za-z0-9_.-] or over 128 characters is made safe (other characters become _, truncated, de-duplicated with _2, _3) and mapped back.
With read_only on, tools that are not read-only are unlisted, and calling one is JSON-RPC -32602 like any filtered tool. elicitation_approvals lets people signed in with OAuth approve a call in their client; elicitation_for_api_keys extends that to API-key callers and is off by default, because a headless automation could approve its own calls.
Streamable HTTP, stateless. No session id is issued and a received Mcp-Session-Id is ignored. Each request gets one application/json JSON-RPC message; a body of only notifications or responses gets 202 with no body; legacy batches get an array. GET and DELETE answer 405 with Allow: POST. A browser Origin other than the console's gets 403. Because the endpoint is stateless, initialize is optional: tools/list and tools/call work without it.
Authentication.Authorization: Bearer yk_live_… (or yk_test_…, or x-api-key) with mcp:toolset or inference:invoke, from the toolset's organization (another organization's key gets 404, -32600) and inside its project and key scoping (403, -32001 with data.error.codemcp_toolset_key_not_allowed). Or Authorization: Bearer ymcp_at_…, a live MCP OAuth token whose resource is this toolset's URL, from an active grant whose member still holds key:create. Management tokens are refused with 401. A disabled toolset is 403 with data.error.codemcp_toolset_disabled; an organization the platform switched off is 403 with mcp_gateway_disabled. The full status table is under Errors.
Without oauth_enabled, the challenge omits resource_metadata.
Protocol versions. Modern 2026-07-28 and legacy 2025-11-25, 2025-06-18 and 2025-03-26. Legacy: initialize echoes a supported requested version, else 2025-11-25; then notifications/initialized (202), ping, tools/list, tools/call; an MCP-Protocol-Version header naming an unsupported version gets 400. Modern: requests carry params._meta["io.modelcontextprotocol/protocolVersion"]; the MCP-Protocol-Version, Mcp-Method and (for tools/call) Mcp-Name headers must match the body (400, -32020); an unsupported version is 400 -32022 with data: {supported, requested}. Every modern result carries resultType. Capabilities are {"tools": {"listChanged": false}}; resources/* and prompts/* are not offered.
tools/list aggregates every enabled member's cached tools, filtered by allowed_tools, denied_tools, read-only mode and the caller's accounts, in member order then upstream order, 100 per page with an opaque nextCursor. Yelno's own tools come first when include_yelno_tools is on (API-key callers). Each tool keeps its upstream inputSchema, outputSchema, annotations, title, icons and description (up to 4,000 characters) under its namespaced name, and _meta gains "io.yelno/approval": "required" | "none". Modern results carry ttlMs: 60000 and cacheScope: "private".
tools/call checks, in order (the first failure decides):
#Check
1Unknown or filtered tool: JSON-RPC -32602.
2Connection disabled, staff-disabled, host-blocked or not authorized yet: tool error mcp_connection_unavailable. (A disabled toolset is refused with 403 before any method.)
3Read-only toolset and the tool is not read-only: the tool is unlisted and unknown, JSON-RPC -32602.
4Account: the organization's, or the end user's from X-Yelno-External-User (API key) or yelno_user:<id> (OAuth). None: tool error mcp_account_missing with connect_hint.
5Rate limits: tool error mcp_rate_limited with retry_after_seconds.
6Approval: tool error mcp_approval_required or mcp_approval_denied, or an elicitation input request for modern clients.
7Price hold when the per-call price is above 0: tool error insufficient_funds.
8The upstream tools/call with the account's credential, within call_timeout_seconds, response up to 2 MB. An expiring OAuth token is refreshed first; an upstream 401 marks the account needs_reauth (tool error mcp_connection_needs_reauth).
10Trace row, content capture when allowed, charge captured or released.
Result forwarding.text, image and audio content (base64 up to 1 MB each), resource_link and resource (embedded text or blob up to 1 MB) pass through. A larger media item becomes a text note such as [image omitted by the gateway: 3.2 MB exceeds 1 MB]. structuredContent passes through up to 1 MB, else it is dropped with a note. An upstream isError: true result passes through as-is. Results carry _meta["io.yelno/call_id"] and, when output was flagged, _meta["io.yelno/flags"].
Gateway tool errors are results with isError: true, a readable content text, and structuredContent.error = {code, message, …} (codes under Errors). An upstream isError result passes through unchanged and is traced as mcp_tool_error:
GET …/mcp/approvals (approval:read) lists {data: ApprovalOut[]}, filtered by ?status=pending|approved|denied|expired|used, ?toolset_id= and ?limit=; GET …/approvals/{id} returns one. POST …/approvals/{id}/approve (approval:decide) takes {always_allow?: false, reason?}; always_allow also adds the tool to the member's approval_exempt (an audited toolset update). POST …/approvals/{id}/deny takes {reason?}. Deciding an approval that is not pending is 409 mcp_approval_decided. Decisions are audited as mcp_approval.approve and deny.
An approved call must be retried within 15 minutes (approved_until); a pending approval expires after 24 hours; each approval runs at most one call, and the call's trace row carries approval_id. In-place approval by elicitation goes to OAuth callers on modern clients when elicitation_approvals is on, and to API-key callers only with elicitation_for_api_keys; everyone else gets the mcp_approval_required tool error with the approval_url. Approvals for Responses references use the Responses round trip instead.
GET /api/v1/organizations/{org}/mcp-calls (usage:read) takes the filters connection_id, toolset_id, tool_name, status, surface, since and until (ISO 8601), and cursor (the last row id of the previous page), alongside the existing response_id, request_id and limit. The answer carries next_cursor, null on the last page. Rows add surface (responses or gateway), connection_id, toolset_id, external_user_id, oauth_client_id, approval_id, bytes_in (received from the upstream server), bytes_out (sent to it), charge_nanos (a string) and flags (for example ["prompt_injection", "secret_redacted"]). For gateway rows, response_id is the gateway request id (mcpg_…), server_label the toolset slug, server_host the upstream host and kindlist_tools or call_tool. Rate-limit and kill-switch refusals are refused with an error_code. Rows never hold arguments, outputs or credentials.
The window defaults to the last 7 days and can span up to 93. Only call_tool rows count; errors is failed plus refused. Row keys depend on group_by: {day: "YYYY-MM-DD"}, {toolset_id, toolset_name}, {connection_id, connection_name}, and for tool the connection plus tool.
For clients that sign in with OAuth (Claude and ChatGPT connectors, Cursor, VS Code), Yelno is the authorization server. The issuer is https://yelno.ai. Tokens are opaque and bound to one toolset URL: they are valid only at that toolset's endpoint and never accepted by the management API. Both metadata documents are public; the protected-resource document is 404 for unknown toolsets and toolsets with oauth_enabled off. /.well-known/openid-configuration is not served.
Edge routing. In a deployment, the web edge must send GET /.well-known/oauth-authorization-server (exact path) and every path under /.well-known/oauth-protected-resource/ to the API, in addition to /api/* and /v1/*. Everything else (registration, token, revocation, the consent API, the callback, the client metadata document, the toolset endpoints) is already under /api/v1 or /v1. /mcp/oauth/authorize and /mcp/connect/{token} are pages of the web app.
Client registration. A client can use an https://client_id with a path that points to its client metadata document: it is fetched through the network guard (up to 16 KiB, cached 24 hours), its client_id must equal the URL exactly, it needs client_name and redirect_uris, and the consent screen shows the client's host as verified. Or it can register dynamically (deprecated in the MCP specification, still supported). Only public clients are issued: a requested client_secret_* method is answered with none. Redirect URIs (1 to 10) must be https, loopback http (127.0.0.1, [::1] or localhost, any port) or, for application_type: "native", a private-use scheme; no fragments. Registration is limited to 5 per minute per source IP and 100 per minute platform-wide; over that, 429 {"error": "slow_down"} with Retry-After. Clients with no grant are deleted after 30 days.
Authorize. The client opens https://yelno.ai/mcp/oauth/authorize with response_type=code, client_id, redirect_uri, state, code_challenge, code_challenge_method=S256, scope=mcp:toolset and resource (the toolset URL). The page signs the person in if needed and calls the consent API. Validation failures are 400 invalid_request with a code; nothing is ever redirected to an unvalidated URI. invalid_target covers a resource that is not a toolset URL, a toolset with OAuth off, or a toolset the person cannot use (they must be a member of its organization holding key:create). Older clients that send no resource get toolsets to choose from.
Allow creates or updates the grant (client, person, toolset) and issues a single-use code valid for 10 minutes; deny redirects with error=access_denied. Grants are audited as mcp_oauth_grant.create.
Token
POST /api/v1/mcp/oauth/token (form or JSON; client_id in the body)
grant_type=authorization_code&code=…&redirect_uri=…&code_verifier=…&client_id=…&resource=…
grant_type=refresh_token&refresh_token=…&client_id=…
→ { "access_token": "ymcp_at_…", "token_type": "Bearer", "expires_in": 3600,
"refresh_token": "ymcp_rt_…", "scope": "mcp:toolset" }
→ 400 { "error": "invalid_grant", "error_description": "…" }
When resource is sent with the code, it must equal the grant's toolset URL. Access tokens live 1 hour and refresh tokens 30 days, and refresh tokens rotate: reusing a rotated refresh token revokes every token of the grant. Only hashes are stored. Errors use the flat RFC 6749 shape with status 400 (invalid_client 401). POST /api/v1/mcp/oauth/revoke takes token and client_id and always answers 200.
POST …/mcp/oauth-grants/{id}/revoke (policy:update) revokes a grant and its tokens, audited. Each person can list their own grants (same row shape) at GET /api/v1/mcp/oauth/grants and revoke one with POST /api/v1/mcp/oauth/grants/{id}/revoke, which answers {id, state: "revoked"}.
For your own backend, with an API key that has the mcp:connect scope: create connect links for your users, see which accounts they connected and revoke them. X-Yelno-External-User on the toolset endpoint and on /v1/responses picks an account within the key's organization only; it never selects a tenant, and it is not accepted from OAuth callers. A key without mcp:connect gets 403 insufficient_scope.
The connect-link body is the one in Accounts and connect links, plus connection (id or slug). Revoking an account asks the vendor to revoke its tokens, best effort.
Management and /v1 key routes answer the standard error envelope, {"error": {type, message, code, param, …}}. The OAuth protocol endpoints use the RFC 6749 and RFC 7591 shapes described above.
approval_required400Connection create or server_url change under an enabled data_policy.relax approval policy. error.approval_request_id names the request; resubmit with X-Yelno-Approval.
mcp_connection_not_found · mcp_toolset_not_found404 · 400404 on management routes; 400 when named in a /v1/responses tool.
not_found404Unknown ids (never 403 across organizations), an unknown or disabled directory entry, an unknown connect link.
mcp_host_blocked403Connection create or update, and ad-hoc server_url tools on /v1/responses.
insufficient_scope403/v1/mcp/* key routes called without mcp:connect.
mcp_approval_decided409Approving or denying an approval that is no longer pending.
mcp_oauth_discovery_failed · mcp_oauth_client_required · mcp_oauth_unreachable · mcp_server_blocked · mcp_oauth_failed424oauth/start, a connect link's start, and refresh-tools.
mcp_list_tools_failed424A /v1/responses reference to a disabled, unauthorized or unreachable connection or toolset.
No or invalid credential, or a management token401 + WWW-Authenticate-32001
Key of another organization, or unknown toolset404-32600
Key without mcp:toolset or inference:invoke; key or project scoping403-32001 (data.error.code mcp_toolset_key_not_allowed for scoping)
Toolset disabled, or gateway switched off by staff403-32001, data.error.code mcp_toolset_disabled or mcp_gateway_disabled
Browser Origin other than the console's403-32600
Unsupported MCP-Protocol-Version header or _meta version400-32022 with data.supported
Modern header mismatch (MCP-Protocol-Version, Mcp-Method, Mcp-Name)400-32020
Unknown method404 (modern) · 200 (legacy)-32601
Unknown or filtered tool, bad params, bad cursor, tampered, expired or used requestState200-32602
Parse error · invalid request400-32700 · -32600
Gateway refusals and upstream failures answer HTTP 200 with a result whose isError is true and whose structuredContent.error.code is one of these. An upstream isError result passes through unchanged.
CodeMeaning
mcp_connection_unavailableConnection disabled, staff-disabled, host-blocked or not authorized yet.
mcp_connection_needs_reauthThe upstream answered 401; the account is now needs_reauth.
mcp_account_missingNo account for this caller on an end-user connection; carries connect_hint.
mcp_rate_limitedA rate limit was reached; carries retry_after_seconds.
mcp_approval_required · mcp_approval_deniedThe call needs approval (carries approval_id, approval_url, expires_at), or it was denied or declined.
insufficient_funds · mcp_price_currency_mismatchThe per-call hold could not be placed; the call was not made.
mcp_gateway_disabledThe gateway was switched off for your organization.
mcp_timeout · mcp_server_unreachable · mcp_server_errorThe upstream server timed out, could not be reached, or failed.
mcp_protocol_error · mcp_protocol_unsupportedThe upstream answered something that is not valid MCP, or only a protocol version the gateway does not speak.
mcp_input_requiredThe upstream server asked the client for input during the call, which the gateway does not relay.
mcp_server_blocked · mcp_host_blocked · mcp_redirect_refusedThe network guard refused the address, the platform blocks the host, or the server tried to redirect.
mcp_response_too_largeThe upstream response was over 2 MB.
What the gateway does and does not do. It calls only the remote MCP servers your organization connects and allows, hosts no code and runs no local servers. Redaction is the only change it makes to tool output. It cannot judge what a tool does: connect only servers you trust.