Skip to content

platools.tool() factory

Register a typed tool with Zod input/output schemas and get back the handler unchanged. This is the TypeScript analogue of the Python @platools.tool() decorator.

Signature:

platools.tool<Input extends z.ZodTypeAny, Output extends z.ZodTypeAny>(
options: ToolOptions<Input, Output>,
handler: ToolHandler<z.infer<Input>, z.infer<Output>>,
): ToolHandler<z.infer<Input>, z.infer<Output>>;

Both arguments are required. The factory:

  1. Validates the ToolOptions (auth level, timeout, roles, required name + input).
  2. Builds input + output JSON schemas via zod-to-json-schema (see Schemas).
  3. Registers a ToolDef in the owning Platools instance’s registry.
  4. Returns the handler unchanged - decorated tools remain directly callable in user code, exactly like the Python SDK.

ToolOptions

interface ToolOptions<Input extends z.ZodTypeAny, Output extends z.ZodTypeAny = z.ZodNever> {
readonly name: string; // required, unique within the registry
readonly description?: string;
readonly input: Input; // required Zod schema
readonly output?: Output; // optional Zod output schema
readonly auth?: AuthLevel; // "none" | "user" | "admin", default "none"
readonly roles?: readonly string[];
readonly rateLimit?: string; // e.g. "10/min"
readonly timeoutMs?: number; // must be positive
readonly annotations?: Readonly<Record<string, unknown>>;
}

Validation errors thrown at registration time:

  • platools.tool: 'name' is required - empty / whitespace-only name
  • platools.tool: auth must be one of admin, none, user, got ... - unknown auth level
  • platools.tool: timeoutMs must be positive, got N - zero or negative timeout
  • platools.tool: 'input' (Zod schema) is required - missing input schema

Duplicate tool names raise from the underlying ToolRegistry.register().

ToolHandler

type ToolHandler<Input, Output> = (
params: Input,
ctx: ToolContext,
) => Output | Promise<Output>;

Handlers may be sync or async - the transport layer awaits the return value unconditionally. The params argument is fully typed off the Zod input schema, so TypeScript catches shape mismatches at compile time.

ToolContext is intentionally tiny in the SDK ship gate:

interface ToolContext {
readonly callId: string;
}

Phase G orchestration lands richer fields (agentName, userId, traceId). Until then, the transport populates callId from the platform’s tool-call message and leaves the rest as placeholders.

Example: a fully-typed support tool

import { z } from "zod";
import { platools } from "./platools.js";
const TicketStatus = z.enum(["open", "pending", "resolved", "closed"]);
const Ticket = z.object({
id: z.string(),
subject: z.string(),
status: TicketStatus,
priority: z.enum(["low", "medium", "high", "urgent"]),
});
export const listTickets = platools.tool(
{
name: "list_tickets",
description: "Return open support tickets for a customer",
input: z.object({
customerId: z.string().uuid(),
status: TicketStatus.optional(),
limit: z.number().int().min(1).max(100).default(20),
}),
output: z.array(Ticket),
auth: "user",
roles: ["support"],
},
async ({ customerId, status, limit }) => {
return ticketStore.list({ customerId, status, limit });
},
);

Notes on idiomatic usage:

  • Re-use Zod schemas. Pull enums and object shapes into named constants so they show up with readable names in the generated JSON Schema and can be shared with UI code that wants them.
  • Name matches style. The factory does not re-case the tool name - list_tickets in Python and list_tickets in TypeScript both register as the same MCP-visible name. If you want camelCase handlers but snake_case tool names, set name explicitly.
  • Handler returns the raw shape. No need to output.parse(result) yourself. When the platform dispatches a call, the transport validates the handler’s return value against outputZodSchema before it goes over the wire.

Next steps

  • Schemas - how Zod shapes become JSON Schema.
  • Client - WebSocket transport with heartbeat and backoff.
  • Python decorator - the equivalent API in Python.