npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

standard-tool

v0.1.1

Published

Reference implementation of StandardToolV0 — one type for an LLM tool, portable across providers, SDKs, and frameworks. Zero dependencies, built on Standard Schema.

Downloads

487

Readme

import type { StandardSchemaV1, StandardJSONSchemaV1 } from '@standard-schema/spec';

interface StandardToolV0<
  Input = unknown, Output = unknown, FormattedOutput = Output, Context = unknown,
> {
  name: string;
  title?: string; // human label; shown by MCP-style clients in tool lists
  description: string;
  inputSchema?: StandardSchemaV1<Input, unknown> & StandardJSONSchemaV1<Input, unknown>;
  outputSchema?: StandardSchemaV1<unknown, Output> & StandardJSONSchemaV1<unknown, Output>;
  meta?: Record<string, unknown>; // static data about the tool, for consumers to read
  execute(input: Input, context?: Context): FormattedOutput | Promise<FormattedOutput>;
}

StandardToolV0 is the shape of a self-describing function: a callable together with its name, description, and schemas. Any object of this shape conforms; nothing is required beyond a schema library implementing Standard Schema and Standard JSON Schema (Zod 4.2+, ArkType 2.1.28+, Valibot via @valibot/to-json-schema). The schemas provide static types, runtime validation, and JSON Schema emission via inputSchema['~standard'].jsonSchema.input({ target }). The npm package is a reference implementation.

Status: RFC. The V0 shape is frozen — a breaking change would be StandardToolV1; the reference package follows its own 0.x semver. Critiques and counter-proposals welcome.

Why one type, how existing tool objects compare, and the case against: WHY.md.

Defining a tool

import { z } from 'zod'; // or arktype, or valibot
import type { StandardToolV0 } from 'standard-tool'; // types only — or paste the interface above

const getWeather: StandardToolV0<{ city: string }, { tempC: number }> = {
  name: 'get_weather',
  description: 'Current temperature for a city',
  inputSchema: z.object({ city: z.string() }),
  outputSchema: z.object({ tempC: z.number() }),
  execute: async ({ city }) => ({ tempC: 21 }),
};

Three ways to use it:

Provider field names and dialects: wiring table. The same fields also serve UIs, forms, and CLIs: Beyond LLM tools. Tools can be derived from RPC procedures you already have: tRPC, oRPC.

Per-call context

context is execute's optional second argument — per-call data like a locale, an auth token, a request-scoped handle. Never validated, never in the JSON Schema. Annotate it on the handler (execute: (input, context: { locale: string }) => …) and it types every caller.

Tool-level meta

meta is per-tool data consumers read and execute never sees: { destructive: true, tags: ['fs'] } — confirmation hints, tool selection, ownership. Untyped by design (a generic erases to unknown in StandardToolV0[]); narrow with & { meta: { budget: number } } when you want types. The spec fixes no keys — agree within your system, or follow MCP's tool annotations.

The interface

| field | type | purpose | | --- | --- | --- | | name | string | identifier the model emits | | description | string | what the tool does | | title? | string | human label for MCP-style tool lists; ignored by plain function-calling APIs | | inputSchema? | StandardSchemaV1<Input, unknown> & StandardJSONSchemaV1<Input, unknown> | validates and emits JSON Schema; Input is its input side | | outputSchema? | StandardSchemaV1<unknown, Output> & StandardJSONSchemaV1<unknown, Output> | validates and emits JSON Schema; Output is its output side | | meta? | Record<string, unknown> | static data about the tool; read by consumers, never passed to execute | | execute | (input: Input, context?: Context) => FormattedOutput \| Promise<FormattedOutput> | runs the tool; input untrusted until checked against inputSchema; may throw |

The reference implementation

Copy src/index.ts into your project — ~90 lines — replacing its first import with the types-only @standard-schema/spec. Or install the package:

npm i standard-tool
  • standardTool(def) returns the definition with validation wired into execute: input checked before the handler runs, output after. The handler receives the validated input (the input schema's output side) and returns the raw result the output schema validates. Violations throw StandardToolValidationError, carrying target: 'input' | 'output' and the Standard Schema issues.
  • withFormattedOutput(tool, format?) is the bare-catch recipe written once, with types: a throw inside execute reaches the caller as data.
import { standardTool, withFormattedOutput } from 'standard-tool';
import { z } from 'zod';

const getWeather = standardTool({
  name: 'get_weather',
  description: 'Current temperature for a city',
  inputSchema: z.object({ city: z.string() }),
  outputSchema: z.object({ tempC: z.number() }),
  execute: async ({ city }) => ({ tempC: 21 }),
}); // execute validates input and output

await getWeather.execute({ city: 123 } as never); // throws StandardToolValidationError

await withFormattedOutput(getWeather).execute({ city: 123 } as never);
// { error: 'input validation failed: city: …' }

const asText = withFormattedOutput(getWeather, (r) =>
  r instanceof Error ? `error: ${r.message}` : `${r.tempC}°C`);
await asText.execute({ city: 'Paris' }); // '21°C'

The formatter receives the validated Output or an Error, runs once per call, and its own throws propagate unformatted. It accepts only tools whose execute still returns the plain Output, so wrapping an already-wrapped tool is a compile error. Frameworks with their own formatting hook (toModelOutput in the AI SDK, Mastra) don't need it — hand them the tool unwrapped.

Wiring a provider

Every integration hands the provider the same descriptor — name, description, and the emitted JSON Schema — then runs execute on the model's call. Typed here for Anthropic; the table maps the rest:

import type Anthropic from '@anthropic-ai/sdk';

const descriptor: Anthropic.Tool = {
  name: tool.name,
  description: tool.description,
  input_schema: (tool.inputSchema?.['~standard'].jsonSchema
    .input({ target: 'draft-2020-12' }) ??
    { type: 'object', properties: {} }) as Anthropic.Tool.InputSchema,
};

// execute throws on failure; catch to hand the model something to correct from
let result: unknown;
try { result = await tool.execute(args); }
catch (e) { result = { error: e instanceof Error ? e.message : String(e) }; }

What varies is where the schema goes and which dialect:

| Consumer | schema field | target | result goes back as | | --- | --- | --- | --- | | OpenAI | parameters | draft-2020-12 | function_call_output item | | Anthropic | input_schema | draft-2020-12 | tool_result block | | Gemini | parameters (or parametersJsonSchema) | openapi-3.0 | functionResponse part | | Vercel AI SDK | inputSchema — takes the Standard Schema as-is | — | SDK runs the loop | | MCP | inputSchema in the descriptor | draft-2020-12 | { content, structuredContent?, isError? } — map the result and errors onto it |

Links

License

MIT © Andrey Gubanov