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

armorer

v0.6.1

Published

A lightweight registry for validated AI tools. Build tools with Zod schemas and metadata, register them in an armorer, and execute/query them with event hooks.

Readme

Armorer

A lightweight, type-safe registry for validated AI tools. Build tools with Zod schemas and metadata, register them in an armorer, execute them, and query/rank them with registry helpers and event hooks.

Table of Contents

Overview

Armorer turns tool calling into a structured, observable, and searchable workflow. Define schemas once, validate at runtime, and export tools to popular providers without rewriting adapters.

Features

  • Zod-powered schema validation with TypeScript inference
  • Central tool registry with execution, policy, and event hooks
  • Query + search helpers with scoring and metadata filters
  • Semantic search with vector embeddings (OpenAI, Pinecone, etc.)
  • Provider adapters for OpenAI, Anthropic, and Gemini
  • Tool composition utilities (pipe/compose/bind/when/parallel/retry)
  • MCP server integration for exposing tools over MCP
  • Claude Agent SDK adapter with tool gating
  • Registry middleware for tool configuration transformation
  • Concurrency controls and execution tracing hooks
  • Pre-configured search tool for semantic tool discovery in agentic workflows

Core vs Runtime

Armorer splits tool definitions from execution so you can import only what you need:

  • armorer/core: tool specs, registry/search, ToolError model, serialization, and minimal context types
  • armorer/runtime: execution, policies, createTool/createArmorer, composition utilities (pipe/parallel/retry)
  • armorer/adapters/*: provider formatting (OpenAI/Anthropic/Gemini) built on serialized core definitions
  • armorer/mcp and armorer/claude-agent-sdk: optional integrations (install peer deps when needed)
import { defineTool, createRegistry } from 'armorer/core';
import { createArmorer, createTool } from 'armorer/runtime';

The root import (armorer) still works for now, but new code should prefer the subpaths above.

Installation

# npm
npm install armorer zod

# bun
bun add armorer zod

# pnpm
pnpm add armorer zod

Optional integrations:

npm install @modelcontextprotocol/sdk @anthropic-ai/claude-agent-sdk

Quick Start

import { createArmorer, createTool } from 'armorer/runtime';
import { z } from 'zod';

const addNumbers = createTool({
  name: 'add-numbers',
  description: 'Add two numbers together',
  schema: z.object({
    a: z.number(),
    b: z.number(),
  }),
  tags: ['math', 'calculator'],
  async execute({ a, b }) {
    return a + b;
  },
});

const armorer = createArmorer();
armorer.register(addNumbers);

const toolCall = await armorer.execute({
  id: 'call-123',
  name: 'add-numbers',
  arguments: { a: 5, b: 3 },
});

console.log(toolCall.result); // 8

Safety, Policy, and Metadata

Armorer supports registry-level policy hooks and per-tool policy for centralized guardrails. You can also tag tools as mutating or read-only and enforce those tags at the registry. See the Registry documentation for details on querying, searching, and middleware.

import { createArmorer, createTool } from 'armorer/runtime';
import { z } from 'zod';

const armorer = createArmorer([], {
  readOnly: true,
  policy: {
    beforeExecute({ toolName, metadata }) {
      if (metadata?.mutates) {
        return { allow: false, reason: `${toolName} is mutating` };
      }
    },
  },
  telemetry: true,
});

const writeFile = createTool({
  name: 'fs.write',
  description: 'Write a file',
  schema: z.object({ path: z.string(), content: z.string() }),
  metadata: { mutates: true },
  async execute() {
    return { ok: true };
  },
});

armorer.register(writeFile);

Metadata keys with built-in enforcement:

  • metadata.mutates: true marks a tool as mutating
  • metadata.readOnly: true marks a tool as read-only
  • metadata.dangerous: true marks a tool as dangerous
  • metadata.concurrency: number sets a per-tool concurrency limit

Registry options for enforcement:

  • readOnly: true denies mutating tools automatically
  • allowMutation: false denies mutating tools automatically
  • allowDangerous: false denies dangerous tools automatically

Execution tracing events (opt-in via telemetry: true):

  • tool.started with startedAt
  • tool.finished with status and durationMs

Per-tool concurrency:

createTool({
  name: 'git.status',
  description: 'status',
  metadata: { concurrency: 1 },
  schema: z.object({}),
  async execute() {
    return { ok: true };
  },
});

Creating Tools

Overview

Define tools with Zod schemas, validation, and typed execution contexts. For advanced patterns like chaining tools together, see Tool Composition.

Basic Tool

const greetUser = createTool({
  name: 'greet-user',
  description: 'Greet a user by name',
  schema: z.object({
    name: z.string(),
    formal: z.boolean().optional(),
  }),
  async execute({ name, formal }) {
    return formal ? `Good day, ${name}.` : `Hey ${name}!`;
  },
});

Tools are callable. await tool(params) and await tool.execute(params) are equivalent. If you need a ToolResult object instead of throwing on errors, use tool.execute(toolCall) or tool.executeWith(...).

executeWith(...) lets you supply params plus callId, timeoutMs, and signal in a single call, returning a ToolResult instead of throwing. rawExecute(...) invokes the underlying implementation with a full ToolContext when you need precise control over dispatch/meta or to bypass the ToolCall wrapper.

Tool schemas must be object schemas (z.object(...) or a plain object shape). Tool calls always pass a JSON object for arguments, so wrap primitives inside an object (for example, z.object({ value: z.number() })).

You can use isTool(obj) to check if an object is a tool:

import { isTool, createTool } from 'armorer/runtime';

const tool = createTool({ ... });
if (isTool(tool)) {
  // TypeScript knows tool is ArmorerTool here
  console.log(tool.name);
}

Creating and Registering in One Step

You can create a tool and register it with an armorer in one step by passing the armorer as the second argument:

const armorer = createArmorer([], {
  context: { userId: 'user-123', apiKey: 'secret' },
});

const tool = createTool(
  {
    name: 'my-tool',
    description: 'A tool with armorer context',
    schema: z.object({ input: z.string() }),
    async execute({ input }, context) {
      // context includes armorer.context automatically
      console.log('User:', context.userId);
      return input.toUpperCase();
    },
  },
  armorer, // Automatically registers the tool
);

Tool Without Inputs

If your tool accepts no parameters, omit schema (it defaults to z.object({})):

const healthCheck = createTool({
  name: 'health-check',
  description: 'Verify service is alive',
  async execute() {
    return 'ok';
  },
});

Tool with Metadata

Metadata is a lightweight, out-of-band descriptor for things that should not be part of the tool's input schema. It is useful for discovery and routing (filter/query by tier, cost, capabilities, auth requirements), for UI grouping, or for analytics and policy checks without changing the tool signature.

const fetchWeather = createTool({
  name: 'fetch-weather',
  description: 'Get current weather for a location',
  schema: z.object({
    city: z.string(),
    units: z.enum(['celsius', 'fahrenheit']).optional(),
  }),
  tags: ['weather', 'api', 'external'],
  metadata: {
    requiresAuth: true,
    rateLimit: 100,
    capabilities: ['read'],
  },
  async execute({ city, units = 'celsius' }) {
    // ... fetch weather data
    return { temp: 22, conditions: 'sunny' };
  },
});

Tool with Context

Use withContext to inject shared context into tools:

const createToolWithContext = withContext({ userId: 'user-123', apiKey: 'secret' });

const userTool = createToolWithContext({
  name: 'get-user-data',
  description: 'Fetch user data',
  schema: z.object({}),
  async execute(_params, context) {
    // Access context.userId and context.apiKey
    return { userId: context.userId };
  },
});

Lazy-Loaded Execute Functions

You can supply execute as a promise that resolves to a function. To avoid import() starting immediately, wrap the dynamic import with lazy so it only loads on first execution:

import { lazy } from 'armorer/lazy';

const heavyTool = createTool({
  name: 'heavy-tool',
  description: 'Runs an expensive workflow',
  schema: z.object({ input: z.string() }),
  execute: lazy(() => import('./tools/heavy-tool').then((mod) => mod.execute)),
});

If the promise rejects or resolves to a non-function, tool.execute(toolCall) returns a ToolResult with error set, and tool.execute(params) or calling the tool directly throws an Error with the same message.

Tool Events

Listen to tool execution lifecycle events:

const tool = createTool({
  name: 'my-tool',
  description: 'A tool with events',
  schema: z.object({ input: z.string() }),
  async execute({ input }, { dispatch }) {
    dispatch({ type: 'progress', detail: { percent: 50, message: 'Processing...' } });
    return input.toUpperCase();
  },
});

tool.addEventListener('execute-start', (event) => {
  console.log('Starting:', event.detail.params);
});

tool.addEventListener('execute-success', (event) => {
  console.log('Result:', event.detail.result);
});

tool.addEventListener('execute-error', (event) => {
  console.error('Error:', event.detail.error);
});

tool.addEventListener('progress', (event) => {
  if (event.detail.percent !== undefined) {
    console.log(`${event.detail.percent}%: ${event.detail.message ?? ''}`);
  } else {
    console.log(event.detail.message ?? 'Progress update');
  }
});

Dispatching Progress Events

To report progress from inside a tool, use the dispatch function provided in the ToolContext (second argument to execute). Emit a progress event with an optional percent number (0–100) and an optional message:

const longTask = createTool({
  name: 'long-task',
  description: 'Does work in phases',
  schema: z.object({ input: z.string() }),
  async execute({ input }, { dispatch }) {
    dispatch({ type: 'progress', detail: { percent: 10, message: 'Queued' } });
    // ... do work
    dispatch({ type: 'progress', detail: { percent: 50, message: 'Halfway' } });
    // ... do more work
    dispatch({ type: 'progress', detail: { percent: 100, message: 'Done' } });
    return input.toUpperCase();
  },
});

Then subscribe to progress on the tool:

longTask.addEventListener('progress', (event) => {
  console.log(`${event.detail.percent}%: ${event.detail.message ?? ''}`);
});

Search Tool for Agentic Workflows

Armorer includes a pre-configured search tool that lets agents discover available tools dynamically. This is useful when you have many tools and want the LLM to find the right one for a task.

import { createArmorer, createTool } from 'armorer/runtime';
import { createSearchTool } from 'armorer/tools';
import { z } from 'zod';

const armorer = createArmorer();

// Install the search tool - it auto-registers with the armorer
createSearchTool(armorer);

// Register your tools (can be done before or after the search tool)
createTool(
  {
    name: 'send-email',
    description: 'Send an email to recipients',
    schema: z.object({ to: z.string(), subject: z.string(), body: z.string() }),
    tags: ['communication'],
    async execute({ to, subject, body }) {
      return { sent: true };
    },
  },
  armorer,
);

// Agents can now search for tools via armorer.execute()
const result = await armorer.execute({
  name: 'search-tools',
  arguments: { query: 'contact someone' },
});

console.log(result.result);
// [{ name: 'send-email', description: '...', tags: ['communication'], score: 1.5 }]

The search tool:

  • Auto-registers with the armorer when created
  • Discovers tools dynamically - finds tools registered before or after it
  • Works with provider adapters - included in toOpenAI(armorer), etc.
  • Supports semantic search when embeddings are configured on the armorer

See Search Tool documentation for filtering by tags, configuration options, and agentic workflow examples.

TypeScript

Overview

TypeScript inference guidance and type-level patterns. For a complete list of exported types, see the API Reference.

Armorer is written in TypeScript and provides full type inference:

const tool = createTool({
  name: 'typed-tool',
  description: 'A typed tool',
  schema: z.object({
    count: z.number(),
    name: z.string().optional(),
  }),
  async execute(params) {
    // params is typed as { count: number; name?: string }
    return params.count * 2;
  },
});

// Return type is inferred
const result = await tool({ count: 5 }); // number

Documentation

Longer-form docs live in documentation/:

Migration Guide

See documentation/migration.md for before/after import examples, error model updates, and adapter path changes.

License

MIT. See LICENSE.