---
title: "Govern a remote MCP server"
description: "Register a remote MCP server with Anlyon, and adopt its tools as actions with approvals and receipts. Your agent can set it up; a person signs off what it can call."
---

An agent connected straight to an MCP server holds the server's credential and can call every tool on it, including tools nobody reviewed and tools whose definition changed after you trusted them. Register the server with Anlyon instead, and adopt the tools you want. The agent calls those tools as [actions](/execution/actions), the credential stays in Anlyon, and each call goes through your approvals and leaves a receipt.

This page is about MCP servers **behind** Anlyon. To connect your agent **to** Anlyon, see [MCP servers](/agents/mcp-servers).

## Why this cuts risk and effort

- **Risk: only what a person adopted can be called.** A tool you did not adopt is not offered and cannot be called through Anlyon.
- **Risk: a tool that changes stops.** An adopted tool is pinned to the definition a person reviewed. If the server changes it, the call is refused, nothing is sent, and it stays refused until a person adopts it again.
- **Risk: the credential stays out of the agent's context.** It is stored as a [secret](/execution/secrets), or as a connected account for a server that signs people in.
- **Effort: your agent does the setup.** It can register the server and read its tools from a CLI login or an API key. You make one decision, at one link, instead of working through the setup yourself.

## Set one up

1. **Register the server**

    ```bash
    anlyon mcp add https://mcp.tracker.example/mcp \
      --header "Authorization: Bearer {{secret:TRACKER}}"
    ```

    ```text
    Registered tracker: 2 tools listed, none adopted.
    TOOL          SUGGESTED    STATE        APPROVAL  ACTION
    delete_issue  destructive  not adopted  -         -
    list_issues   read         not adopted  -         -
    ```

    Anlyon lists the server's tools and adopts none of them, so no agent can call anything yet. Store the credential first in the console's **Secrets**, and name it in the header as a reference. Naming a secret needs the `secrets:read` scope.

    A registration with no stored credential applies at once. It waits for a [sign-off](/trust-control/sign-offs) when it would send a stored secret, or when the server signs people in with OAuth. Registering from the console needs no sign-off, because a person is already there.

2. **Read what it offers**

    ```bash
    anlyon mcp tools tracker
    ```

    The suggested class comes from what the server says about the tool, so treat it as a suggestion. Server text is untrusted: a server can describe a tool that deletes data as harmless. Read the tool yourself before you adopt anything that writes.

3. **Ask to adopt tools**

    ```bash
    anlyon mcp adopt tracker list_issues delete_issue
    ```

    ```text
    Needs a human's sign-off. A workspace admin must approve this change before it applies:
      https://console.anlyon.com/sign-offs/sor_...
    ```

    The command exits 15 and nothing is adopted. No API key, CLI login or MCP session can approve the request, including the one that asked. With `--wait 15m` it follows the request until a person decides.

    By default, a tool suggested as `irreversible` or `destructive` waits for approval on every call. `--approval all` gates every tool you name, and `--approval none` gates none of them. The person signing off sees which you asked for.

4. **A person signs it off**

    A workspace admin opens the link, or the email they were sent, reads the tools with their definitions and suggested classes, and approves or rejects. A person signed in to the console can also adopt from the server's page directly.

5. **Your agent calls the tool**

    Each adopted tool is an action named `<server>-<tool>`, here `tracker-list_issues`. Over [MCP](/agents/mcp-servers) it appears as the tool `action_tracker-list_issues` within 30 seconds. From the [SDKs](/sdks) and the CLI, invoke it like any other action.

## What needs a person

| Step | From an API key, CLI login or MCP session | Why |
| --- | --- | --- |
| Register a server that takes no stored credential | Applies at once | It adds nothing an agent can call |
| Register a server with no stored credential | Applies at once | Nothing stored goes anywhere, and no agent can call anything yet |
| Register a server with a stored secret | Needs a sign-off | Listing the server sends the secret to that address, before any tool is adopted |
| Register a server that signs people in with OAuth | Needs a sign-off | It can create an OAuth client at a third party under your deployment's name, and shows a sign-in page under a name the server chose |
| Read the listing again (`anlyon mcp tools --refresh`) | Applies at once | Adopted tools keep the definition they were adopted at |
| Adopt a tool | Always needs a sign-off | It is what lets an agent call the tool |
| Adopt a tool again after it changed | Always needs a sign-off | The change is what a person has to read |
| Connect your own account to a server that signs people in | Only a person, in the console | It is your consent, at the vendor |
| Archive a server | Applies at once | It archives every action adopted from it, which leave your actions list. Their call history stays |

A sign-off binds the definitions it showed. If the server's listing changes between the request and the decision, approving fails with `SIGN_OFF_STALE` and nothing is adopted: ask again and a person reads the new definition.

## When a tool changes

Anlyon checks an adopted tool's definition before each call. When it differs from what was adopted, or the server no longer lists it, the tool is not called, nothing is sent, and the evidence names `tool_changed` or `tool_missing`.

`anlyon mcp tools tracker --refresh` lists the server again and shows the tool as `changed`. To accept the change:

```bash
anlyon mcp adopt tracker list_issues
```

A person signs that off with a diff of the definition. The action keeps its approval setting and impact declaration, and an approval pending on the old definition cannot release a call against the new one.

### Hear about a change before a call fails

Anlyon also lists each registered server again on a schedule, every 24 hours by default, with the same credential as a refresh. When a listing finds an adopted tool changed or gone, or a tool it has not seen before, Anlyon sends one security alert per change to the workspace's notification channels, records `mcp_upstream.tools_changed` in the [audit log](/trust-control/audit), and marks the server in the console. The check adopts nothing and accepts nothing: a changed tool stays refused until a person adopts it again. A server that signs people in is checked once a person has connected an account on its page, or if it lists its tools without a sign-in.

## Servers that sign people in

```bash
anlyon mcp add https://mcp.tracker.example/mcp --oauth
```

`--oauth` registers a server that uses OAuth. Pass `--client-id` and `--client-secret "{{secret:NAME}}"` if you registered a client with the vendor. That secret must have allowed hosts, and they must include the vendor's token host. Otherwise Anlyon brings its own client on a verified vendor's server, and registers a client where the server allows it. If neither applies, you need a client from the vendor. The registration waits for a sign-off. A person then connects an account from the server's page in the console, which also lists its tools.

Calls that act for one of your users use that user's own account. Calls for no end user use the account the person connected.

A call for a user who has not connected the server is answered before anything is recorded or sent, as for an [integration](/execution/end-user-connections#handle-a-user-who-has-not-connected): `409 END_USER_NOT_CONNECTED`, or `409 END_USER_RECONNECT_REQUIRED` when their connection needs renewing. The answer names the server's key in `integration` (`mcp-<id>`) and its name in `displayName`. It carries a `connectUrl` when the environment has a default redirect URL. So the AI SDK adapter returns `connect_required` and `<ConnectPrompt>` shows a **Connect** card with the server's name.

To make a link yourself, call `POST /api/v2/mcp-upstreams/{id}/connect-links`, which needs `end-users:write`. In the browser, an end-user session can call `POST /api/v2/end-user-session/connect-links` with the `mcp-<id>` key from the 409. That is the call `<ConnectPrompt>` makes inside `<AnlyonProvider>`.

A call that waited for approval checks the connection again when it runs. If the user's connection was removed or revoked in the meantime, the call fails with `no_connection` and nothing is sent.

## How a tool's input is checked

Anlyon checks each call's arguments against the tool's input schema before anything is sent, as JSON Schema 2020-12 or the draft the schema names in `$schema` (draft-07 and 2019-09 too). `$ref` within the schema, `allOf`, `anyOf`, `oneOf`, `not`, `if`/`then`/`else`, `prefixItems`, `unevaluatedProperties` and the other validation keywords are enforced, and the formats `uri`, `date`, `date-time`, `email` and `uuid` are checked. Another format is only a description, as the standard says. Anlyon never fetches anything a schema points to. A schema it cannot check in full, because it points outside itself, is invalid, is very large or uses a pattern that could run for too long, does not stop the tool from being adopted: the tool is marked **Loosely checked**, its arguments only have to be a JSON object of at most 256 KiB, approval on every call is suggested for it, and an approval shows its arguments in full. The tool is still pinned, so a change on the server still stops it. The API reports this as `schemaCheck` (`full` or `loose`) with a `schemaCheckReason` on each listed and adopted tool.

## What it does not do

- **Limits need the API, at first adoption.** A tool draws on an [impact limit](/execution/impact-limits) only if the first request to adopt it, through the API or an SDK, sets its `impact`. The console, the CLI and `anlyon.yaml` adopt with none, and it cannot be added afterwards.
- **Remote servers only.** Anlyon speaks MCP's Streamable HTTP transport to an https endpoint. Servers on the older SSE transport work too: Anlyon finds that out when you add one. A stdio server is not supported.
- **Receipts grade `acknowledged`.** The server answered. Anlyon does not read the change back, so these receipts never grade `confirmed`. See [Receipts and grades](/execution/outcomes).
- **It only governs calls that go through Anlyon.** An agent that still has the server in its own configuration, or still holds the credential, can reach the server directly. Remove the direct route: see [Put your coding agent behind Anlyon](/agents/coding-agents-behind-anlyon#5-remove-the-direct-route).

## Keep it in code

`anlyon plan` and `anlyon apply` read the `mcp` section of `anlyon.yaml`. `apply` registers what is missing and asks once for each server's tools. See [Config as code](/cli/config-as-code).

## From the SDKs

```ts title="TypeScript"
import { Client, SignOffRequiredError } from '@anlyonhq/sdk';

const anlyon = new Client({ apiKey: process.env.ANLYON_API_KEY! });

const { data: upstream } = await anlyon.mcpUpstreams.register({
  name: 'tracker',
  url: 'https://mcp.tracker.example/mcp',
  headers: { Authorization: 'Bearer {{secret:TRACKER}}' },
});

try {
  await anlyon.mcpUpstreams.adopt(upstream!.id, { list_issues: {} });
} catch (error) {
  if (!(error instanceof SignOffRequiredError)) throw error;
  console.log(`Ask a workspace admin to approve: ${error.signOffUrl}`);
}
```

```python title="Python"
from anlyon import Client, SignOffRequiredError

anlyon = Client()

upstream = anlyon.mcp_upstreams.register(
    "tracker",
    "https://mcp.tracker.example/mcp",
    headers={"Authorization": "Bearer {{secret:TRACKER}}"},
)

try:
    anlyon.mcp_upstreams.adopt(upstream["id"], {"list_issues": {}})
except SignOffRequiredError as error:
    print(f"Ask a workspace admin to approve: {error.sign_off_url}")
```

Reading needs `actions:read`. Registering, refreshing, adopting and archiving need `actions:write`. Naming a stored secret also needs `secrets:read`. The [API reference](/api-reference) has every field.

## Fix the errors you will meet

| What you see | When | What to do |
| --- | --- | --- |
| `202` with `signOffRequired: true`, or exit 15 | The change needs a person | Give the link to a workspace admin |
| `The server asks for a sign-in (HTTP 401)` | The server needs a credential you did not send | Add `--oauth`, or put its credential in a header as a `{{secret:NAME}}` reference |
| `Header ... carries a credential` | A value, not a reference | Store it as a secret and write `{{secret:NAME}}` |
| `403` naming `secrets:read` | The credential cannot name stored secrets | Use a credential with `secrets:read`, or have a person register the server |
| `409`, `An MCP upstream named ... already exists` | The name is taken in this environment | Use `--name`, or archive the old one |
| `MCP_SERVER_NOT_FOUND`, `MCP_TOOL_NOT_LISTED` | The CLI could not find that server or tool | Run `anlyon mcp list`, or `anlyon mcp tools <name> --refresh` |
| `failed` with `SIGN_OFF_STALE` | The listing moved after the request was made | Ask to adopt again |
| Tool error naming `tool_changed` or `tool_missing` | The server changed or removed an adopted tool. Nothing was sent | [Adopt it again](#when-a-tool-changes) |
| `403` on `POST /api/v2/mcp-upstreams/{id}/connect` | Connecting an account is a person's consent | Open the server's page in the console |
| `409 END_USER_NOT_CONNECTED` with `integration: "mcp-..."` | The call is for a user who has not connected this server. Nothing was sent | Send the user to `connectUrl`, then call again |

## Related

- [Put your coding agent behind Anlyon](/agents/coding-agents-behind-anlyon)
- [Sign-offs](/trust-control/sign-offs): how a person decides
- [Actions](/execution/actions): what an adopted tool becomes
- [Approvals](/trust-control/approvals): what applies to each call
- [Audit](/trust-control/audit): the records registering, adopting and archiving leave
