Claude Agent SDK
Give a Claude Agent SDK agent your workspace's actions as MCP tools with an API key, act for one of your users, and have an approver decide gated calls in Anlyon.
An agent built on the Claude Agent SDK (@anthropic-ai/claude-agent-sdk for TypeScript, claude-agent-sdk for Python) runs Claude Code’s tools in your process. Give it a provider key and any of those tools, Bash included, can read it, and a wrong call goes straight out.
Connect it to Anlyon’s MCP server instead. The agent sees your actions as tools. It names an action and passes input; Anlyon adds the credential at dispatch, applies your limits, waits for an approver when a policy says so, and records a receipt. The agent’s process holds an Anlyon API key whose scopes are exactly what it can do, and no provider credential.
This covers calls routed through Anlyon. A tool your own code calls directly is not governed by Anlyon.
The statements about the Claude Agent SDK on this page come from Anthropic’s documentation as checked on 2026-10-27. Check Anthropic’s MCP, permissions and custom tools pages for current behaviour.
Decide where the approval happens
The SDK can pause a tool call for approval: a canUseTool callback on query() receives the tool name and input and returns allow or deny. It runs in your process, and the tool then runs with whatever credential your process holds. Use it when the user in front of the agent is the reviewer.
Use an Anlyon action when the decision belongs to someone else, when you need the decision and the call on record, when a limit must hold across sessions and agents, or when the provider key must stay out of the agent’s process.
Create the keys
The SDK does not open a browser or run an OAuth flow, so connect with an API key. Create two in the console under Settings → API keys:
- Agent key (
ANLYON_AGENT_KEY):actions:read,actions:invoke,approvals:read. This is the key the MCP server acts with. Do not give itapprovals:decide. - Operator key (
ANLYON_OPERATOR_KEY):approvals:decideandactions:read, held by the person or service that reviews. A key can never decide an approval it requested. Addactions:resolveif this key will also record the outcome of anunknowninvocation.
Connect the Anlyon MCP server
Pass Anlyon’s hosted server in mcpServers with type: 'http'. The SDK names each MCP tool mcp__<server>__<tool>, where <server> is the key you give it. Every enabled action is an MCP tool named action_<name>, so an action named refund is mcp__anlyon__action_refund here.
// Needs ANTHROPIC_API_KEY and ANLYON_AGENT_KEY.
import { query } from '@anthropic-ai/claude-agent-sdk';
for await (const message of query({
prompt:
'Refund payment pi_3P9x for order ord_1042, 24000 cents in usd, with action_refund. ' +
'If it is parked for approval, call wait_for_approval with the approvalId and report the decision.',
options: {
mcpServers: {
anlyon: {
type: 'http',
url: 'https://api.anlyon.com/mcp',
headers: { Authorization: `Bearer ${process.env.ANLYON_AGENT_KEY}` },
},
},
// The approval happens in Anlyon, so the SDK does not prompt for these two tools.
allowedTools: ['mcp__anlyon__action_refund', 'mcp__anlyon__wait_for_approval'],
},
})) {
if (message.type === 'system' && message.subtype === 'init') {
const server = message.mcp_servers.find((s) => s.name === 'anlyon');
if (server?.status === 'failed' || server?.status === 'needs-auth') throw new Error('Anlyon MCP server is not connected. Check ANLYON_AGENT_KEY.');
}
if (message.type === 'result' && message.subtype === 'success') console.log(message.result);
}
The same in Python:
import asyncio
import os
from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query
async def main():
options = ClaudeAgentOptions(
mcp_servers={
"anlyon": {
"type": "http",
"url": "https://api.anlyon.com/mcp",
"headers": {"Authorization": f"Bearer {os.environ['ANLYON_AGENT_KEY']}"},
}
},
allowed_tools=["mcp__anlyon__action_refund", "mcp__anlyon__wait_for_approval"],
)
async for message in query(
prompt="Refund payment pi_3P9x for order ord_1042, 24000 cents in usd, with action_refund. "
"If it is parked for approval, call wait_for_approval with the approvalId and report the decision.",
options=options,
):
if isinstance(message, ResultMessage) and message.subtype == "success":
print(message.result)
asyncio.run(main())
List the Anlyon tools by name in allowedTools. An allowed tool runs without a canUseTool prompt, which would otherwise put a second approval, one Anlyon does not record, in front of the one that counts. mcp__anlyon__* allows every Anlyon tool. Leave a tool out if you want the user in front of the agent to confirm it as well.
To run the MCP server in the agent’s own process tree instead, use the npm package over stdio:
const anlyonServer = {
command: 'npx',
args: ['-y', '@anlyonhq/mcp-server'],
env: { ANLYON_API_KEY: process.env.ANLYON_AGENT_KEY! },
};
Act for one of your users
When the agent acts in one of your users’ accounts, your server names the user. Add the Anlyon-End-User header to the server’s headers, from your own session:
// userId comes from your session, never from the model.
const anlyonForUser = {
type: 'http' as const,
url: 'https://api.anlyon.com/mcp',
headers: {
Authorization: `Bearer ${process.env.ANLYON_API_KEY}`,
'Anlyon-End-User': userId,
},
};
With the stdio server, add '--end-user', userId to args instead. Each call then uses that user’s own connected account, counts toward that user’s limits, and waits for that user’s approval when the policy is decided by the end user. Create the key with Require an end user so a connection without the header is refused (400 END_USER_REQUIRED).
When the user has not connected the integration, the tool returns a result, not an error: { "status": "connect_required", "integration": "gmail", "connectUrl": "...", "message": "..." }. Nothing was sent. The agent can pass the message on and your app can show connectUrl as a Connect button. See Act for one of your users.
What happens on a gated call
The action refund in this example uses the stripe.refund adapter with requiresApproval: true. See Governed effects to define it.
- The agent calls
action_refund. Anlyon records the invocation, creates an approval, and returnsAwaiting human approvalwith itsapr_...id. Nothing has been sent to Stripe. - The agent calls
wait_for_approval, which holds the call open (60 seconds by default, 120 at most) until an approver decides. If it comes back stillpending, the agent calls it again. - An approver approves in the Anlyon inbox, in Slack, or with the operator key:
curl -sS -X POST "${ANLYON_BASE_URL:-https://api.anlyon.com}/api/v2/approvals/$APPROVAL_ID/approve" \
-H "Authorization: Bearer $ANLYON_OPERATOR_KEY" \
-H "Content-Type: application/json" \
-d '{"note": "Checked the order; refund is correct."}'
- The approver approves the exact change, and that exact change is what runs. The approval is bound to a digest of the request, target, versions, preconditions and expiry, and Anlyon recomputes it before dispatch and refuses on any difference. Stripe has no compare-and-set, so the refundable amount shown is as of review.
- Anlyon reserves the refund against any impact limit that applies, attaches the Stripe key from the vault, and sends the refund. Every agent, session and key in the environment draws from the same limit. A new session or key does not get a new allowance.
wait_for_approvalreturns the approval record. The agent reads the decision (approved,deniedorexpired) and who made it.payload.invocationIdnames the invocation it gated. A denied or expired approval comes back with that status, and the gated request is never sent.
Read the outcome
The approval says who decided. The invocation says what happened when Anlyon made the request, and for a governed action it carries a receipt grade. Read it with any key that has actions:read:
curl -sS "${ANLYON_BASE_URL:-https://api.anlyon.com}/api/v2/actions/invocations/$INVOCATION_ID" \
-H "Authorization: Bearer $ANLYON_OPERATOR_KEY"
| Grade | Meaning |
|---|---|
confirmed |
A read-back matched the request. |
acknowledged |
The provider accepted the request. Nothing read it back. |
unknown |
No usable response. Anlyon does not send it again. It stays unknown until a read-back or an operator settles it. |
failed |
The provider rejected the request, or it never left Anlyon. |
refused |
Anlyon refused it at dispatch. |
denied |
The approval was denied. |
pending |
No receipt yet. |
For stripe.refund, every success is read from Stripe’s own record of the refund. See Receipts and grades and When the outcome is unknown.
Call Anlyon from a custom tool instead
To control the wait and the text the agent reads, define your own tool with the SDK’s tool() and createSdkMcpServer() and call the Anlyon SDK from its handler:
import { tool, createSdkMcpServer } from '@anthropic-ai/claude-agent-sdk';
import { Client } from '@anlyonhq/sdk';
import { z } from 'zod';
const anlyon = new Client({ apiKey: process.env.ANLYON_AGENT_KEY! });
const refundOrder = tool(
'refund_order',
'Refund a Stripe payment. Amount is in cents. Every refund waits for an approver.',
{ orderId: z.string(), paymentIntent: z.string(), amount: z.number().int() },
async ({ orderId, paymentIntent, amount }) => {
const { data } = await anlyon.actions.invoke(
'refund',
{ paymentIntent, amount, currency: 'usd' },
{ operationKey: `refund:${orderId}` },
);
const text = data!.pendingApproval
? `Waiting for an approver (${data!.approvalId}). Do not retry.`
: `Status ${data!.status}, grade ${data!.grade}.`;
return { content: [{ type: 'text', text }] };
},
);
export const billing = createSdkMcpServer({ name: 'billing', version: '1.0.0', tools: [refundOrder] });
Pass it as mcpServers: { billing } and allow mcp__billing__refund_order. The handler returns as soon as the approval is created. Approving it is what dispatches, so the agent does not have to be running when the approver decides. To act for a user here, build the client with anlyon.forUser(userId); it throws EndUserNotConnectedError with a connectUrl when the user has not connected.
Fix connection errors
| What you see | Why | What to do |
|---|---|---|
The init message shows anlyon as needs-auth or failed |
The key was refused (401), or ANLYON_AGENT_KEY is empty |
Check the key. The run continues without Anlyon’s tools, so stop it as the example does |
| The agent sees the tools but does not call them | The tools are not in allowedTools |
Add mcp__anlyon__<tool> names, or mcp__anlyon__* |
API key is missing required scope: actions:invoke |
The key lacks that scope | Add the scope to the key |
400 END_USER_REQUIRED |
The key requires an end user and no Anlyon-End-User was sent |
Add the header |
400 INVALID_END_USER |
The id has characters outside letters, digits and . _ : @ -, or is over 128 characters |
Use your own user id in that form |
"status": "connect_required" |
The end user has not connected the integration. Nothing was sent | Send the user to connectUrl |
Credentials
The model never sees a provider credential: an action references {{secret:NAME}} or a connection, and Anlyon resolves it at dispatch. A secret is bound to a destination and a placement, checked again at fire time.
The agent’s process still holds the Anlyon agent key and your Anthropic key. The SDK runs Claude Code’s built-in tools, including Bash and Read, unless you remove them with the tools or disallowedTools options. A key in the agent’s environment is reachable by those tools, so keep provider keys in the vault and give the agent key only the scopes above.
More than one approver
To require more than one decision, set an approval policy on the action. N counts distinct credentials for API-key deciders and distinct users for dashboard and OAuth deciders: two keys held by one person are two deciders.
Related
- MCP servers: the full reference
- OpenAI Responses and Agents API: the same pattern with OpenAI
- Vercel AI SDK: Anlyon actions as AI SDK tools, without MCP
- Governed effects: adapters such as
stripe.refund

