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.
Maintainers
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
- Features
- Core vs Runtime
- Installation
- Quick Start
- Safety, Policy, and Metadata
- Creating Tools
- TypeScript
- Documentation
- Migration Guide
- License
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 typesarmorer/runtime: execution, policies, createTool/createArmorer, composition utilities (pipe/parallel/retry)armorer/adapters/*: provider formatting (OpenAI/Anthropic/Gemini) built on serialized core definitionsarmorer/mcpandarmorer/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 zodOptional integrations:
npm install @modelcontextprotocol/sdk @anthropic-ai/claude-agent-sdkQuick 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); // 8Safety, 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: truemarks a tool as mutatingmetadata.readOnly: truemarks a tool as read-onlymetadata.dangerous: truemarks a tool as dangerousmetadata.concurrency: numbersets a per-tool concurrency limit
Registry options for enforcement:
readOnly: truedenies mutating tools automaticallyallowMutation: falsedenies mutating tools automaticallyallowDangerous: falsedenies dangerous tools automatically
Execution tracing events (opt-in via telemetry: true):
tool.startedwithstartedAttool.finishedwithstatusanddurationMs
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 }); // numberDocumentation
Longer-form docs live in documentation/:
- Armorer Registry - Registration, execution, querying, searching, middleware, and serialization
- Tool Composition -
pipe,compose,bind,tap,when,parallel,retry,preprocess,postprocess - Embeddings & Semantic Search - Vector embeddings with OpenAI and Pinecone
- LanceDB Integration - Serverless vector database for local and cloud deployments
- Chroma Integration - Open-source embedding database with built-in embedding functions
- Search Tools Tool - Pre-configured tool for semantic tool discovery in agentic workflows
- AbortSignal Support - Cancellation and timeout handling
- JSON Schema Output - Export tools as JSON Schema
- Provider Adapters - OpenAI, Anthropic, and Gemini integrations
- MCP Server - Expose tools over Model Context Protocol
- Claude Agent SDK - Integration with
@anthropic-ai/claude-agent-sdkincluding tool gating - Public API Reference - Complete API reference with all exports and types
- Migration Guide - Upgrade notes and import changes for core/runtime split
- Development - Local development workflows
Migration Guide
See documentation/migration.md for before/after import examples, error model updates, and adapter path changes.
License
MIT. See LICENSE.
