Tools

Typed tools, execution context, approvals, and client-side calls.

A tool is anything conforming to AIToolProtocol: a name, a description, JSON Schema parameters, and an async execute. The closure-based Tool covers most cases.

For what the model actually emits and why descriptions decide whether a tool gets used at all, see tool calling.

let search = Tool(
  name: "search",
  description: "Search the product catalog.",
  parameters: Schema.object(["query": .string()])
) { arguments in
  let query = arguments["query"]?.stringValue ?? ""
  return try await catalog.search(query)
}

Typed arguments

Tool.typed decodes arguments into a Decodable before your code runs:

struct SearchArgs: Decodable { let query: String }

let search = Tool.typed(
  name: "search",
  description: "Search the product catalog.",
  parameters: Schema.object(["query": .string()])
) { (args: SearchArgs) in
  try await catalog.search(args.query)
}

Execution context

Tools can see which call they are servicing, the step messages, and per-request context that should never ride in the prompt. This is the AI SDK's tool execution options plus toolsContext:

let orders = Tool(
  name: "list_orders",
  description: "List recent orders for the signed-in user.",
  parameters: Schema.object([:])
) { _, options in
  let userID = options.context?["userID"]?.stringValue ?? "anonymous"
  return try await store.orders(for: userID)
}

let result = try await generateText(
  model: model,
  prompt: "What did I order recently?",
  tools: [orders],
  toolsContext: ["list_orders": ["userID": .string("user-7")]]
)

options.toolCallID and options.messages are there too.

Human-in-the-loop approvals

A tool can require user approval before it runs. The loop pauses with a toolApprovalRequest, your app answers, and execution resumes on the next turn; denials surface to the model as denied results.

let delete = Tool(
  name: "delete_file",
  description: "Delete a file.",
  parameters: Schema.object(["path": .string()]),
  needsApproval: true
) { arguments in
  try await files.delete(arguments["path"]?.stringValue ?? "")
}

Approval can also depend on the arguments: pass needsApproval: { arguments in ... }. In chat UIs, respond with ChatSession.addToolApprovalResponse(approvalID:approved:).

Per-tool needsApproval only expresses "ask a human". For policy that lives on the call — automatic denials with a reason, argument- or history-dependent decisions, or a rule set that differs per tenant — pass toolApproval: to generateText / streamText / Agent, and sign approvals with toolApprovalSecret: so a client cannot forge them. See Timeouts and approvals.

Client-side tools

A tool without an executor ends the turn with the call unexecuted. The model asked; your app answers, whenever it's ready:

let pickPhoto = Tool(
  name: "pick_photo",
  description: "Ask the user to pick a photo.",
  parameters: Schema.object([:])
)

// later, in the chat UI:
chat.addToolResult(toolCallID: tool.toolCallID, result: ["photoID": "IMG_0042"])

Provider-executed tools

Some providers run their own server-side tools — live web and X search, code execution, file search, computer use — and stream the calls and results back in the same turn. Each provider exposes typed builders under <Model>.Tools; drop them in tools: next to your own:

let result = try await generateText(
  model: XaiModel("grok-4.5"),
  prompt: "What shipped in AI this week?",
  tools: [
    XaiModel.Tools.webSearch(allowedDomains: ["arxiv.org"]),
    XaiModel.Tools.xSearch(allowedXHandles: ["xai"]),
  ]
)

These carry no executor: the provider runs them, so the loop never asks your app to and the turn doesn't pause. The calls and results arrive as .toolCall / .toolResult parts with toolCall.providerExecuted == true (web citations also surface as .source), and each provider ignores builders that belong to another — so mixing them is safe. Each provider page lists its catalog: xAI, OpenAI, Google, Anthropic.

For a server-side tool without a typed builder yet, construct one directly with ProviderDefinedTool(provider:id:name:args:)args is the native tool entry the provider expects.

Validation

When parameters is built from the Schema DSL, arguments are validated before execution and malformed calls become error results for the model to correct, instead of crashing your tool.

Repairing tool calls

Pass repairToolCall to generateText / streamText to fix a call the model got wrong (an unknown tool name, mangled arguments) before it runs. Return a corrected ToolCall, or nil to leave it unhandled:

repairToolCall: { call, tools in
  call.name == "web_serch" ? ToolCall(id: call.id, name: "web_search", arguments: call.arguments) : nil
}

Multimodal tool results

A tool can hand the model images (not just text) by setting modelOutput, which maps its output to content parts. Providers that support it (Anthropic today) send them as image blocks; others fall back to the JSON output.

var screenshot = Tool(name: "screenshot", description: "…", parameters: schema) { _ in
  .object(["path": .string("/tmp/shot.png")])
}
screenshot.modelOutput = { _ in [.text("Here's the screen:"), .image(ImageContent(data: pngBytes))] }

Computer use

The provider computer-use tools are client-executed: the model requests an action (click, type, scroll), your executor performs it and returns a screenshot via .image content, and the library wires the round-trip.

  • OpenAI: OpenAIModel.Tools.computerUse(displayWidth:displayHeight:environment:). computer_call actions surface as a computer_use_preview tool call; the screenshot result maps back to computer_call_output.
  • Anthropic: AnthropicModel.Tools.computer(displayWidthPx:displayHeightPx:), bash(...), textEditor(...). The beta header is set automatically.

Name your executor to match the tool (computer_use_preview for OpenAI, the name: you pass on Anthropic), return screenshots as .image content, and the agent loop handles the rest.

MCP tools

MCPClient connects to a Model Context Protocol server and turns its tools into ordinary tools for the loop. Connect, then pass tools() to generateText or streamText:

let mcp = MCPClient(transport: MCPHTTPTransport(
  url: URL(string: "https://mcp.example.com/mcp")!,
  headers: ["Authorization": "Bearer \(token)"]
))
try await mcp.connect()
defer { Task { await mcp.close() } }

let result = try await generateText(
  model: AnthropicModel("claude-sonnet-5"),
  prompt: "What is on my calendar today?",
  tools: try await mcp.tools(),
  stopWhen: [stepCountIs(5)]
)

Bridged tools validate arguments against the server's schemas and run in the same loop as local tools. MCPStdioTransport (local subprocess) and MCPSSETransport (legacy HTTP+SSE) are drop-in transport swaps.

Before trusting a server's tools, fingerprint them and compare on later fetches so a server can't silently change a description or widen a schema after approval:

let baseline = fingerprintTools(try await mcp.tools())
let drift = detectToolDrift(fingerprintTools(try await mcp.tools()), baseline: baseline)
if drift.hasDrift { /* block or force re-approval */ }

See MCP for transports, pagination, and rug-pull detection in full.