MCP servers
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.
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, Cursor, Codex, Windsurf, ChatGPT, the OpenAI Responses API or the 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 |
| 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:
- Sign in to Anlyon, or create an account. A new account gets a workspace during sign-in.
- 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.
- 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:
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:
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, 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 for the model behind this, and 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
impactsays what one call counts, for example “Each call counts emails toward any shared limit set for this environment”. - A plain
GETaction 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:readgets the fixed tools only.
Call an action and handle an approval
-
The agent calls an action tool:
{ "name": "action_update-ticket-priority", "arguments": { "ticketId": "T-1042", "priority": "high" } } -
If the action needs approval, nothing is sent yet. The tool answers:
Awaiting human approval. Call wait_for_approval with approvalId=apr_7Hc2... to block until they decide. The action runs by itself once approved.(
invoke_actionreturns the same as JSON:"pendingApproval": trueand"approvalId": "apr_...".) -
The agent calls
wait_for_approvalwith{ "approvalId": "apr_7Hc2..." }. A reviewer decides in the console, in Slack, or withdecide_approvalfrom their own connection (who can decide). For an approval decided by the end user, that user decides in your app. -
wait_for_approvalreturns the approval record:{ "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 isdeniedorexpired, the request is never sent. -
Approving runs the exact request that was reviewed. The invocation
payload.invocationIdnames holds the outcome, the response and, for a governed action, the receipt grade. Read it in the console’s invocation log, or withGET /api/v2/actions/invocations/{id}(needsactions: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.
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 exampleaction_github-create-issue. Connect the account and turn on its operations in the console first. check_approvalandwait_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:
ANLYON_API_KEY=anlyon_live_... npx -y @anlyonhq/mcp-server
To act for one of your users, fix the user for the process:
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:
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:
{
"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: define what the agent can call
- Approvals: policies, reviewers and N-of-M
- Impact limits: cap what calls can do per environment or per user
- Receipts and grades: what
confirmed,acknowledgedandunknownmean - Workspace connections and end-user connections
- Vercel AI SDK: the same tools in a TypeScript app, without MCP

