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
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
Acceptance Criteria