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

toolwire

v0.1.4

Published

Framework-agnostic tool registry for LLM agents. Define tools with Zod schemas, validate arguments, export to OpenAI, Anthropic, Gemini, or Vercel AI.

Readme

toolwire

Framework-agnostic tool registry for LLM agents.

Define tools once with Zod schemas. Get input validation, structured error messages the LLM can act on, and one-line schema export to OpenAI, Anthropic, Gemini, or Vercel AI — with zero runtime dependencies.

npm install toolwire zod

The problem

Every team building an LLM agent writes the same three pieces of boilerplate:

  1. A JSON schema for each tool
  2. Validation of the LLM's arguments before calling the function
  3. An error message the LLM can understand and retry

And they do it differently every time, for every framework, in every project. toolwire is the standard.


Quick start

import { tool, registry } from 'toolwire';
import { z } from 'zod';

// 1. Define a tool
const searchWeb = tool({
  name: 'search_web',
  description: 'Search the web for current information',
  input: z.object({
    query: z.string().min(1).describe('The search query'),
    maxResults: z.number().int().min(1).max(20).default(5),
  }),
  handler: async ({ query, maxResults }) => {
    return await mySearchAPI(query, maxResults);
  },
  timeout: 10_000,
  retries: 2,
});

// 2. Create a registry
const reg = registry([searchWeb, readFile, writeFile]);

// 3. Use with any LLM provider
const openaiTools = reg.toOpenAI();
const anthropicTools = reg.toAnthropic();

// 4. Execute a tool call — always resolves, never throws
const result = await reg.call(llmToolCall);

if (result.success) {
  // result.data is the validated return value
  messages.push({ role: 'tool', content: JSON.stringify(result.data) });
} else {
  // result.error.llmMessage is pre-formatted for the LLM to retry
  messages.push({ role: 'tool', content: result.error.llmMessage });
}

Features

  • Type-safe — Zod schemas infer TypeScript types end-to-end
  • Input + output validation — validate arguments in, validate results out
  • LLM-readable errors — every failure includes a llmMessage ready to append to messages
  • Four provider adapters — OpenAI, Anthropic, Gemini, Vercel AI SDK
  • Middleware — hook into beforeCall / afterCall / onError for logging, auth, caching
  • Hot-swap — replace a tool at runtime without restarting the agent
  • Timeout + retries — configurable per-tool, with exponential backoff
  • Runtime discovery — load tools from a directory or remote manifest
  • Zero runtime dependencies — only Zod (peer dep) required

API

tool(config)

Define a tool. Returns a frozen ToolDefinition with pre-computed JSON Schema.

const myTool = tool({
  name: 'my_tool',         // 1–64 chars: letters, digits, _ or -
  description: string,     // shown to the LLM — explain when to use this tool
  input: ZodSchema,        // validates LLM arguments
  output?: ZodSchema,      // optional — validates handler return value
  handler: async (input, context) => { ... },
  timeout?: number,        // ms, default 30_000
  retries?: number,        // additional attempts on execution failure, default 0
  annotations?: {          // informational hints (not enforced)
    readOnly?: boolean,
    destructive?: boolean,
    expensive?: boolean,
    requiresConfirmation?: boolean,
  },
});

The context object passed to the handler:

interface ToolContext {
  signal: AbortSignal; // tied to the timeout — honour this for cooperative cancellation
  attempt: number;     // 0 = first try, 1 = first retry, …
}

registry(tools, options?)

Create a registry from an array of tool definitions.

const reg = registry([searchWeb, readFile, writeFile], {
  defaultTimeout: 15_000, // fallback timeout for tools that don't set their own
});

reg.call(request)

Execute a tool call from an LLM. Always resolves — never throws.

const result = await reg.call({
  name: 'search_web',
  arguments: { query: 'TypeScript tips', maxResults: 5 },
});

// ToolResult is a discriminated union
if (result.success) {
  console.log(result.data);       // validated return value
  console.log(result.durationMs); // wall time in ms
} else {
  console.log(result.error.code);       // error category
  console.log(result.error.message);    // developer-readable message
  console.log(result.error.llmMessage); // ready to send back to the LLM
  console.log(result.error.retryable);  // should the LLM retry?
}

Error codes:

| Code | When | Retryable | |------|------|-----------| | NOT_FOUND | Tool name not registered | ✓ | | DISABLED | Tool is currently disabled | ✗ | | VALIDATION_INPUT | Arguments fail Zod schema | ✓ | | VALIDATION_OUTPUT | Return value fails output schema | ✗ | | TIMEOUT | Handler exceeded timeout | ✓ | | EXECUTION | Handler threw (all retries exhausted) | ✗ |


Provider adapters

Export tool schemas in whatever format your LLM provider expects. All adapters exclude disabled tools.

// OpenAI function-calling
await openai.chat.completions.create({
  model: 'gpt-4o',
  tools: reg.toOpenAI(),
  // or with strict mode:
  tools: reg.toOpenAI({ strict: true }),
  messages,
});

// Anthropic tool-use
await anthropic.messages.create({
  model: 'claude-opus-4-6',
  tools: reg.toAnthropic(),  // uses input_schema key
  messages,
});

// Google Gemini
await model.generateContent({
  tools: [reg.toGemini()],   // wraps in functionDeclarations
  contents,
});

// Vercel AI SDK
const { text } = await generateText({
  model: openai('gpt-4o'),
  tools: reg.toVercelAI(),   // passes Zod schemas directly
  prompt,
});

Standalone adapter functions are also exported for use outside a registry:

import { toOpenAI, toAnthropic, toGemini, toVercelAI } from 'toolwire';

const schemas = toOpenAI([searchWeb, readFile]);

Middleware

Add hooks for logging, authentication, caching, or tracing.

reg.use({
  name: 'logger', // optional — used in error messages

  // Runs before execution, in registration order
  // Return a value to transform the arguments, or void to keep them
  beforeCall: (toolName, args) => {
    console.log(`→ ${toolName}`, args);
  },

  // Runs after success, in reverse registration order
  // Return a ToolSuccess to transform the result, or void to keep it
  afterCall: (toolName, args, result) => {
    console.log(`← ${toolName} (${result.durationMs}ms)`);
    tracer.record(toolName, result.data);
  },

  // Runs on any failure
  // Return a ToolResult to recover from the error, or void to propagate it
  onError: (toolName, args, failure) => {
    alerting.send(toolName, failure.error);
    // return a ToolResult here to recover, or return nothing to propagate
  },
});

Multiple middleware are chained — beforeCall runs in order, afterCall in reverse (stack-style):

reg
  .use({ name: 'auth', beforeCall: checkAuth })
  .use({ name: 'cache', beforeCall: checkCache, afterCall: writeCache })
  .use({ name: 'metrics', afterCall: recordMetrics });

Hot-swapping tools

Replace a registered tool in-place without restarting the agent:

// Start with the live implementation
const reg = registry([searchWeb]);

// Mid-run: swap to a cached version
reg.swap('search_web', cachedSearchWeb);

// Disable a tool temporarily (returns DISABLED error if called)
reg.disable('send_email');
reg.enable('send_email');

// Add new tools at any time
reg.register(newTool);

reg.describe()

Generate a human-readable tool list for injecting into a system prompt:

const systemPrompt = `You have access to the following tools:\n${reg.describe()}`;
// → "- search_web: Search the web for current information"
//   "- calculate: Evaluate a mathematical expression"

ToolRegistry.fromDir(path)

Load tools from a directory of compiled JavaScript files:

const reg = await ToolRegistry.fromDir('./tools/');

Each file may export:

// Option A: default export
export default tool({ name: 'my_tool', ... });

// Option B: named `tools` array
export const tools = [tool({ ... }), tool({ ... })];

// Option C: any named export that is a ToolDefinition
export const myTool = tool({ ... });

Only .js, .mjs, and .cjs files are scanned. Files that fail to import are skipped with a warning.


ToolRegistry.fromManifest(url)

Load tools from a remote JSON manifest and proxy calls over HTTP:

const reg = await ToolRegistry.fromManifest('https://tools.mycompany.com/manifest.json');

Manifest format:

{
  "version": "1.0",
  "tools": [
    {
      "name": "search_web",
      "description": "Search the web",
      "inputSchema": { "type": "object", "properties": { ... } },
      "endpoint": "https://api.mycompany.com/tools/search"
    }
  ]
}

TypeScript

tool() infers input and output types from your Zod schemas — no explicit generics needed:

const greet = tool({
  name: 'greet',
  description: 'Greet someone',
  input: z.object({ name: z.string() }),
  output: z.object({ message: z.string() }),
  handler: async ({ name }) => ({ message: `Hello, ${name}!` }),
  //                ^^^^ typed as { name: string }
  //                                              ^^^^ typed as { message: string }
});

Use the inference helpers to extract types from definitions:

import type { InferInput, InferOutput } from 'toolwire';

type GreetInput = InferInput<typeof greet>;   // { name: string }
type GreetOutput = InferOutput<typeof greet>; // { message: string }

Provider format reference

| Field | OpenAI | Anthropic | Gemini | Vercel AI | |-------|--------|-----------|--------|-----------| | Schema key | parameters | input_schema | parametersJsonSchema | Zod schema | | Wrapper | { type: "function", function: {...} } | direct object | { functionDeclarations: [...] } | Record by name | | Strict mode | strict?: boolean | — | — | — |


Zod v3 support

Zod v4 (the default) has built-in JSON Schema generation. For Zod v3 support, install the optional peer:

npm install zod-to-json-schema

toolwire detects the Zod version automatically and uses the right conversion path.


Complete agent loop example

import Anthropic from '@anthropic-ai/sdk';
import { registry, tool } from 'toolwire';
import { z } from 'zod';

const searchTool = tool({
  name: 'search_web',
  description: 'Search the web for current information',
  input: z.object({ query: z.string().min(1) }),
  handler: async ({ query }) => ({ results: await mySearch(query) }),
});

const reg = registry([searchTool]).use({
  beforeCall: (name, args) => console.log(`→ ${name}`, args),
  afterCall: (name, _, r) => console.log(`← ${name} ${r.durationMs}ms`),
});

const client = new Anthropic();
const messages: Anthropic.MessageParam[] = [
  { role: 'user', content: 'What are the latest TypeScript features?' },
];

while (true) {
  const response = await client.messages.create({
    model: 'claude-opus-4-6',
    max_tokens: 1024,
    tools: reg.toAnthropic(),
    messages,
  });

  messages.push({ role: 'assistant', content: response.content });

  if (response.stop_reason === 'end_turn') break;

  // Process tool calls
  const toolResults: Anthropic.ToolResultBlockParam[] = [];
  for (const block of response.content) {
    if (block.type !== 'tool_use') continue;

    const result = await reg.call({ name: block.name, arguments: block.input });

    toolResults.push({
      type: 'tool_result',
      tool_use_id: block.id,
      content: result.success
        ? JSON.stringify(result.data)
        : result.error.llmMessage,
      is_error: !result.success,
    });
  }

  messages.push({ role: 'user', content: toolResults });
}

License

MIT