Skip to content

[Story] Bm25Ranker for MCP tool search (pure-JS, no external deps) #575

@edelauna

Description

@edelauna

Context

A pure-JS BM25 ranker is the always-available baseline for dynamic tool loading. It works without any embedding provider configured, has no network dependency, and handles the keyword-heavy queries that dominate MCP tool search ("send slack message", "read jira ticket").

This is foundational for the dynamic loading feature (#577 ToolRouter, #578 threshold gate); it doesn't close a user-facing issue on its own.

Developer Notes

  • New `src/services/tools/Bm25Ranker.ts` implementing a `Ranker` interface.
  • `Ranker` interface (in `src/services/tools/types.ts`): `rank(query: string, items: ToolDoc[], k: number): ToolDoc[]`.
  • `ToolDoc` shape: `{ serverName: string, toolName: string, description: string }`. Ranker tokenizes the concatenation `${serverName} ${toolName} ${description}`.
  • Tokenizer: lowercase, split on non-word chars. Standard BM25 (k1=1.5, b=0.75). No external dependencies.
  • Build the inverted index lazily on first `rank()` call; rebuild when the input set changes (detected by reference inequality).

Acceptance Criteria

  • `Bm25Ranker` implements `Ranker` interface
  • Unit test: query "send slack message" ranks `slack.postMessage` above `jira.createIssue` above `postgres.query`
  • Unit test: empty query → empty result; query with no matches → empty result
  • No external npm dependencies added

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions