# Tools

A tool is a typed action the model can call: hit an API, run a query,
write a file. Each file in `bot/tools/` defines one tool, and the
filename becomes the tool name the model sees. Tools come in two
execution flavors: server tools run on the serving host, and
agent tools run as scripts where the agent runs. Every server tool can
also be called directly, with no model turn.

## Define a server tool

By default, `execute` runs in-process on the serving host with full
access to `process.env` and your `bot/lib/` code.

```ts
// bot/tools/inspect_pr.ts
import { defineTool } from "@grok/agent-sdk/tools";
import { z } from "zod";

export default defineTool({
  description: "Inspect a pull request before approval.",
  // execution: "server" is the default
  inputSchema: z.object({ prUrl: z.string().url() }),
  async execute({ prUrl }, ctx) {
    return { prUrl, checks: ["unit", "lint"], ready: true };
  },
});
```

A tool definition needs a filename slug (the model-facing name), a
`description` written for the model, an optional `inputSchema`, and the
code that runs it. With a Zod `inputSchema`, the input is validated before
`execute` runs and the input type is inferred. A plain JSON Schema
object is forwarded as-is and the input arrives as raw JSON.

A server tool can call `decide` from
[`@grok/agent-sdk/extensions/jev`](/docs/guides/jev.md) for a risk tier or a
finding gate without a second model turn.

For multi-line descriptions, reminder prompts, and error messages, use
[`prompt`](/docs/reference/prompt.md) so the string can sit indented with the surrounding
TypeScript:

```ts
import { prompt } from "@grok/agent-sdk";
import { defineTool } from "@grok/agent-sdk/tools";
import { z } from "zod";

export default defineTool({
  description: prompt`
    Inspect a pull request before approval.
    Prefer this over guessing from the title alone.
  `,
  inputSchema: z.object({ prUrl: z.string().url() }),
  async execute({ prUrl }) {
    return { prUrl };
  },
});
```

### Tool context

`execute` receives a `ctx` with the runtime accessors:

| Member | What it is |
| --- | --- |
| `ctx.toolCallId` | The call id, matching the `actions.requested` / `action.result` stream events |
| `ctx.session` | Read-only session info: id, channel, mode, auth |
| `ctx.approvedBy` | For a `needsApproval` tool, the auth of whoever approved this call, in the same shape as `ctx.session.auth`. On Slack, `attributes.user_id` is the person who clicked Approve, who can differ from the person who started the thread. Unset when the call needed no approval |
| `ctx.workspaceDir` | The session's workspace directory |
| `ctx.stateRoot` | The agent's durable state root, shared across every session |
| `ctx.host` | Shared host services (see below) |
| `ctx.send(channelId, message, options?)` | Start or resume a session on any channel. `auth` defaults to this call's session principal |
| `ctx.getSession(channelId, sessionId)` | Look up a session on a channel |
| `ctx.artifacts` | Session-bound [artifacts](/docs/reference/artifacts.md) facade: `tag` auto-fills the session |

`ctx.host` carries the shared host services: `host.mcp` (authored MCP
connections: `names()`, `listTools(name)`, `callTool(name, tool, args)`),
`host.github` and `host.slack` (shared platform clients), `host.kv` and
`host.files` (durable storage), and `host.reminders` (per-session wakes,
when attached). Custom metrics use the
[OpenTelemetry](/docs/guides/opentelemetry.md) API.

### Return values

`execute` may return a string (passed to the model verbatim), a
JSON-shaped value (serialized), or the envelope
`{ content: [...], isError? }` for rich results. Returns must be
JSON-shaped: use object literals or `type` aliases. An `interface` type
fails assignability (no index signature), and tsx won't catch it. Your
typecheck will.

Throwing inside `execute` reports an error result to the model
(`isError: true`). The turn and the server keep running.

### Declare structured output

Set `outputSchema` when MCP clients need a structured result contract. Use a
Zod schema or plain JSON Schema:

```ts
const Weather = z.object({
  city: z.string(),
  temperatureF: z.number(),
});

export default defineTool({
  description: "Read the current weather.",
  inputSchema: z.object({ city: z.string() }),
  outputSchema: Weather,
  async execute({ city }) {
    return { city, temperatureF: 72 };
  },
});
```

Agent SDK advertises the schema. It does not validate tool results against
it. Plain object returns fill MCP `structuredContent` automatically. An
envelope return must set `structuredContent` next to `content` and `isError`.

Use the exported `ToolInput<TSchema>` type when another function needs the
input type inferred from a tool schema.

### Control dry-run effects

Set `effect` to `"read"`, `"write"`, `"idempotent"`, or a predicate over
validated input. A dry-run session executes reads and answers writes and
idempotent calls without running their tool bodies. An authored tool
without `effect` is treated as a write. `"idempotent"` may change
something outside the session, and repeating the same arguments has no
additional effect. It is not a read.

```ts
export default defineTool({
  description: "Publish a release.",
  inputSchema: z.object({ version: z.string() }),
  effect: "write",
  dryRunResult: ({ version }) => ({ published: false, version }),
  async execute({ version }) {
    return publishRelease(version);
  },
});
```

`dryRunResult` keeps the result shape stable. Without it, a stubbed write
returns `"Operation acknowledged."`. Traces mark the result as stubbed.

### Retry a transient read or idempotent call

When a read or an idempotent call fails because the service or the network
was briefly unavailable, Agent SDK runs that same call again before the model
sees the error. A write, and a tool that does not declare an effect, runs
once. An advertised MCP tool is retried when its annotations classify as
a read, or as idempotent. `idempotentHint: true` with `destructiveHint`
explicitly true or explicitly false is idempotent. The two hints are
orthogonal: a destructive call can still be safe to repeat. A bare
`idempotentHint`, a destructive hint without `idempotentHint`, and
`destructiveHint: false` without `idempotentHint` are not eligible.
`idempotent` is not a read and is not a non-destructive claim. Declaring
`effect: "read"` or `effect: "idempotent"` opts the call into the default
policy of 3 attempts.

If the call still fails, the tool error is the last attempt's message,
unchanged.

### Bound time arguments under `asOf`

Declare `timeArgs` with the input properties that carry time bounds. In a
session created with `asOf`, every named argument must be an absolute
ISO-8601 instant with a timezone, no later than the session's frozen
instant. A call that omits one, or supplies a relative or later value, is
refused with an error telling the model to retry with absolute bounds.
Sessions without `asOf` ignore the declaration.

```ts
export default defineTool({
  description: "Query telemetry between two instants.",
  inputSchema: z.object({ since: z.string(), until: z.string() }),
  effect: "read",
  timeArgs: ["since", "until"],
  async execute({ since, until }) {
    return queryTelemetry(since, until);
  },
});
```

Every name in `timeArgs` must exist in the input schema; a session with
`asOf` refuses to start otherwise, because a misspelled name would silently
check nothing.

## Define an agent tool

Set `execution: "agent"` and the tool materializes as a shell script
that runs in the Cursor agent workspace. The script receives JSON
arguments on stdin and prints its
result on stdout. Use this flavor when the tool must run next to the
checkout the agent works in.

```ts
import { defineTool } from "@grok/agent-sdk/tools";
import { z } from "zod";

export default defineTool({
  description: "Echo a message from the agent workspace.",
  execution: "agent",
  inputSchema: z.object({ message: z.string() }),
  script: `#!/usr/bin/env bash
set -euo pipefail
message=$(python3 -c 'import json,sys; print(json.load(sys.stdin)["message"])')
printf '%s\\n' "$message"
`,
});
```

Scripts land in the session workspace with a catalog in `AGENTS.md`.

## Tools from an MCP connection, advertised by name

Set `advertiseTools: true` on a connection to expose its MCP tools by
name. Add per-session `auth` when credentials depend on the caller.
Model calls and [direct tool calls](#call-a-tool-without-a-model-turn)
use the same exposed names. See
[MCP connections](/docs/reference/connections.md#advertise-tools) for the full
contract.

## Call a tool without a model turn

Server tools can be called deterministically. You pick the tool and the
input. Validation and execution match a model-initiated call, and no
Cursor API key is needed.

Over HTTP, with the same auth chain as the session API:

```bash
curl -X POST http://127.0.0.1:3000/<slug>/v1/tools/inspect_pr \
  -H 'content-type: application/json' \
  -d '{"input":{"prUrl":"https://github.com/acme/checkout/pull/42"}}'
# {"ok":true,"toolName":"inspect_pr","callId":"tool_inspect_pr_…",
#  "isError":false,"result":{…},"durationMs":12}
```

From the CLI, which starts an ephemeral server unless `--url` targets
a running one:

```text
agent-sdk tool call inspect_pr --dir . \
  --input '{"prUrl":"https://github.com/acme/checkout/pull/42"}'
agent-sdk tool call inspect_pr \
  --url http://127.0.0.1:3000/<slug> \
  --input '{"prUrl":"https://github.com/acme/checkout/pull/42"}'
```

Programmatically, `callTool(toolName, input, options?)` is available on
the serve handle, on channel route handlers and `onStart` args, and on
schedule `run` handlers, so a channel can mix deterministic calls with
model turns:

```ts
const outcome = await handle.callTool("inspect_pr", {
  prUrl: "https://github.com/acme/checkout/pull/42",
});
if (outcome.isError) {
  return outcome;
}
// { toolName, callId, isError, result, durationMs }
```

Omit both identifiers to run against an ephemeral workspace that is
removed when the call returns. Pass `sessionId` (HTTP body, `--session`,
or `options.sessionId`) or `continuationToken` (`<channelId>:<key>` as
`/v1/sessions` lists it) to bind the call. The two are mutually
exclusive.

| Rule | What happens |
| --- | --- |
| Existing `sessionId`, or a token that maps to an existing session | Session workspace, owner check, and event recording |
| Token with no matching session | Scratch workspace; the token's channel id and key are the call identity |
| Busy session, `effect: "read"` (or an advertised MCP tool marked read-only) | Runs alongside the turn, reading the workspace as the turn left it; recorded under its own `turnId` |
| Busy session, write, idempotent, or undeclared | `409 session_busy` until the turn finishes |
| Session workspace cannot be created, read | Scratch workspace; the outcome has `scratchWorkspace: true` |
| Session workspace cannot be created, write | `500 workspace_unavailable` |

| Error | When |
| --- | --- |
| `404 unknown_tool` | Name is not a callable server tool; the response lists available names |
| `400 tool_not_callable` | Agent-execution tools cannot run on the host |
| `400 invalid_tool_input` | Zod rejected the input before `execute`. Plain JSON Schema is forwarded unvalidated |
| `400 invalid_continuation_token` | Malformed token, or passed together with `sessionId` |
| `404 session_not_found` | `sessionId` does not exist |
| `409 session_busy` | Write-effect call while a model turn is running |
| `500 workspace_unavailable` | Write-effect call when the session workspace cannot be created |
| `isError: true` | The tool body threw; same envelope the model would see |

## Design habits

Keep one decision per tool. Small tools with crisp descriptions beat
multi-purpose tools with mode flags. The model chooses better and evals
gate cleaner.

Put deterministic policy in tool code, not model judgment. A PR-approval
tool should re-read the live PR inside the tool before acting, so a
spoofed payload can't steer it.

Test tools with `tool call` before blaming prompts. If the tool's output is
wrong, no instruction change fixes it.

Declare each tool's `effect` so dry-run sessions can execute reads and
stub writes.

## Related

- [MCP connections](/docs/reference/connections.md): tools that come from MCP servers
  instead
- [Extensions](/docs/reference/extensions.md): tools installed as a package under a
  namespace
- [Evals](/docs/evals.md): gating tool decisions with `calledTool`
