@bernierllc/agent-tool-registry
v0.2.0
Published
Typed in-process tool definition, registration, and permission-scoped execution for LLM agent loops
Readme
@bernierllc/agent-tool-registry
Typed in-process tool definition, registration, and permission-scoped execution for LLM agent loops.
Covers the full tool lifecycle: schema definition, registration, permission check at call time, input validation via @bernierllc/schema-validator, normalized ToolResult output, and before/after execution hooks (for audit logging and changeset recording).
Distinct from @bernierllc/mcp-server-core: this registry is for tools wired directly into a running agent loop. A one-way exportToMCPFormat() adapter converts registered tools into mcp-server-core format when needed.
Installation
npm install @bernierllc/agent-tool-registryQuick Start
import { createToolRegistry } from '@bernierllc/agent-tool-registry';
const registry = createToolRegistry();
// Register a tool
registry.register({
name: 'addService',
description: 'Add a new service offering to the organization catalog',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string' },
price: { type: 'number', minimum: 0 },
durationMinutes: { type: 'number', minimum: 1 },
},
required: ['name', 'price', 'durationMinutes'],
},
permissions: ['admin:services:write'],
async execute(input, context) {
const { name, price } = input as { name: string; price: number; durationMinutes: number };
// Use context.callerId, context.metadata, etc.
return { success: true, message: `Service "${name}" added at $${price}` };
},
});
// Execute (called by ai-agent-runtime when the LLM makes a tool call)
const result = await registry.execute(
'addService',
toolCallId, // opaque ID from the LLM response
{ name: 'Haircut', price: 45, durationMinutes: 30 },
{
conversationId: 'conv-abc',
callerId: 'org-123',
grantedPermissions: ['admin:services:write'],
}
);
// result: { success: true, message: 'Service "Haircut" added at $45' }Execution Pipeline
registry.execute(name, toolCallId, input, context)
↓
Lookup tool definition → ToolNotFoundError if missing
↓
Permission check → ToolPermissionError if caller lacks required permissions
↓
Input validation via schema-validator → ToolInputValidationError if invalid
↓
Fire before-hooks (audit log start, changeset setup)
↓
tool.execute(input, context) → ToolResult
↓
Fire after-hooks (audit log complete, changeset record)
↓
Return ToolResult to callerBefore/After Hooks
registry.addHook({
before: async (record, context) => {
// record: { toolName, toolCallId, input }
// context: { conversationId, callerId, grantedPermissions, metadata }
await auditLogger.start({ action: record.toolName, callerId: context.callerId });
},
after: async (record, context) => {
// record: { toolName, toolCallId, input, state: 'success' | 'error', output: ToolResult }
await auditLogger.complete({ ...record });
await metrics.record({ tool: record.toolName, success: record.output?.success });
},
});Hook errors are caught and logged — they never suppress the tool result or rethrow to the caller. Multiple hooks run in registration order.
Error Classes
All errors extend ToolRegistryError and include Error.cause chaining.
| Class | Code | When thrown |
|---|---|---|
| ToolNotFoundError | TOOL_NOT_FOUND | execute() called with unregistered name |
| ToolPermissionError | PERMISSION_DENIED | Caller lacks a required permission |
| ToolInputValidationError | INVALID_INPUT | Input fails JSON Schema validation |
| ToolExecutionError | EXECUTION_FAILED | tool.execute() threw; original error is cause |
| DuplicateToolNameError | DUPLICATE_TOOL_NAME | register() called with an already-registered name |
import {
ToolNotFoundError,
ToolPermissionError,
ToolInputValidationError,
ToolExecutionError,
DuplicateToolNameError,
ToolRegistryError,
} from '@bernierllc/agent-tool-registry';
try {
await registry.execute('myTool', callId, input, context);
} catch (err) {
if (err instanceof ToolPermissionError) {
console.error('Missing permissions:', err.context?.missingPermissions);
} else if (err instanceof ToolRegistryError) {
console.error(`Registry error [${err.code}]:`, err.message, 'caused by:', err.cause);
}
}MCP Bridge
Export all registered tools into the format expected by @bernierllc/mcp-server-core:
import { exportToMCPFormat } from '@bernierllc/agent-tool-registry';
// After registering all tools:
const mcpTools = exportToMCPFormat(registry);
mcpServer.registerTools(mcpTools);exportToMCPFormat is a one-way snapshot — call it after all tools are registered.
API Reference
createToolRegistry(logger?: Logger): ToolRegistry
Create a new isolated registry. Accepts an optional pre-configured @bernierllc/logger instance; omit for a silent no-op logger.
registry.register(tool: ToolDefinition): void
Register a tool. Throws DuplicateToolNameError if the name is already registered.
registry.get(name: string): ToolDefinition | undefined
Look up a tool by name.
registry.list(): ToolDefinition[]
Return all registered tools.
registry.execute(name, toolCallId, input, context): Promise<ToolResult>
Run the full execution pipeline for a named tool.
registry.addHook(hook: ExecutionHook): void
Install a before/after hook pair. Both before and after are optional.
exportToMCPFormat(registry: IToolRegistry): MCPToolDefinition[]
Convert registered tools to mcp-server-core format.
License
Bernier LLC limited-use license. See LICENSE.
