> ## Documentation Index
> Fetch the complete documentation index at: https://docs.duraton.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Providers and MCP

> Bring your own model provider, attach an external MCP server as a tool source, list tools before they run, and expose the same tools over MCP to other agents.

## Bring your own provider

`provider` takes an `AIProvider` instance as well as a name, which is how an agent runs against an
adapter you wrote - or against a deterministic stand-in in tests, with no API key:

```ts theme={null}
await agent(ctx, "agent", {
  model: "demo-1",
  prompt: "Resolve the ticket",
  tools: [searchPolicy],
  maxIterations: 4,
  provider: myProvider,
});
```

See [Providers](/reference/sdk/ai-steps#providers) for the `AIProvider` contract, including how a tool
declaration reaches the model and how tool calls come back.

## Attaching an external MCP server

`mcpServers` gives an agent tools you did not write. The kit connects to the server, reads its
tool list, and hands those tools to the model alongside your own - each call running as an ordinary
durable step.

```ts theme={null}
const result = await agent(ctx, "support", {
  model: "claude-opus-4-8",
  prompt: `Resolve this ticket: ${ctx.event.data.ticket}`,
  tools: [searchPolicy],
  mcpServers: [
    {
      name: "orders",
      transport: { kind: "http", url: process.env.ORDERS_MCP_URL },
      tools: ["lookup-order"],
      requiresApproval: (tool) => tool.name === "cancel-order",
    },
  ],
  maxIterations: 6,
});
```

<ResponseField name="name" type="string" required>
  Namespaces this server's tools. "orders" turns the remote "lookup-order" into "orders\_\_lookup-order", so two servers offering the same tool stay distinguishable.
</ResponseField>

<ResponseField name="transport" type="McpTransport" required>
  How to reach the server. \{ kind: "http", url } speaks Streamable HTTP.
</ResponseField>

<ResponseField name="headers" type="Record<string, string> | (() => Promise<Record<string, string>>)">
  Sent with every request. The function form runs inside the durable step, which is where credentials.resolve() is legal.
</ResponseField>

<ResponseField name="tools" type="string[]">
  Remote tool names to attach, unprefixed. Omitted, the agent gets everything the server offers.
</ResponseField>

<ResponseField name="requiresApproval" type="(tool: McpTool) => boolean | ApprovalRule | undefined">
  Which of this server's tools park on a human first. A predicate, because you did not author these tools and cannot annotate them one by one. Return a rule to say how the gate is raised as well as whether; return nothing for a tool and it falls to the agent's own approval default.
</ResponseField>

The predicate answers per tool, and what it returns places that tool in the
[same two levels](/agent-kit/approvals#which-rule-applies-to-a-tool) a declared tool goes through:

| It returns                      | The gate                                                                                                                     |
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| an `ApprovalRule`               | raised, under that rule. It sets the rule and leaves `requiresApproval` unset, so an explicit `false` stays the only opt-out |
| `true`                          | raised, under the agent's `approval` default when there is one                                                               |
| `false`                         | none, whatever the default says                                                                                              |
| nothing, or no predicate at all | the agent's `approval` default                                                                                               |

The last row is why the default exists. These are the tools you did not write, attached here and
discovered at runtime, so there is no declaration to annotate and no list to enumerate ahead of time.
Silence about a tool you did not write is not a decision that it is harmless.

Install the MCP SDK alongside the kit - it is an optional peer, so an agent that attaches no server
never pulls it:

```bash theme={null}
bun add @modelcontextprotocol/sdk
```

### What it records

| Step                | Written when                | Holds                            |
| ------------------- | --------------------------- | -------------------------------- |
| Tool discovery      | once, before the first turn | the tool list the server offered |
| One per remote call | each remote call            | the call's result, memoized      |

Discovery is a durable step because the tool list decides what the model was offered. A server that
adds or drops a tool mid-run cannot change what a replay sees, and a fully replayed run opens no
connection at all.

<Note>
  Name the tools you want. An allow-list is what stops a server you do not control from widening your
  agent's reach by adding a tool - and asking for one the server does not offer fails the run rather
  than quietly attaching fewer tools than you asked for.
</Note>

### Credentials, approval and bad arguments

A remote tool is still a tool, so everything the kit already does applies to it:

* **Credentials** resolve inside the step, so a stored credential works:

  ```ts theme={null}
  headers: async () => {
    const orders = await client.credentials.resolve("orders");
    return { Authorization: `Bearer ${orders.token}` };
  },
  ```

* **Approval** parks the run before the call, and a denial comes back to the model as that tool's
  result. A `destructiveHint` in the server's own annotations never gates on its own - hints inform
  a default, `requiresApproval` is the boundary.

* **Guardrails** validate the model's arguments against the *server's* `inputSchema`, so a schema
  you did not write still stops a malformed call reaching a third party.

A remote tool that answers with MCP's `isError` **fails its step** rather than returning the error
text to the model. The step then retries under your workflow's policy and the run records why;
memoizing a broken call as a successful result would leave no replay able to get past it.

## Listing tools before they run

By default the engine has no record of a tool until a run calls it. `toolManifest()` projects
your `tools` and `mcpServers` arrays into `workflow`'s advisory `tools` field, so a tool -
its name, description, and parameter schema - is listable before it has ever executed:

```ts theme={null}
import { agent, tool, toolManifest } from "@duraton/agent-kit";

const tools = [searchPolicy, issueRefund];
const mcpServers = [{ name: "orders", transport: { kind: "http", url: process.env.ORDERS_MCP_URL } }];

export default workflow({
  name: "support.ticket",
  tools: toolManifest({ tools, mcpServers }),
  handler: (ctx) =>
    agent(ctx, "support", { model: "claude-opus-4-8", prompt: "Resolve the ticket", tools, mcpServers }),
});
```

One array, two readers - `toolManifest()` reads the same `tools`/`mcpServers` you pass to `agent()`,
so the manifest can never silently disagree with what the model actually sees. An attached MCP
server appears as an unexpanded group (its name only) rather than a tool list: its tools are
discovered at run time, not registration time, so listing them here would mean contacting the
server before a run even starts - reintroducing the replay drift durable discovery exists to
prevent. Full field reference and the advisory guarantees are in [Tool
manifest](/reference/sdk/defining-workflows#tool-manifest-optional).

## Exposing the same tools over MCP

A tool is one definition, and the kit emits it in whichever dialect a surface needs. `agent()` uses
the function-calling dialect; `createMcpEmitter()` produces the same tools as
[Model Context Protocol](https://modelcontextprotocol.io) definitions, so the tools your agent uses
are the tools an MCP client sees - one source, not two lists that drift.

```ts theme={null}
import { createMcpEmitter } from "@duraton/agent-kit";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";

const tools = [searchPolicy, lookupOrder];

server.setRequestHandler(ListToolsRequestSchema, () => ({
  tools: createMcpEmitter().emit(tools),
}));
```

<Note>
  The emitter produces the definitions a server advertises - the name, description, input schema, and
  the `outputSchema` and `annotations` when a tool declares them. What a call then does is the
  server's decision, and stays yours to write.
</Note>

`requiresApproval` is never emitted. Approval is Duraton parking *your* run on *your* reviewer; it
is not something a client on the other end of an MCP connection can honour, and advertising it
would read as a promise the protocol cannot keep.

Use the low-level `tools/list` handler as above rather than `registerTool`: `registerTool` takes a
Zod schema for its input, while a kit tool declares a JSON Schema, which is what `tools/list`
carries on the wire.

## Extension points

Two seams, each a closed set of adapters. A new one is a new adapter, not a change to the existing
ones.

| Port            | Decides                                            | Adapters                  |
| --------------- | -------------------------------------------------- | ------------------------- |
| `AgentStrategy` | how a turn is composed                             | `function-calling`        |
| `ToolEmitter`   | which dialect a tool is emitted in                 | `function-calling`, `mcp` |
| `Guardrail`     | what a tool call is checked against before it runs | `schema`                  |
| `McpTransport`  | how an attached MCP server is reached              | `http`                    |
