- Updated
- Reading
- 9 min
- By
- Anlyon Team
How to put a remote MCP server behind Anlyon and govern each tool your agent calls
Register a remote MCP server, adopt the tools your agent may call, require approval on the risky ones, let each of your users sign in with their own account, and see a changed tool refused before it runs.
- mcp
- mcp servers
- approvals
- receipts
- end users
- oauth
- tool changes
Short answer: register the server with Anlyon instead of connecting your agent to it. Anlyon lists the tools and adopts none. A person adopts the ones your agent may call and chooses which wait for approval. Your agent calls them as actions and never holds the key. A tool that changes is refused until someone adopts it again.
TL;DR
anlyon mcp add https://mcp.example.com/mcp --oauth --name trackerregisters the server. Nothing is callable yet.anlyon mcp adopt tracker list_issues delete_issueasks to adopt two tools, and a workspace admin signs it off.delete_issueis suggested as destructive, so each call waits for approval, bound to the exact request and the reviewed tool definition.- Each of your users signs in to the server with their own account through a connect link, and a call made for that user uses that account.
- If the server changes
delete_issue, the next call is not sent and the receipt namestool_changed.
Why put an MCP server behind Anlyon?
An agent connected straight to an MCP server holds the server's credential and can call every tool on it. Three things go wrong:
- A tool nobody chose. The agent needed to list issues. The same server also deletes them.
- A definition that changed. The tool you reviewed last month says something else today, and the next call goes out anyway.
- A credential in every client. Each agent and laptop that connects holds the key, and no call leaves a receipt of its own.
| Agent connected to the server | Server behind Anlyon | |
|---|---|---|
| Who holds the credential | The agent's config or environment | Anlyon |
| What the agent can call | Every tool the server lists | The tools a person adopted |
| A tool that changes | Runs with its new definition | Refused until a person adopts it again |
| A risky call | Runs when the agent decides | Waits for approval bound to the exact request and the reviewed definition |
| What is recorded | Whatever your own logs keep | A receipt for each call |
| Your users' accounts | Your app stores and refreshes each user's token | Each user signs in through a connect link, and Anlyon keeps the token |
That is less risk, and it is less to build: no adapter, pack or gateway of your own, no token table and no approval backend.
Before you start
- An Anlyon workspace, and the CLI signed in:
npx @anlyonhq/cli auth login. Your coding agent can run the commands in this guide. The sign-offs and connecting your own account stay with a person. - The address of a remote MCP server. This guide uses
https://mcp.example.com/mcp, a server that signs people in with OAuth and offerslist_issuesanddelete_issue. A real server's address comes from its vendor's documentation. - For step 4, your app's server, with an Anlyon API key that has
end-users:write.
Step 1: register the server
anlyon mcp add https://mcp.example.com/mcp --oauth --name tracker
--oauth says the server signs people in. Anlyon reads its sign-in metadata and brings its own client where the server accepts one. If you registered a client with the vendor, pass --client-id as well.
Registering adds nothing an agent can call. A server that signs people in still waits for a sign-off, because registering it can create an OAuth client at a third party under your deployment's name. The command prints a link and exits 15, and a workspace admin approves it.
A server that takes a token instead is registered with a header. Store the token in the console's Secrets, with the server's host as its allowed host, and name it as a reference:
anlyon mcp add https://mcp.example.com/mcp --name tracker \
--header "Authorization: Bearer {{secret:TRACKER}}"
The header holds a reference, and the token stays in Anlyon.
Step 2: connect your account and read the tools
If the server lists its tools only to a signed-in account, open the server's page in the console and connect your own account there. That is your consent at the vendor, so it is a person's step. Then read what the server offers:
anlyon mcp tools tracker
TOOL SUGGESTED STATE APPROVAL ACTION
delete_issue destructive not adopted - -
list_issues read not adopted - -
Suggested classes come from what the server says, which is untrusted.
A server can describe a tool that deletes data as harmless. Read each tool yourself before you adopt anything that writes.
Step 3: adopt tools and require approval
anlyon mcp adopt tracker list_issues delete_issue
Needs a human's sign-off. A workspace admin must approve this change before it applies:
https://console.anlyon.com/sign-offs/sor_...
Nothing is adopted yet. A workspace admin opens the link, reads each tool's definition and suggested class, and approves or rejects. No API key, CLI login or MCP session can approve the request, including the one that asked, so your agent cannot widen what it may call. If the listing changes before the decision, approving fails with SIGN_OFF_STALE and nothing is adopted.
By default, a tool suggested as irreversible or destructive waits for approval on every call. Here that is delete_issue, and list_issues runs without asking. To gate every tool you name, pass --approval all. The admin signing off sees which you asked for.
Once approved, each tool is an action named <server>-<tool>: tracker-list_issues and tracker-delete_issue. Over Anlyon's MCP endpoint your agent sees them as action_tracker-list_issues and action_tracker-delete_issue. From the SDKs and the CLI you invoke them like any other action.
A tool whose input schema Anlyon cannot check in full can still be adopted. It is marked loosely checked: its arguments only have to be a JSON object, approval on every call is suggested for it, and an approval shows its arguments in full. It is still pinned, so a change on the server still stops it.
Step 4: let each user sign in with their own account
Your server creates a connect link for the user signed in to your app:
// app/connect/tracker/route.ts
import { Client } from '@anlyonhq/sdk';
import { requireSession } from '@/lib/auth';
const anlyon = new Client({ apiKey: process.env.ANLYON_API_KEY! });
export async function GET() {
const user = await requireSession();
const { data: servers } = await anlyon.mcpUpstreams.list();
const tracker = servers!.find((server) => server.name === 'tracker')!;
const { data: link } = await anlyon.mcpUpstreams.createConnectLink(tracker.id, {
endUserId: user.id,
redirectUrl: 'https://app.example.com/connections/callback',
});
return Response.redirect(link!.url);
}
The user signs in on the server's own page and comes back to your redirectUrl. Your callback confirms the connection for the user in your own session:
// app/connections/callback/route.ts
import { parseConnectRedirect } from '@anlyonhq/sdk';
import { anlyon } from '@/lib/anlyon';
import { requireSession } from '@/lib/auth';
export async function GET(request: Request) {
const user = await requireSession();
const result = parseConnectRedirect(request.url);
if (result?.status === 'pending_confirmation' && result.flowId) {
await anlyon.endUsers.confirmConnection(user.id, result.flowId);
}
return Response.redirect(new URL('/settings/connections', request.url));
}
Until that call, no connection exists, so a forwarded link cannot attach one person's account to another person in your app. The redirect has to be one of the environment's allowed redirect URLs.
Now make the call for that user, with a client bound to them:
const result = await anlyon
.forUser(user.id)
.actions.invoke('tracker-delete_issue', { id: 'ISS-142' });
if (result.data!.pendingApproval) {
console.log('Waiting for approval', result.data!.approvalId);
}
The call uses that user's own account at the server. Anlyon keeps the token and refreshes it, and neither your app nor the agent ever holds it. A user who has not connected is refused before anything is sent. A call made for no user uses the account you connected in step 2.
delete_issue waits for approval in the console's Approvals, or in Slack. Once it is approved, the exact request that was reviewed goes out, with that user's account. To hand the bound client to the Vercel AI SDK or another adapter, see how to let your AI agent act in your users' accounts.
Step 5: see a changed tool refused
Say the server's next release changes delete_issue: a new argument, or a new description. Before every call Anlyon lists the server's tools and compares the tool with the definition you adopted. This time it differs, so the tool is not called and nothing is sent. The receipt is graded failed, because the call never left Anlyon, and its evidence names the reason:
{
"action": { "name": "tracker-delete_issue" },
"outcome": "failed",
"partialEffect": "none",
"evidence": [{
"source": "system",
"summary": "The tool changed since it was adopted, so it was not called.",
"details": { "code": "tool_changed" }
}]
}
List the server again to see the change:
anlyon mcp tools tracker --refresh
TOOL SUGGESTED STATE APPROVAL ACTION
delete_issue destructive changed: re-adopt to call it required tracker-delete_issue
list_issues read adopted no tracker-list_issues
To accept the change, ask to adopt the tool again:
anlyon mcp adopt tracker delete_issue
A workspace admin signs that off with a diff of the definition. The action keeps its approval setting, and an approval pending on the old definition cannot release a call against the new one. A tool the server stopped listing is refused the same way, with tool_missing.
What this does not cover
- Calls that skip Anlyon. A tool your agent calls directly is not routed through Anlyon and is not governed by it. Take the server out of the agent's own config and the token out of its environment. Remove the direct route has the steps for Claude Code and Cursor.
- Local servers. Only remote servers over Streamable HTTP, or the older SSE transport, at an https address. A stdio server cannot go behind Anlyon.
- Read-back. Receipts for adopted tools grade
acknowledged: the server accepted the request, and nothing read it back. They never gradeconfirmed. - A tool that was harmful when you adopted it. The check catches a change. It cannot make a tool safe that was not, so read each tool before you adopt it.
- The moment between check and call. They are two requests in one session, so a change made between them is not caught on that call.
Read next
- Govern a remote MCP server: every command, the SDK calls and the errors you will meet
- Put your coding agent behind Anlyon
- How to let your AI agent act in your users' accounts
- Govern the MCP tools your agents call, the announcement
Add a server in the console at console.anlyon.com/mcp-servers/new, or paste this guide's first command into your coding agent.
Frequently asked questions
- What happens when I register an MCP server with Anlyon?
- Anlyon lists the server's tools and adopts none, so no agent can call anything yet. It suggests a risk class for each tool from what the server says about it. That text is untrusted, and the suggestion is never applied by itself.
- Can my agent adopt an MCP tool by itself?
- It can ask. Adopting a tool adds what an agent can do, so it stays a person's step. The request prints a sign-off link, and no API key, CLI login or MCP session can approve it, including the one that asked.
- What happens when an MCP server changes a tool I adopted?
- Before every call Anlyon lists the server's tools and compares the tool with the definition you adopted. If it differs, the tool is not called and nothing is sent. The receipt names tool_changed, and the tool stays refused until a person adopts it again.
- Can each of my users sign in to the MCP server with their own account?
- Yes, for a server that signs people in with OAuth. Your server creates a connect link for the user, confirms the connection when they come back, and makes calls with a client bound to that user. Each call uses that user's own account.
- Which MCP servers can go behind Anlyon?
- Remote servers that speak MCP's Streamable HTTP transport, or the older SSE transport, at an https address. A server that runs as a local process over stdio cannot.
