# Tools

A tool is a typed action the model can call, such as hitting an API, running a query, or writing a file. Each file in `bot/tools/` defines one tool, and the filename is the name the model sees. Server tools run on the serving host, and agent tools run as scripts next to the agent.

## 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. `execution: "server"`
is the default.

A tool 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 schema, Agent SDK validates the input and infers its type. With plain JSON Schema, `execute` gets raw JSON.

`prompt` from `@grok/agent-sdk` or `@grok/agent-sdk/prompt` dedents a
template literal so a multi-line description, reminder, or error can
sit indented in TypeScript. Blank lines and relative indentation stay.
`prompt.lines` returns the same text as `string[]`, one entry per line.

```ts
context: prompt.lines`
  Merged PR detected: ${pr.url} by ${author}.

  Call plan_deploy, then follow its nextStep.
`
```

```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`                         | Auth of whoever approved a `needsApproval` call, same shape as `ctx.session.auth`. On Slack, `attributes.user_id` is the person who clicked Approve. 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](https://cursor.com/docs/agent-sdk/reference/artifacts.md) facade: `tag` auto-fills the session                                                                    |

`ctx.host` has `mcp`, `github`, `slack`, `kv`, `files`, and `reminders`.
Custom metrics use the [OpenTelemetry](https://cursor.com/docs/agent-sdk/guides/opentelemetry.md) API.

### Return values

`execute` may return a string, a JSON-shaped value, or `{ content: [...], isError? }`. Use object literals or `type` aliases, since an `interface` return fails typecheck with no index signature. If `execute` throws, the model gets the error and the turn keeps going.

### 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 and does not validate results against it.
A plain object fills MCP `structuredContent`. An envelope must set
`structuredContent` next to `content` and `isError`.

### Control dry-run effects

Set `effect` to `"read"`, `"write"`, `"idempotent"`, or a predicate over
the validated input. A dry-run session runs reads and stubs writes and
idempotent calls. A tool with no `effect` is a write. `"idempotent"` can
change something outside the session, and repeating the same arguments
has no further effect.

```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);
  },
});
```

Set `dryRunResult` so a stubbed write returns the real shape. Without it, the write returns "Operation acknowledged." Traces mark the result as stubbed.

### Retry a transient read or idempotent call

If a read or an idempotent call fails with a transient error, Agent SDK
tries it up to 3 times before the model sees the error. It runs writes,
and tools that don't declare an effect, only once. Agent SDK retries an
MCP tool when the tool's annotations mark it read-only, or when the tool
sets `idempotentHint: true` together with an explicit `destructiveHint`.
If the call still fails, the tool error is the last attempt's message.

### Bound time arguments under `asOf`

`timeArgs` names the input fields that carry time bounds. In a session
created with `asOf`, each named argument must be an absolute ISO-8601
instant with a timezone, no later than that frozen instant. A missing,
relative, or later value is refused. Sessions without `asOf` ignore it.

```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 check
nothing.

## Define an agent tool

Set `execution: "agent"` when the tool must run next to the checkout.
The script receives JSON on stdin and prints its result on stdout.

```ts
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
python3 -c 'import json,sys; print(json.load(sys.stdin)["message"])'
`,
});
```

Agent SDK copies the scripts into the workspace and lists them in the system prompt.

## 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](https://cursor.com/docs/agent-sdk/reference/tools.md#call-a-tool-without-a-model-turn)
use the same exposed names. See
[MCP connections](https://cursor.com/docs/agent-sdk/reference/connections.md#advertise-tools) for the full
contract.

## Call a tool without a model turn

You can call any server tool yourself, with your own input and no model turn. 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. Without `--url`, this starts an ephemeral server:

```text
agent-sdk tool call inspect_pr --dir . \
  --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",
});
// { 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                              |

See also [MCP connections](https://cursor.com/docs/agent-sdk/reference/connections.md), [Extensions](https://cursor.com/docs/agent-sdk/reference/extensions.md), and [Evals](https://cursor.com/docs/agent-sdk/evals.md).


---

## Sitemap

[Overview of all docs pages](/llms.txt)
