---
title: Claude Agent SDK tools
description: The outbound tools as an in-process MCP server for the Claude Agent SDK, with the list of tool names to pre-approve.
sidebar:
  label: Claude Agent SDK
related:
  - /docs/tools
  - /docs/cli/mcp
  - /docs/tools/untrusted-content
  - /guides/agent-tools
---

`outbound-sdk/tools/claude-agent` gives the [tools](/docs/tools) to the Claude Agent SDK as an in-process MCP server. Nothing is spawned and no port is opened: the server lives in your process.

This subpath imports `@anthropic-ai/claude-agent-sdk`, which is an optional peer dependency. Install it only if you import this subpath. See [installation](/docs/installation).

| Export | What it returns |
| --- | --- |
| `claudeAgentTools(outbound, options)` | `server` to pass as `options.mcpServers[serverName]`, `allowedTools` to pass as `options.allowedTools`, and `serverName`. |
| `claudeToolDefinitions(outbound, options)` | The tool definitions on their own, for an MCP server you build yourself with `createSdkMcpServer`. |

Both take the `only` and `extra` options of [`createTools`](/docs/tools#options). `claudeAgentTools` also takes `serverName`, which defaults to `outbound`. The agent addresses each tool as `mcp__<serverName>__<tool>`.

## Build the server and run a tool

```ts title="claude-agent.ts"
import { createOutbound } from "outbound-sdk";
import { demoData, memory } from "outbound-sdk/memory";
import {
  claudeAgentTools,
  claudeToolDefinitions,
} from "outbound-sdk/tools/claude-agent";

const outbound = createOutbound({
  adapter: memory({ data: demoData(), workspaceId: "acme" }),
});

// An in-process MCP server and the names to pre-approve.
const tools = claudeAgentTools(outbound, {
  only: ["campaign_overview", "sender_health"],
});

console.log(tools.serverName, tools.server.type, tools.allowedTools);

// `serverName` changes the prefix the agent addresses the tools by.
console.log(
  claudeAgentTools(outbound, { only: ["reply_samples"], serverName: "sends" })
    .allowedTools
);

// The definitions on their own, to add to an MCP server you build yourself.
const definitions = claudeToolDefinitions(outbound);
const overview = definitions.find((tool) => tool.name === "campaign_overview");

console.log(definitions.length, "definitions;", overview?.annotations);

// What the Agent SDK does when the model calls the tool, done by hand here.
const result = await overview?.handler({ status: "paused" }, undefined);

console.log(JSON.stringify(result, null, 2));

const failed = await overview?.handler({ client: "acme" }, undefined);

console.log(failed?.isError, failed?.content[0]);
```

```text title="Output"
outbound sdk [ 'mcp__outbound__campaign_overview', 'mcp__outbound__sender_health' ]
[ 'mcp__sends__reply_samples' ]
9 definitions; { readOnlyHint: true }
{
  "content": [
    {
      "text": "{\"data\":{\"campaigns\":[{\"campaign\":\"memory:acme:cmp_agencies\",\"channel\":\"email\",\"name\":\"Agency owners\",\"metrics\":{\"bounce_rate\":0.0667,\"bounced\":4,\"connections_accepted\":null,\"connections_sent\":null,\"contacted\":60,\"opened\":18,\"positive\":0,\"positive_reply_rate\":0,\"replied\":1,\"reply_rate\":0.0167,\"reply_rate_basis\":\"contacted\",\"sent\":60},\"status\":\"paused\"}]},\"meta\":{\"evidence\":\"exact\",\"provider\":\"memory\",\"requests\":1,\"today\":\"2026-11-14\"},\"next\":\"Use campaign_trend for one campaign over time, or winning_steps to see which step drives replies.\",\"ok\":true}",
      "type": "text"
    }
  ],
  "isError": false
}
true {
  text: '{"error":{"code":"Invalid","message":"campaign_overview: \\"client\\" is not an input. It takes: from, limit, status, to"},"next":"Check the input and try again.","ok":false}',
  type: 'text'
}
```

- **Every tool is marked `readOnlyHint: true`.** All of them only read, which is why `allowedTools` lists every one: you can pre-approve the whole set.
- **A result is MCP text content.** The tool's result object is serialized as JSON in one `text` item.
- **A failed tool sets `isError: true`.** The content is still the `{ ok: false, error, next }` object, so the model can read why.

## Wiring it to an agent

```ts title="query.ts"
import { query } from "@anthropic-ai/claude-agent-sdk";
import { createOutbound } from "outbound-sdk";
import { instantly } from "outbound-sdk/instantly";
import { claudeAgentTools } from "outbound-sdk/tools/claude-agent";

// Reads INSTANTLY_API_KEY. Constructing it makes no request.
const outbound = createOutbound({ adapter: instantly() });
const tools = claudeAgentTools(outbound);

/** Wiring only: the test suite defines this function and never calls it. */
export const ask = async (prompt: string) => {
  const answers: string[] = [];

  for await (const message of query({
    options: {
      allowedTools: tools.allowedTools,
      mcpServers: { [tools.serverName]: tools.server },
    },
    prompt,
  })) {
    if (message.type === "result" && message.subtype === "success") {
      answers.push(message.result);
    }
  }

  return answers.join("\n");
};
```

> **No model was called**
>
> The test suite loads `query.ts`, type-checks it against the Agent SDK and
> never calls `ask`, so the agent loop is wiring that was not run. The first
> sample calls a tool's `handler` by hand, which is the step the Agent SDK
> performs when Claude asks for the tool.

To serve the same tools to Claude Code, Claude Desktop or another MCP client without writing code, run [`outbound mcp`](/docs/cli/mcp).

## Reply text

A model given `reply_samples` can ask for reply text. It comes back truncated and marked as untrusted. See [untrusted content](/docs/tools/untrusted-content).
