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

# Tool authoring

> The complete tool authoring contract, from schemas to results and diagnostics.

Define a tool once, then compose it into a server over stdio or HTTP. This guide covers schemas, handler context, results, and errors.

## Authoring API

```ts theme={null}
import { createServer, tool } from "@toolgrain/mcp";
import { stdio } from "@toolgrain/node";
import { z } from "zod";

const greet = tool({
  title: "Greeting",
  description: "Greet someone by name",
  annotations: { readOnlyHint: true, openWorldHint: false },
  input: z.object({ name: z.string().min(1) }),
  output: z.object({ message: z.string() }),
  execute: ({ name }) => ({ message: `Hello, ${name}!` }),
});

const server = createServer({
  name: "hello-server",
  version: "1.0.0",
  tools: { greet },
});

await server.listen(stdio());
```

Input and output schemas can use Zod 4 Classic or Zod Mini, including mixed
definitions. Handler inputs still infer the parsed type, and declared outputs
check the handler's return type before output parsing. For example, Mini users
can define the same tool with its functional schema API:

```ts theme={null}
import * as mini from "zod/v4/mini";

const greet = tool({
  input: mini.object({ name: mini.string().check(mini.minLength(1)) }),
  output: mini.object({ message: mini.string() }),
  execute: ({ name }) => ({ message: `Hello, ${name}!` }),
});
```

Toolgrain consumes user schemas through `zod/v4/core`. Its internal wire and
metadata validators continue using Classic.

The MCP authoring API supports TypeScript 5.5.4 or later with `strict` enabled.
Supported Zod versions follow the `^4.6.6` peer range.

MCP packages JavaScript source maps, declaration maps, and their original
TypeScript sources for debugging and editor navigation. Public imports still
resolve to compiled files in `dist`. For mapped Node stack traces, start your
application with `node --enable-source-maps app.js`.

Registration keys supply exposed names: 1–128 ASCII letters, digits, underscores,
hyphens, or dots. Names are case-sensitive. Definitions are reusable across
servers, and registrations must be created with `tool()`; raw objects and modified
object spreads are rejected. Pass modified definitions through `tool()` again to
check their schema/handler types. Definitions and `mcpResult()` values from a
duplicate installed copy of a compatible `@toolgrain/mcp` version are recognized.

Variants retain the original input and output schema types:

```ts theme={null}
const friendlyGreet = tool({ ...greet, title: "Friendly greeting" });
```

`tool()` accepts `title`, `description`, `annotations`, `icons`, and `_meta`
directly. Tool annotations are descriptive hints, not access controls. Metadata
must be JSON-compatible and is validated and copied when the server is created.
Readonly icon arrays, including nested `sizes`, are accepted.

Handlers receive asynchronously parsed input, including defaults and transforms.
An optional `output` schema constrains the handler return type and asynchronously
parses successful returned data before serialization, applying output defaults,
transforms, and refinements. Input JSON Schema describes the accepted input;
output JSON Schema describes the parsed output. Unrepresentable schemas fail at
server construction with `ToolSchemaError`. An output-side `.transform()` is not
representable by itself; pipe it into a schema describing the parsed value, such
as `z.string().transform(Number).pipe(z.number())`. Optional output properties
that parse to `undefined` are omitted. Without `output`, handlers must return a
string, JSON value, or `mcpResult()` whose structured content is JSON-compatible.
Toolgrain checks the value at runtime but does not parse or transform it before
result conversion; `undefined` inside that data is rejected rather than dropped.

`execute` on a definition is the **raw handler**. Calling it directly bypasses
input validation, output validation, and MCP result conversion. Invoke tools
through the server's transport or `server.handle()` for the complete behavior.
The registry is an internal module; there is no separate core package.

### Handler context

Handlers receive a second argument, `ctx` (type `ToolContext`). Handlers that
only take `input` remain valid.

```ts theme={null}
const search = tool({
  input: z.object({ query: z.string() }),
  async execute({ query }, ctx) {
    const response = await fetch(
      `https://example.com/search?q=${encodeURIComponent(query)}`,
      {
        signal: ctx.signal,
      },
    );
    return response.json();
  },
});
```

* `ctx.signal` is an `AbortSignal`. It aborts when a stdio client sends
  `notifications/cancelled`, when an HTTP caller's signal aborts (for example on
  disconnect), when the transport closes (including stdin EOF), or when the
  server closes. Its reason is an `AbortError`: "Request cancelled by client",
  "Connection closed" for stdio requests, or "Server closed" for `handle()`
  calls. When an HTTP caller's signal aborts, its own reason is used.
  A response is never sent after the signal aborts, so whatever the handler
  returns or throws afterwards is discarded.
* `ctx.request` holds `id`, `protocolVersion`, an optional `client` with the
  `info` and `capabilities` the client reported, and `meta`, a copy of the
  request's `_meta`. Each request gets its own deeply frozen copies. Client info
  and capabilities are recursively readonly in the handler types, including
  nested arrays. Their types cover both supported protocol eras; fields that
  differ between eras need guards, and checking `protocolVersion` alone does
  not narrow them. Legacy details retain their reported values without
  normalization. Modern requests carry client details on each request; legacy
  stdio uses those from `initialize`; legacy HTTP has none, so `client` is
  absent. Client details are self-reported and unverified, so don't base security
  decisions on them.
* If the request is cancelled while its input is still being validated, the
  handler is not started.
* Cancellation is cooperative. A handler that ignores `ctx.signal` keeps running
  until it finishes; its response is still discarded, and on stdio its request
  ID stays in use until then.

Calling `execute` directly requires a context. Build one with `toolContext()`,
optionally passing a `signal` or `request` fields. `toolContext()` is the
supported way to create a context: future versions may add required fields that
it fills in, so contexts written out by hand can stop compiling.

```ts theme={null}
import { toolContext } from "@toolgrain/mcp";

await search.execute({ query: "mcp" }, toolContext());
```

### Lazy context providers

Use inferred accessors for application dependencies while keeping tools standalone:

```ts theme={null}
import { createContext } from "@toolgrain/mcp";

const database = createContext({
  name: "database",
  resolve: () => sharedDatabase, // Your application-owned client or pool.
});
const useDatabase = database.use;

const services = createContext({
  name: "services",
  async resolve(ctx) {
    const db = await useDatabase(ctx);
    return { findUser: (id: string) => db.findUser(id, ctx.signal) };
  },
});
const useServices = services.use;
```

Register `context: [services, database]` on `createServer()`, in any order.
Handlers call `await useServices(ctx)`. Only accessed factories run; each tool
call caches its provider promises and values. Use
`database.provide(() => testDatabase)` for a typed server/test override, and
`toolContext({ context: [...] })` for direct calls. Context values retain their
original identity and applications own resource cleanup. See the
[context guide](https://toolgrain.mintlify.site/guides/context.md) for a full example, cancellation, and errors.

### Rich results

Return ordinary strings or JSON values for automatic conversion. Use
`mcpResult()` for explicit MCP content, structured data, result metadata, or
`isError`:

```ts theme={null}
import { mcpResult, tool } from "@toolgrain/mcp";
import { z } from "zod";

const report = tool({
  input: z.object({}),
  output: z.object({ uri: z.string() }),
  execute: () => mcpResult({
    content: [{
      type: "resource_link",
      uri: "https://example.com/report",
      name: "Report",
    }],
    structuredContent: { uri: "https://example.com/report" },
  }),
});
```

The helper supports text, image, audio, resource links, and embedded text/blob
resources. Content accepts readonly arrays and nested protocol fields, including
`as const` values. Content and data are validated and copied during conversion.
Optional result and content fields set to `undefined` are treated as absent. A JSON text
block accompanies structured data unless an identical text block is already
present. When an output schema changes the data, a text block containing the
original JSON is updated to the parsed JSON instead of being duplicated. Ordinary objects containing `content` or `isError` remain application
data; Toolgrain does not guess that they are protocol results.

When `output` is declared, an explicit successful result must supply
`structuredContent`; TypeScript checks its presence as well as its type. Use
`McpStructuredResult<T>` when explicitly annotating a helper that returns such
a result. Any supplied structured data is parsed against the schema,
including data in error results (`isError: true`). Errors may omit structured data
and report text in `content`; output defaults do not create structured data for
these errors. Without an output schema, structured data may be any valid JSON.
Invalid outputs become tool errors without structured data. Modern results can
contain any JSON value as structured content. Legacy structured content and
advertised output schemas are limited to objects.
Other values remain available as JSON text in legacy results. Legacy listings
retain titles, annotations, and `_meta`, and omit icons.

### Errors and diagnostics

Return `mcpResult({ isError: true, content: [...] })` for an error intended for
the client. Valid explicit errors preserve their content, structured data, and
metadata under the existing output-validation rules. Input validation failures
also return public feedback identifying the rejected fields.

Unexpected exceptions from input parsing, handlers, or context providers, along
with invalid outputs, return `isError: true` with the generic text
`Tool execution failed`, without structured data. Exception messages, stacks,
and causes remain private. These failures reach the server's optional
`onError(error, details)` callback once, with the original thrown value:

```ts theme={null}
const server = createServer({
  name: "projects",
  version: "1.0.0",
  tools: { listProjects },
  onError(error, details) {
    console.error("Unexpected server failure", error, details);
  },
});
```

For unexpected tool failures, `details` is a frozen `ToolErrorDetails` containing
the exposed `toolName` and the context's frozen `request`: ID, protocol revision,
optional client information, and request metadata. Toolgrain adds no arguments,
verified identity, or bearer token to these details. Request metadata and client
information retain the client-supplied values; keep diagnostics private.

Unexpected auth verification failures, including provider
`AuthError("temporarily_unavailable")` outages, also reach `onError` once with
the original thrown value and no `details`. Expected auth rejections
(`invalid_request`, `invalid_token`, and `access_denied`) do not. Auth responses
keep their existing public error codes and optional provider-supplied descriptions.
Transport and other internal request failures omit `details`. Existing callbacks
taking only `error` remain supported. Callback throws and asynchronous rejections
cannot replace the response or interrupt cleanup; diagnostics are not awaited.

Explicit tool errors, ordinary input validation failures, and protocol errors do
not reach `onError`. Failures after cancellation remain discarded without
diagnostics. Direct definition `execute()` and provider accessor calls keep their
raw return and rejection behavior; this boundary applies to server execution.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.