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:
- Validates the
ToolOptions(auth level, timeout, roles, required name + input). - Builds input + output JSON schemas via
zod-to-json-schema(see Schemas). - Registers a
ToolDefin the owningPlatoolsinstance’s registry. - 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 nameplatools.tool: auth must be one of admin, none, user, got ...- unknown auth levelplatools.tool: timeoutMs must be positive, got N- zero or negative timeoutplatools.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_ticketsin Python andlist_ticketsin TypeScript both register as the same MCP-visible name. If you want camelCase handlers but snake_case tool names, setnameexplicitly. - 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 againstoutputZodSchemabefore 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.