---
title: "MCP servers"
description: "Connect any MCP client to Anlyon's hosted MCP server, to one integration's own MCP server, or to the npm package over stdio or HTTP, and act for your company or for one of your users."
icon: "plug"
---

An agent that calls production APIs needs credentials, and a credential in an agent's context can be leaked, misused or replayed. Anlyon's MCP servers give the agent your actions as tools instead. The agent names an action and passes input. Anlyon adds the credential at dispatch, applies your limits, waits for a person when a policy says so, and records a receipt for the call. The agent never holds a token.

This page is the reference for every way to connect. For step-by-step setup in one client, see [Claude Code](/agents/claude-code), [Cursor](/agents/cursor), [Codex](/agents/codex), [Windsurf](/agents/windsurf), [ChatGPT](/agents/chatgpt), the [OpenAI Responses API](/agents/openai-agents-api) or the [Claude Agent SDK](/agents/claude-agent-sdk).

## Choose a server

| Server | Address | Use it for |
| --- | --- | --- |
| Anlyon MCP server | `https://api.anlyon.com/mcp` | Every enabled action in one environment, plus approvals and workspace tools |
| One integration's MCP server | `https://api.anlyon.com/mcp/<integration>` | One integration's actions only, such as GitHub |
| npm package, stdio | `npx -y @anlyonhq/mcp-server` | A local process started by your MCP client, with an API key |
| npm package, HTTP | `npx -y @anlyonhq/mcp-server --http` | Running the MCP server over HTTP yourself, on your own host |

All four serve the same tools and enforce the same scopes, limits and approvals. Every call goes through the Anlyon API at `https://api.anlyon.com/api/v2`.

## Choose how to sign in

The hosted servers accept two credentials on the `Authorization` header. Anlyon tells them apart by shape: an Anlyon API key starts with `anlyon_`, and anything else is treated as an OAuth access token.

| | OAuth ("Login with Anlyon") | API key |
| --- | --- | --- |
| Who holds it | A coding agent or chat app acting for you | Your server or CI job |
| How to get it | Your MCP client opens a browser and you sign in | **Settings → API keys** in the [console](https://console.anlyon.com) |
| Workspace and environment | Chosen on the consent screen | Fixed: a key belongs to one environment |
| What it may do | The permission groups you granted. The tool list shows only tools they cover | The key's scopes. Every tool is listed, and the API checks the scope on each call |
| Acting for one of your users | Not allowed | Yes, with the `Anlyon-End-User` header |
| Lifetime | Access tokens last 15 minutes. Your client refreshes them | Until you revoke the key, or it expires |

### Sign in with OAuth

Point the client at `https://api.anlyon.com/mcp`. The server answers the first request with `401` and a `WWW-Authenticate` header that names its OAuth metadata (`https://api.anlyon.com/.well-known/oauth-protected-resource/mcp`). The client registers itself and opens a browser. Then:

1. Sign in to Anlyon, or create an account. A new account gets a workspace during sign-in.
2. If you belong to more than one workspace, choose the workspace and environment the client acts in. With one workspace, the connection uses its default environment.
3. Choose permissions. **Actions** is on. **Approve on your behalf**, **Switch between your workspaces**, **Secrets**, **Budgets** and **Notification channels** are off unless you turn them on.

To disconnect, remove the app in the console's settings. Its next request is refused with `401`.

### Use an API key

Send the key as a bearer token:

```text
Authorization: Bearer anlyon_live_...
```

Give the key only what the agent needs:

| Tools | Scopes |
| --- | --- |
| `list_actions` and the `action_<name>` tools appearing | `actions:read` |
| `invoke_action`, `action_<name>` | `actions:invoke` |
| `check_approval`, `wait_for_approval` | `approvals:read` |
| `request_approval` | `approvals:write` |
| `list_pending_approvals`, `decide_approval` | `approvals:read`, `approvals:decide` (privileged) |

`actions:read`, `actions:invoke`, `approvals:read` and `approvals:write` are in the **Standard** preset. `approvals:decide` is privileged: give it to a reviewer's key, never to the agent's.

### Who can decide an approval

- A key can never decide an approval it requested.
- A person can never decide an approval that their own agent requested: an app signed in as them, or a key they created. Not from that app, not from another app, not with another key. A teammate decides it. The one exception: when nobody else in the environment can decide approvals, the person decides it themselves, directly in the console or in Slack.
- An approval that needs N approvers counts each OAuth decision as one **user** and each key decision as one **credential**.

The workspace tools (`list_workspaces`, `select_workspace`) work only with OAuth. A key belongs to one environment and cannot switch.

## Act for one of your users

When your product's agent acts in **your users'** accounts (their Gmail, their HubSpot), your server names the user and the model never does. Connect with an API key and add one header:

```text
Authorization: Bearer anlyon_live_...
Anlyon-End-User: user_123
```

- The user is fixed for the whole connection. No tool has a user argument, so nothing the model writes can choose or switch it.
- Each call uses that user's own connected account, counts toward that user's [limits](/execution/impact-limits), and waits for that user's approval when a policy is **decided by the end user**.
- The id is 1 to 128 characters: letters, digits and `. _ : @ -`.
- An OAuth connection that sends the header is refused with `400 END_USER_HEADER_NOT_ALLOWED`. OAuth connections act for you, with workspace connections.
- A key created with **Require an end user** refuses a connection without the header (`400 END_USER_REQUIRED`).

With the npm package over stdio, pass `--end-user <id>` or set `ANLYON_END_USER` instead. See [Workspace connections and end-user connections](/execution/end-user-connections) for the model behind this, and [Act for your users](/guides/act-for-your-users) for a full app.

## Tools

| Tool | What it does | Consent group |
| --- | --- | --- |
| `action_<name>` | One tool per enabled action, with the action's own input schema and description. `refund-customer` becomes `action_refund-customer` | Actions |
| `list_actions` | Lists the environment's actions, their input schemas, and which need approval | Actions |
| `invoke_action` | Invokes an action by name: `{ "name": "...", "input": { } }` | Actions |
| `request_approval` | Asks a person to decide something that is not an action. It does not release a gated action | Actions |
| `check_approval` | Reads an approval's status once: `pending`, `approved`, `denied` or `expired` | Actions |
| `wait_for_approval` | Waits for the decision inside the turn. `timeoutSeconds` default 60, max 120. Returns the still-`pending` approval when time runs out | Actions |
| `list_pending_approvals` | Lists approvals waiting for a decision (`limit` default 20, max 50) | Actions and Approve on your behalf |
| `decide_approval` | Approves or denies with a `reason`, recorded as the decision note. Approving a gated action runs it | Approve on your behalf |
| `list_workspaces` | Lists your workspaces and environments, and which one this connection uses. OAuth only | None |
| `select_workspace` | Moves this connection to another workspace and environment. OAuth only | Switch between your workspaces |

How the `action_<name>` tools work:

- An action defined in the console appears in the tool list within 30 seconds, with no restart. The list is cached for 30 seconds per caller.
- A gated action says so in its description: "Gated. It requires human approval, and the call parks until a human decides."
- An action that declares an `impact` says what one call counts, for example "Each call counts emails toward any shared limit set for this environment".
- A plain `GET` action with no adapter is marked read-only. Every other action is marked destructive, so a client that confirms destructive calls asks you first.
- A key without `actions:read` gets the fixed tools only.

## Call an action and handle an approval

1. The agent calls an action tool:

   ```json
   { "name": "action_update-ticket-priority", "arguments": { "ticketId": "T-1042", "priority": "high" } }
   ```

2. If the action needs approval, nothing is sent yet. The tool answers:

   ```text
   Awaiting human approval. Call wait_for_approval with approvalId=apr_7Hc2... to block until they decide. The action runs by itself once approved.
   ```

   (`invoke_action` returns the same as JSON: `"pendingApproval": true` and `"approvalId": "apr_..."`.)

3. The agent calls `wait_for_approval` with `{ "approvalId": "apr_7Hc2..." }`. A reviewer decides in the console, in Slack, or with `decide_approval` from their own connection ([who can decide](#who-can-decide-an-approval)). For an approval **decided by the end user**, that user decides in your app.
4. `wait_for_approval` returns the approval record:

   ```json
   {
     "id": "apr_7Hc2...",
     "kind": "action",
     "status": "approved",
     "payload": { "invocationId": "inv_...", "actionName": "update-ticket-priority", "method": "PATCH" },
     "decidedBy": { "type": "user", "id": "...", "displayName": "Dana" },
     "requiredApprovals": 1,
     "approvedCount": 1
   }
   ```

   If it is still `pending`, the agent calls it again. If it is `denied` or `expired`, the request is never sent.

5. Approving runs the exact request that was reviewed. The invocation `payload.invocationId` names holds the outcome, the response and, for a governed action, the receipt grade. Read it in the console's invocation log, or with `GET /api/v2/actions/invocations/{id}` (needs `actions:read`).

An action that does not need approval answers at once with its invocation: `status` (`succeeded`, `failed` or `unknown`), `responseStatus`, `body`, and for a governed action `effectId` and `grade`. `unknown` means the request may have reached the destination: do not call it again. See [When the outcome is unknown](/execution/when-the-outcome-is-unknown).

## One integration's MCP server

Each integration has its own server at `https://api.anlyon.com/mcp/<integration>`. Use it when an agent needs one integration and should not see the rest of your workspace.

| Integration | MCP server |
| --- | --- |
| Asana | `https://api.anlyon.com/mcp/asana` |
| Attio | `https://api.anlyon.com/mcp/attio` |
| Discord | `https://api.anlyon.com/mcp/discord` |
| GitHub | `https://api.anlyon.com/mcp/github` |
| Intercom | `https://api.anlyon.com/mcp/intercom` |
| Notion | `https://api.anlyon.com/mcp/notion` |
| PagerDuty | `https://api.anlyon.com/mcp/pagerduty` |
| Resend | `https://api.anlyon.com/mcp/resend` |
| Sentry | `https://api.anlyon.com/mcp/sentry` |
| Stripe | `https://api.anlyon.com/mcp/stripe` |

It serves:

- **The integration's actions**, as `action_<name>` tools. These are the actions installed from that integration's toolset, whose names start with `<integration>-`, for example `action_github-create-issue`. Connect the account and turn on its operations in the console first.
- **`check_approval` and `wait_for_approval`**, so the agent can follow a gated action to its decision.

Everything else stays on `https://api.anlyon.com/mcp`: `list_actions`, `invoke_action`, other integrations' actions, deciding approvals and switching workspaces. An action you created yourself, or installed without the prefix, is served there too.

Sign-in, API keys, `Anlyon-End-User`, approvals and limits work exactly as on `/mcp`. An OAuth token issued for `/mcp` is accepted here too. An address that is not an integration answers `404 NOT_FOUND`.

## Run the npm package

`@anlyonhq/mcp-server` (Node 18 or later) serves the same tools from your own machine or host. It talks to the Anlyon API with an API key.

### Over stdio

Your MCP client starts the process:

```bash
ANLYON_API_KEY=anlyon_live_... npx -y @anlyonhq/mcp-server
```

To act for one of your users, fix the user for the process:

```bash
ANLYON_API_KEY=anlyon_live_... npx -y @anlyonhq/mcp-server --end-user user_123
```

A malformed id stops the server at startup.

### Over HTTP

Run the MCP server over HTTP yourself, for a client that needs a URL but cannot sign in with OAuth:

```bash
PORT=3001 npx -y @anlyonhq/mcp-server --http
```

It serves `POST /mcp` (streamable HTTP, stateless) on `PORT`, default `3001`. `GET /mcp` returns the server's name and version. It holds no key of its own: each request's `Authorization: Bearer <key>` is the key the tools act with, and `Anlyon-End-User` on a request names the user. `--end-user` and `ANLYON_END_USER` are refused in this mode. It answers `401` without a bearer, `400 INVALID_END_USER` for a malformed id, `415` without a JSON content type, and `413` for a body over 1 MB.

The hosted `https://api.anlyon.com/mcp` also accepts an API key, so run your own only when you need it on your network.

### Environment variables

| Variable | Meaning |
| --- | --- |
| `ANLYON_API_KEY` | The API key, for stdio. Required |
| `ANLYON_END_USER` | The end user every call acts for, for stdio. Same as `--end-user` |
| `ANLYON_BASE_URL` | The API origin. Default `https://api.anlyon.com` |
| `ANLYON_MCP_DYNAMIC_ACTIONS` | `0` turns off the `action_<name>` tools, for a client that dislikes a tool list that changes |
| `MCP_TRANSPORT` | `http` is the same as `--http` |
| `PORT` | The HTTP port. Default `3001` |

## Fix a refused connection or call

| What you see | Why | What to do |
| --- | --- | --- |
| `401` with `invalid_token` | The OAuth token expired or the app was disconnected, or the API key is unknown, revoked or expired | Sign in again from your client. For a key, check it in **Settings → API keys**. A client that receives this `401` may open the OAuth sign-in, even when you meant to use a key |
| `400 END_USER_REQUIRED` | The key was created with **Require an end user** and the request had no `Anlyon-End-User` | Send the header, or use `--end-user` |
| `400 END_USER_HEADER_NOT_ALLOWED` | `Anlyon-End-User` was sent with an OAuth sign-in | Use an API key to act for a user |
| `400 INVALID_END_USER` | The id is empty, longer than 128 characters, or has other characters | Use letters, digits and `. _ : @ -` |
| `503` | Anlyon could not check the credential | Retry shortly |
| `Error: API key is missing required scope: actions:invoke` | The key lacks the scope that tool needs. With OAuth: `This authorization is missing required scope: ...` | Add the scope to the key, or reconnect and grant the group |
| `Unknown tool: ...` | The tool is not served to this connection: its consent group was not granted, or it is not on this integration's server | Grant the group, or use `https://api.anlyon.com/mcp` |
| `"status": "connect_required"` | The user has not connected the integration the action needs. Nothing was sent | Send the user to `connectUrl` (below) |
| `"status": "reconnect_required"` | The user's connection was revoked or stopped refreshing | Send the user to `connectUrl` to connect again |
| `ANLYON_API_KEY is not set` | The stdio server started without a key | Set it in the client's `env` block |

`connect_required` and `reconnect_required` come back as a tool result, not an error, so the model can pass the message on:

```json
{
  "status": "connect_required",
  "integration": "gmail",
  "connectUrl": "https://api.anlyon.com/connect/...",
  "message": "The user has not connected gmail yet, so nothing was done. Ask them to connect it, then try again."
}
```

The same fields are in the result's `structuredContent`. `connectUrl` works once, for 10 minutes. It is `null` when the environment has no default redirect URL: create a link with `endUsers.createConnectLink()` instead.

## Related

- [Actions](/execution/actions): define what the agent can call
- [Approvals](/trust-control/approvals): policies, reviewers and N-of-M
- [Impact limits](/execution/impact-limits): cap what calls can do per environment or per user
- [Receipts and grades](/execution/outcomes): what `confirmed`, `acknowledged` and `unknown` mean
- [Workspace connections and end-user connections](/execution/end-user-connections)
- [Vercel AI SDK](/sdks/ai-sdk): the same tools in a TypeScript app, without MCP
