MCP & agents / MCP gateway

# 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.

Remote servers you connect, nothing else. The gateway runs tools only on remote MCP servers your organization connects and allows. It does not host or run code, it does not run local (stdio) MCP servers, and every call is an HTTPS request to a server you registered, through the platform's network guard.

## 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](https://yelno.ai/console/mcp).

| Thing | Id | What it is |
| --- | --- | --- |
| connection | `mcpcn_…` | A remote MCP server your organization registered: its URL, auth type, ownership, status and cached tool list. Up to 100 per organization. |
| connected account | `mcpa_…` | The credential a connection uses. One for an organization connection; one per external user for an end-user connection. Stored sealed, never returned. |
| toolset | `mcpts_…` | A named set of connections plus a tool policy. Each toolset is itself an MCP server at POST /v1/mcp/toolsets/{id}. |
| directory entry | `mcpd_…` | A curated, staff-verified remote MCP server (URL, auth type, docs) you can connect in one click. |
| connect link | `mcpl_…` | A hosted, single-use page you hand to one of your own users so they can connect their own account. |
| approval | `mcpap_…` | A tool call waiting for a human decision in the approvals inbox. |

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](https://yelno.ai/docs/mcp).

## Connect a server

From the directory. The [MCP server directory](https://yelno.ai/mcp/servers) 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.

Create a connection

```
curl https://yelno.ai/api/v1/organizations/$ORG/mcp/connections \
  -H "Authorization: Bearer $SESSION_TOKEN" -H "Content-Type: application/json" \
  -d '{"directory_entry_id": "mcpd_…", "ownership": "organization"}'
```

| auth_type | What is sent |
| --- | --- |
| none | No credential. The server is called as-is. |
| bearer | Sends Authorization: Bearer {token}. credential is {"token": "…"}. |
| header | Sends {header_name}: {value}. credential is {"header_name": "…", "value": "…"}. Transport headers (Host, Content-*, Mcp-*, X-Yelno-Hop, …) cannot be used. |
| oauth2 | Yelno signs in to the server's authorization server for you (see Managed OAuth). No credential in the body; the connection starts needs_auth. |

- 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](https://yelno.ai/docs/mcp); 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`.

| Step | What happens |
| --- | --- |
| Discovery | Yelno sends an unauthenticated MCP request to the server and reads its WWW-Authenticate challenge, or falls back to the server's protected-resource metadata. It then fetches the authorization server's metadata. The issuer in that document must match, and the server must support PKCE S256; otherwise 424 mcp_oauth_discovery_failed. |
| Client | Tried in order: the client id and secret you stored on the connection; a client platform staff registered for the directory entry; a Client ID Metadata Document Yelno publishes, when the authorization server supports it; dynamic client registration, when it offers it. If none works, 424 mcp_oauth_client_required: register an app with the vendor and PATCH its client_id and client_secret into the connection's oauth field. |
| Authorize | You get an authorize_url built with PKCE (S256), a single-use state, the server URL as resource and the requested scopes. Open it and sign in at the vendor. The flow is valid for 15 minutes. |
| Callback | The vendor redirects to Yelno's callback, which checks the state and issuer, exchanges the code and stores the tokens sealed on the account. Your browser then lands on the return_to page you gave (a page of the console; anything else is refused with 400), by default /console/mcp/connections/{id}, with ?mcp_oauth=connected or ?mcp_oauth=error&error_code=…. The vendor's own error text is never echoed. |
| Refresh | Automatic: a worker refreshes tokens that expire within 10 minutes, and a call that finds its token within 60 seconds of expiry refreshes it first. Each refresh sends the same resource the authorization used. Rotated refresh tokens are kept. |
| Revocation | Deleting an account or a connection asks the vendor to revoke its tokens, when the vendor lists a revocation endpoint (best effort). |

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`.

`POST` `/v1/mcp/connect-links` `mcp:connect`

Create a connect link

```
import os, requests

link = requests.post(
    "https://yelno.ai/v1/mcp/connect-links",
    headers={"Authorization": f"Bearer {os.environ['YELNO_API_KEY']}"},   # scope mcp:connect
    json={
        "connection": "linear",                 # id or slug of an end-user connection
        "external_user_id": "user-42",          # your own id for this user
        "return_url": "https://app.example.com/settings/integrations",
        "expires_in_seconds": 3600,             # 60 to 604800, default 3600
    },
).json()

# Send your user to link["url"]. The token inside it is shown only this once.
print(link["id"], link["url"], link["expires_at"])
```

201

```
{ "id": "mcpl_…", "url": "https://yelno.ai/mcp/connect/ymcl_…", "token": "ymcl_…",
  "connection_id": "mcpcn_…", "external_user_id": "user-42", "expires_at": "…" }
```

- 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}`.

POST /api/v1/organizations/{org}/mcp/toolsets

```
{ "name": "Engineering agents", "slug": "eng-agents",
  "read_only": false,
  "approval_mode": "destructive",
  "members": [
    { "connection_id": "mcpcn_…", "namespace": "github",
      "allowed_tools": null,
      "denied_tools": ["delete_repository"],
      "approval_required": ["create_pull_request"],
      "approval_exempt": ["search_issues"],
      "rate_limit_per_minute": null },
    { "connection_id": "mcpcn_…", "namespace": "linear" } ],
  "rate_limit_per_minute": 600,
  "project_ids": [], "api_key_ids": [],
  "oauth_enabled": true, "include_yelno_tools": false,
  "redact_secrets": true, "flag_prompt_injection": true,
  "elicitation_approvals": true, "elicitation_for_api_keys": false,
  "reason": "Agents for the platform team" }
```

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.

| Setting | What it does |
| --- | --- |
| read_only | Only tools that declare readOnlyHint: true are listed or callable. Others are unlisted, and a call to one is treated like any unknown tool (JSON-RPC -32602). |
| approval_mode | destructive (default): calls need approval unless the tool says it is read-only or not destructive. always: every call. never: no call (unless a member's approval_required names it). |
| elicitation_approvals | Let a person signed in to a modern MCP client with OAuth approve a call in place (see Approvals). Default on. |
| elicitation_for_api_keys | Extend in-place approval to API-key callers. Default off, because a headless automation holding a key could approve its own calls. |
| rate_limit_per_minute | Calls per minute per calling principal for the whole toolset. Unset means the platform default, 600. |
| project_ids · api_key_ids | Empty: any API key of your organization. Otherwise the key must belong to one of these projects or be one of these keys (403 mcp_toolset_key_not_allowed). |
| include_yelno_tools | Also list Yelno's own tools (list_models, chat, get_usage, get_balance) to API-key callers, unprefixed and under the key's scopes. chat is charged as an ordinary request. Never listed to OAuth callers. |
| redact_secrets | Replace credentials found in tool output with [REDACTED:SECRET]. Default on. |
| flag_prompt_injection | Flag (never block) tool output that looks like an injected instruction. Default on. |
| oauth_enabled | Let MCP clients connect to this toolset with OAuth. Off: only API keys work. |
| enabled | Your kill switch for the toolset. Off: every call answers 403 mcp_toolset_disabled. |

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:

Toolset URL

```
https://yelno.ai/v1/mcp/toolsets/mcpts_…
```

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:

Client configuration

```
{
  "mcpServers": {
    "yelno-eng": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://yelno.ai/v1/mcp/toolsets/mcpts_…",
               "--header", "Authorization:${AUTH_HEADER}"],
      "env": { "AUTH_HEADER": "Bearer yk_live_…" }
    }
  }
}
```

From code, use the Responses API on Yelno with a toolset reference, the Anthropic SDK's MCP connector, or plain JSON-RPC:

From code

```
import os
from openai import OpenAI

client = OpenAI(base_url="https://yelno.ai/v1", api_key=os.environ["YELNO_API_KEY"])
response = client.responses.create(
    model="MODEL",
    input="Find open bugs about the billing page and summarize them.",
    tools=[{"type": "mcp", "server_label": "tools", "toolset": "mcpts_…"}],
)
print(response.output_text)
```

| Client | Sign-in | How |
| --- | --- | --- |
| Claude (claude.ai, Claude Desktop custom connector) | OAuth | Add a custom connector and paste the toolset URL. Claude registers itself and opens Yelno's consent screen. |
| ChatGPT (developer mode connector) | OAuth | Add a connector with the toolset URL and choose OAuth. |
| Claude Desktop via mcp-remote | API key | The bridge sends your key as a header (config above). |
| Cursor | API key or OAuth | A headers block sends the key; a bare url makes Cursor sign in with OAuth. |
| VS Code | API key or OAuth | inputs prompts once for the key and stores it; without headers VS Code can sign in with OAuth. |
| Responses API on Yelno | API key | Reference the toolset by id or slug; no URL or credentials in the request. |
| Anthropic SDK, agent frameworks, curl | API key | Send Authorization: Bearer with a key that has mcp:toolset or inference:invoke. |

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

`POST` `/v1/responses` `inference:invoke`

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 references

```
import os
from openai import OpenAI

client = OpenAI(base_url="https://yelno.ai/v1", api_key=os.environ["YELNO_API_KEY"])
response = client.responses.create(
    model="MODEL",
    input="Open a Linear issue for the failing deploy and link the GitHub run.",
    tools=[
        {"type": "mcp", "server_label": "gh", "connection": "github",       # id or slug
         "allowed_tools": ["search_issues"], "require_approval": "never"},
        {"type": "mcp", "server_label": "eng", "toolset": "mcpts_…"},         # id or slug
        {"type": "mcp", "server_label": "mine", "connection": "linear",
         "external_user_id": "user-42"},                                     # an end-user account
    ],
)
print(response.output_text)
```

- `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](https://yelno.ai/docs/mcp)), 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](https://yelno.ai/console/mcp/approvals), 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.

Approval by elicitation

```
// The gateway answers tools/call with an input request (protocol 2026-07-28):
{ "resultType": "input_required",
  "inputRequests": { "yelno_approval": { "method": "elicitation/create", "params": {
      "mode": "form",
      "message": "Approve github__create_issue on GitHub? Arguments: …",
      "requestedSchema": { "type": "object", "required": ["approve"], "properties": {
          "approve": { "type": "boolean", "title": "Approve this call" },
          "always_allow": { "type": "boolean", "title": "Always allow this tool" } } } } } },
  "requestState": "…" }

// The client retries the same call with the person's answer and the same requestState:
"inputResponses": { "yelno_approval": { "action": "accept", "content": { "approve": true } } }
```

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.

mcp_approval_required

```
{ "isError": true,
  "content": [{ "type": "text", "text": "This call needs approval: https://yelno.ai/console/mcp/approvals/mcpap_…. Once approved, call the tool again with the same arguments within 15 minutes." }],
  "structuredContent": { "error": { "code": "mcp_approval_required", "message": "…",
      "approval_id": "mcpap_…", "approval_url": "https://yelno.ai/console/mcp/approvals/mcpap_…", "expires_at": "…" } } }
```

- 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

Connect only servers you trust. The gateway enforces your policy around each call, but it cannot judge what a tool does. A tool can change or delete data, and tool output can carry instructions aimed at the model. Keep toolsets narrow, use read-only mode where you can, and leave approvals on for tools with side effects.

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.

| Layer | Limit |
| --- | --- |
| toolset | rate_limit_per_minute per calling principal (an API key, plus its external user, or an OAuth grant). Default 600. |
| member | The member's rate_limit_per_minute, when set. |
| connection | The connection's rate_limit_per_minute across every caller, when set. |
| organization | The platform ceiling MCP_GATEWAY_ORG_RPM, 1,200 calls per minute per organization by default. |

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.

| Status | Meaning |
| --- | --- |
| pending | Just created, not checked yet. |
| healthy | The last tools/list succeeded. |
| degraded | It succeeded but took longer than 5 seconds, or some accounts need re-authorization. |
| unhealthy | The last two checks failed. |
| needs_auth | OAuth is not authorized yet, or an organization connection has no credential. |
| needs_reauth | A token refresh failed or the server answered 401. Sign in again. |
| disabled | Turned off by your organization (enabled: false) or by platform staff. |

Subscribe a [webhook endpoint](https://yelno.ai/docs/webhooks) to these events:

| Event | When |
| --- | --- |
| mcp.connection.unhealthy | A connection became unhealthy. |
| mcp.connection.needs_reauth | A connection or one account needs a new sign-in. |
| mcp.connection.recovered | An unhealthy connection is healthy again. |
| mcp.approval.requested | A call is waiting in the approvals inbox. Never carries the arguments. |

## 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

| You see | What to do |
| --- | --- |
| 424 mcp_oauth_discovery_failed | The server does not publish usable OAuth metadata, its issuer does not match, or it lacks PKCE S256. Check the server URL against the vendor's docs; some vendors need a different path for OAuth. |
| 424 mcp_oauth_client_required | The vendor does not allow automatic client registration. Register an OAuth app with the vendor (redirect URI: Yelno's callback), then PATCH oauth.client_id and oauth.client_secret on the connection and start OAuth again. |
| status needs_reauth · tool error mcp_connection_needs_reauth | The refresh token stopped working or the server answered 401. Organization connection: Re-authorize in the console (POST …/oauth/start). End-user account: send that user a new connect link. |
| status needs_auth · tool error mcp_connection_unavailable | OAuth was never completed, or the connection is turned off, staff-disabled or host-blocked. Finish OAuth or turn it back on. |
| mcp_account_missing | An end-user connection has no account for this caller. Send X-Yelno-External-User (API key) or connect from the console (OAuth). The gateway never falls back to another user's account. |
| -32602 for a tool you expected | The tool is filtered out: not in allowed_tools, in denied_tools, or not read-only on a read-only toolset. Filtered tools are unlisted and unknown to the caller. |
| 400 mcp_transport_unsupported | Only Streamable HTTP is supported. The deprecated HTTP+SSE transport cannot be connected. |
| 400 mcp_external_user_invalid | external_user_id must be 1 to 256 characters of [A-Za-z0-9._:@\|-]. |
| mcp_rate_limited | A rate limit was reached; retry after retry_after_seconds. Raise the toolset, member or connection limit if needed. |
| mcp_approval_required | Approve the call in the inbox (the approval_url), then call the tool again with the same arguments within 15 minutes. |
| mcp_approval_denied | Someone denied the call, or the client declined the elicitation. |
| 403 mcp_toolset_key_not_allowed | The toolset is scoped to certain projects or keys and yours is not one of them. |
| 403 mcp_toolset_disabled | Your organization or platform staff turned the toolset off. |
| 403 mcp_gateway_disabled | Platform staff turned off the MCP gateway for your organization. |
| 403 mcp_host_blocked | The platform refuses to contact that host. It cannot be overridden by an organization. |
| 401 with WWW-Authenticate | No credential, an invalid key, or an OAuth token for a different toolset. The client should follow resource_metadata and sign in, or send an API key with mcp:toolset or inference:invoke. |
| 404 on the toolset URL | The key belongs to another organization, or the toolset id is wrong. |
| insufficient_funds | A per-call price applies and the hold could not be placed from prepaid credit. Top up; the call was not made. |
| mcp_timeout · mcp_server_unreachable · mcp_server_error | The upstream server was slow, down or failing. Check its health history; raise call_timeout_seconds (1 to 120) if it is just slow. |
| mcp_response_too_large | The server's response was over 2 MB. |
| 400 approval_required on create | Your organization requires approval for data-policy relaxations. Once the request in error.approval_request_id is approved, resubmit with X-Yelno-Approval: <approval_request_id>. |

Every route, shape and error code is in the [MCP gateway API reference](https://yelno.ai/docs/mcp-gateway-api).

Source: https://yelno.ai/docs/mcp-gateway
