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:
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.
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.
McpTransport
required
How to reach the server. { kind: “http”, url } speaks Streamable HTTP.
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.
string[]
Remote tool names to attach, unprefixed. Omitted, the agent gets everything the server offers.
(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.
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:
What it records
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.
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.
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:
-
Approval parks the run before the call, and a denial comes back to the model as that tool’s
result. A
destructiveHintin the server’s own annotations never gates on its own - hints inform a default,requiresApprovalis 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.
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:
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.
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 definitions, so the tools your agent uses
are the tools an MCP client sees - one source, not two lists that drift.
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.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.