---
title: "Claude Agent SDK"
description: "Give a Claude Agent SDK agent your workspace's actions as tools, have an approver decide gated ones in Anlyon, and keep provider credentials out of the agent's process"
icon: "code"
---

The Claude Agent SDK (`@anthropic-ai/claude-agent-sdk` for TypeScript, `claude-agent-sdk` for Python) takes MCP servers as a tool source. Anlyon's MCP server works as one: the agent sees your workspace's actions as tools, calls one, and Anlyon executes it against production systems.

The statements about the Claude Agent SDK on this page come from Anthropic's documentation as checked on 2026-10-18. Check Anthropic's [permissions](https://code.claude.com/docs/en/agent-sdk/permissions), [MCP](https://code.claude.com/docs/en/agent-sdk/mcp) and [custom tools](https://code.claude.com/docs/en/agent-sdk/custom-tools) pages for the current behaviour.

Your agent declares what it wants to do. Anlyon executes it against production systems. The agent names an action and passes input, and Anlyon resolves the credentials at dispatch and makes the request. That is true of actions routed through Anlyon's hosted executor. It is not true of a tool your own code calls directly, which is not routed through Anlyon and is not governed by it.

## When the SDK's own approval is enough

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`. As of 2026-10-18, it fires only for calls that no hook, deny rule, permission mode or allow rule resolved first, and a `PreToolUse` hook runs before every call. Both run in your process, and the tool then runs with whatever credential your process holds.

Use the SDK's callback 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 a record, a shared limit across sessions and agents, or a provider key that stays out of the agent's process.

## Create the agent key

An agent built on the SDK runs without a browser, so start the self-hosted server as a stdio process with an API key. The hosted `https://api.anlyon.com/mcp` accepts OAuth access tokens issued through "Login with Anlyon" only.

Create two keys in the [dashboard](https://console.anlyon.com):

- **Agent key** (`ANLYON_AGENT_KEY`): `actions:read`, `actions:invoke`, `approvals:read`. This is the key the MCP server acts with. Do not give it `approvals:decide`.
- **Operator key** (`ANLYON_OPERATOR_KEY`): `approvals:decide` and `actions:read`, held by the person or service that reviews. A key can never decide an approval it requested itself. Add `actions:resolve` if this key will also record the outcome of an `unknown` invocation.

## Connect the Anlyon MCP server

Pass the server in `mcpServers`. The SDK names each MCP tool `mcp__<server>__<tool>`, where `<server>` is the key you give it. Every enabled action is also an MCP tool named `action_<name>`, so an action named `refund` is `mcp__anlyon__action_refund` here.

```typescript Agent
// 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: {
        command: 'npx',
        args: ['-y', '@anlyonhq/mcp-server'],
        env: { ANLYON_API_KEY: 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'],
    permissionMode: 'default',
  },
})) {
  if (message.type === 'result' && message.subtype === 'success') console.log(message.result);
}
```

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. Leave a tool out of `allowedTools` if you want the user in front of the agent to confirm it as well.

The Python SDK takes the same server as `ClaudeAgentOptions(mcp_servers=..., allowed_tools=[...])`.

## What happens

The action `refund` in this example uses the `stripe.refund` adapter with `requiresApproval: true`. See [Governed effects](/execution/governed-effects) to define it.

1. The agent calls `action_refund`. Anlyon records the invocation, creates an approval, and returns `Awaiting human approval` with its `apr_...` id. Nothing has been sent to Stripe.
2. 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 still `pending`, the agent calls it again.
3. An approver approves in the Anlyon inbox, or with the operator key:

```bash Decide
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."}'
```

4. The human 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.
5. Anlyon reserves the refund against any [impact limit](/execution/impact-limits) 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.
6. `wait_for_approval` returns the approval record. The agent reads the decision (`approved`, `denied` or `expired`) and who made it. `payload.invocationId` names the invocation it gated. A denied or expired approval comes back with that status, and the gated request is never sent.

Approval requests notify over Slack, email, Discord or a webhook. The decision happens in the dashboard or through the API.

## 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`:

```bash Outcome
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](/execution/outcomes).

Anlyon does not retry an action invocation. Where a request may have reached the destination and the outcome cannot be confirmed, the invocation is recorded as `unknown` and you reconcile before retrying. See [When the outcome is unknown](/execution/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:

```typescript Custom tool
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.

## Credentials

The model never sees a credential: an action references `{{secret:NAME}}` and Anlyon resolves it at dispatch. The secret is bound to a destination and a placement, checked again at fire time. This covers credentials in the Anlyon vault, not a key your own process already holds.

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.
