Skip to main content
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

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:
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:
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.
  • 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.

Lazy context providers

Use inferred accessors for application dependencies while keeping tools standalone:
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 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:
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:
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.