Alexandro.Net

Download README · GitHub release

@stackline/tool-router

Zero-dependency AI tool discovery and routing for MCP, OpenAI, Anthropic, and Gemini catalogs.

npm version license GitHub repository Docs Reddit community

Documentation | npm | Issues | Repository

Current package version: 1.0.4


Why this package?

Route a user request to the smallest relevant subset of an AI tool catalog. The router is local, deterministic, zero-dependency, and understands MCP, OpenAI, Anthropic, Gemini, and provider-neutral definitions.

Why route tools

Large tool catalogs create three practical problems:

@stackline/tool-router builds an in-memory BM25F-style index over names, namespaces, aliases, tags, descriptions, and JSON Schema text. It adds bounded prefix matching, typo tolerance, uppercase acronym recognition, and a small action-synonym layer. Literal name and namespace matches remain stronger than synonym matches.

The router never calls a model, embedding endpoint, database, or network service. The same catalog and query produce the same ordering.

Compatibility

Item Value
Package @stackline/tool-router@1.0.4
Node.js runtime >=14.17.0
CommonJS / primary entry ./dist/index.cjs
ES module entry ./dist/index.js
Type declarations ./dist/index.d.ts

Detailed runtime and declaration guarantees are in docs/COMPATIBILITY.md.

Installation

npm install @stackline/tool-router

Usage

npm install @stackline/tool-router

Quick start

import { createToolRouter } from '@stackline/tool-router';

const tools = [
  {
    type: 'function',
    name: 'github_create_issue',
    description: 'Create a GitHub issue in a repository.',
    parameters: {
      type: 'object',
      properties: {
        repository: { type: 'string' },
        title: { type: 'string' }
      },
      required: ['repository', 'title'],
      additionalProperties: false
    }
  },
  {
    type: 'function',
    name: 'slack_send_message',
    description: 'Send a message to a Slack channel.',
    parameters: {
      type: 'object',
      properties: {
        channel: { type: 'string' },
        text: { type: 'string' }
      },
      required: ['channel', 'text'],
      additionalProperties: false
    }
  }
];

const router = createToolRouter(tools);
const prompt = 'Open an issue for the checkout regression';
const routed = router.route(prompt, { maxTools: 4 });

// The original OpenAI definitions are returned by reference.
const response = await openai.responses.create({
  model: 'your-model',
  input: prompt,
  tools: routed.tools
});

No provider SDK is required by the package. Route first, then pass routed.tools to the SDK already used by the application.

Search and route

Use search when ranking evidence matters:

const matches = router.search('post the release note in Slack', {
  limit: 5,
  namespaces: ['slack'],
  tags: ['write']
});

for (const match of matches) {
  console.log(match.name, match.score, match.matchedFields);
}

Use select for only the original definitions:

const tools = router.select('find the Q4 plan in Drive', { limit: 3 });

Use route for production request controls:

const result = router.route(userMessage, {
  maxTools: 6,
  maxEstimatedTokens: 4_000,
  pinned: ['auth_get_current_user'],
  fallback: 'none'
});

console.log({
  selected: result.selectedCount,
  estimatedTokens: result.estimatedTokens,
  estimatedReduction: result.tokenReduction
});

Pinned tools are always included before ranked tools. If pinned definitions exceed the token budget, budgetExceeded is true; explicit policy is never silently discarded.

Token counts are transparent estimates based on JSON character length divided by four. They are useful for relative budgets, not a replacement for a provider-specific tokenizer.

Features and Integrations

Supported definitions

Source Recognized shape Returned by route()
MCP { name, inputSchema } original MCP tool
OpenAI Responses { type: 'function', name, parameters } original Responses tool
OpenAI Chat { type: 'function', function: { ... } } original Chat tool
Anthropic { name, input_schema } original Anthropic tool
Gemini { name, parameters } or functionDeclarations original Gemini declaration
Canonical { name, inputSchema } or { name, schema } original object

Provider envelopes are accepted directly:

const openaiRouter = createToolRouter({ tools: openaiTools });
const geminiRouter = createToolRouter({ functionDeclarations });

Keep one provider-compatible catalog per outbound request. The package normalizes definitions for retrieval; it does not rewrite JSON Schema dialects or convert one provider's wire format into another.

Dynamic catalogs

Updates maintain postings and document frequencies without rebuilding the router:

const router = createToolRouter([], { onDuplicate: 'replace' });

router.add(tool);
router.add(updatedTool); // replaces the same id
router.remove('github_create_issue');
router.replace(await loadCurrentCatalog());
router.clear();

By default, duplicate IDs throw ERR_TOOL_DUPLICATE. Tool IDs use an explicit id when present, otherwise namespace:name, otherwise name.

BYOT discovery helper

createToolSearch creates a compact search function and an executor for bring-your-own-tool discovery loops:

import { createToolSearch, createToolRouter } from '@stackline/tool-router';

const router = createToolRouter(mcpTools);
const discovery = createToolSearch(router, {
  target: 'mcp',
  limit: 5
});

console.log(discovery.definition);
console.log(discovery.execute({ query: 'search production errors' }));

Targets are canonical, mcp, openai-responses, openai-chat, anthropic, and gemini. The executor returns compact summaries, not full schemas. Applications decide how selected tools are admitted into the next model request.

Ranking controls

Default field weights favor intent-bearing identifiers:

Field Weight
name 10
namespace 8
aliases 7
tags 5
description 2
schema 1

Override only what the catalog needs:

const router = createToolRouter(tools, {
  fieldWeights: {
    tags: 8,
    schema: 2
  },
  fuzzy: true,
  k1: 1.2,
  b: 0.75
});

The built-in English action synonyms cover common tool verbs such as find/search, send/post, create/open, and change/update. Extend them:

const router = createToolRouter(tools, {
  synonyms: {
    archive: ['store', 'retain'],
    deploy: ['release', 'ship']
  }
});

Set synonyms: false for literal-only retrieval. Custom tokenizers are also supported and receive both indexed text and queries.

Catalog metadata

Canonical metadata improves routing without changing provider payloads:

const tool = {
  id: 'github:pull-request:create',
  namespace: 'github',
  tags: ['git', 'write'],
  aliases: ['open pull request', 'new PR'],
  name: 'github_create_pull_request',
  description: 'Create a pull request from one branch into another.',
  inputSchema: { type: 'object', properties: {} }
};

For provider definitions, metadata can be placed on the outer tool object. The original object is returned unchanged and by reference.

Evaluation and performance

The repository includes the complete corpus and benchmark command:

npm run benchmark

The 1.0.0 release baseline on the maintainer workstation:

These numbers are implementation baselines, not universal guarantees. Hardware, catalog vocabulary, descriptions, aliases, and query distribution materially change the result. Run the included benchmark with representative tools before choosing production limits.

Documentation

Examples are included in the npm tarball and build provider request objects without credentials or network calls. This makes format compatibility executable before an application connects its own SDK.

Security

Security and limits

Tool definitions are untrusted input. The implementation:

Defaults are intended for ordinary provider schemas. Raise limits only for a catalog that has already been validated. See SECURITY.md for private vulnerability reporting.

API Surface

API

Export Purpose
createToolRouter(tools, options) build a mutable in-memory router
router.search(query, options) ranked matches with evidence
router.select(query, options) original tool definitions only
router.route(query, options) selected tools plus budget metrics
router.add/remove/replace/clear update a live catalog
router.get/list/has/stats inspect the normalized catalog
routeTools(tools, query, options) one-shot routing helper
createToolSearch(router, options) provider-shaped discovery function
normalizeTool/normalizeTools inspect canonical retrieval records
detectToolFormat(tool) detect a supported provider shape
estimateToolTokens(tool) bounded provider-neutral size estimate
tokenize/normalizeText use the default text pipeline directly

Every validation error is a ToolRouterError with a stable code beginning with ERR_TOOL_.

Local Development

git clone https://github.com/alexandroit/stackline-tool-router.git
cd stackline-tool-router
npm ci
npm run test

Release tooling uses Node.js 24.20.0 and npm 11.19.0. The consumer runtime contract remains the one documented above.

Consumer Smoke Test

Run the repository's existing consumer/package check after installing development dependencies:

npm run test:install

Release Checklist

Run npm run test and inspect the package contents before release. Publish a new version through the GitHub Actions publishing workflow, using the SHA-512 digest of the reviewed tarball. Verify the exact published version, tarball integrity, and npm provenance after the run.

License

MIT

Credits and original authors

Original copyright, license notices and contributor acknowledgements remain part of this distribution. Stackline maintenance does not replace authorship of the original work.

Community and Links

Use this repository's issue tracker for reproducible bugs and feature requests. Join r/Stackline for examples, usage questions and release discussions.