Enterprise Search

BM25 progressive tool discovery for large tool catalogs

The Enterprise Search template (TypeScript only) shows how to serve a large tool catalog without flooding the model's context. Tools are grouped by domain with @Controller, and BM25SearchTransform collapses the catalog into two meta-tools that the model uses to find and call the tool it needs.

What's included

Three controllers

Finance, inventory, and support tools, each with a name prefix.

BM25 search

In-memory search over tool names, descriptions, and parameters.

Always-visible tools

Four bootstrap tools stay in the catalog.

Widgets

financial-summary, inventory-monitor, and system-health.

Create the project

Scaffold the template and start it:

npx @nitrostack/cli init enterprise-tools --template typescript-enterprise-search
cd enterprise-tools
npm run dev

How it works

Each controller adds a prefix to its tools, so calculate_tax in @Controller('finance') is exposed as finance_calculate_tax:

src/controllers/finance.controller.ts
@Controller('finance')
export class FinanceController {
  @Tool({
    name: 'calculate_tax',
    description: 'Calculate sales and corporate tax for a transaction',
    inputSchema: z.object({
      amount: z.number().positive().describe('Gross transaction amount'),
      region: z.enum(['US-CA', 'US-NY', 'EU-DE', 'UK']).describe('Tax jurisdiction'),
    }),
  })
  async calculateTax(input: { amount: number; region: string }, ctx: ExecutionContext) {
    // ...
  }
}

The root module attaches the transform and lists the tools that should stay visible:

src/app.module.ts
@McpApp({
  module: AppModule,
  server: { name: 'enterprise-search-service', version: '1.0.0' },
  transforms: [
    new BM25SearchTransform({
      defaultLimit: 5,
      alwaysVisible: [
        'support_auth_login',
        'support_get_system_status',
        'finance_get_financial_summary',
        'inventory_report',
      ],
    }),
  ],
})

What the model sees

tools/list returns six tools instead of the full catalog: search_tools, call_tool, and the four always-visible tools. To use anything else, the model first searches, then calls the tool through call_tool:

{ "name": "search_tools", "arguments": { "query": "calculate customer sales tax" } }
{ "name": "call_tool", "arguments": { "name": "finance_calculate_tax", "arguments": { "amount": 1000, "region": "US-CA" } } }

The second call returns { "gross": 1000, "tax": 82.5, "net": 1082.5 }. Arguments are validated against the tool's schema even when it is called through call_tool.

Next steps

On this page