* Keyword-based search over tool names and descriptions. * Handles both MCP tools (mcp__server__action) and regular tools (CamelCase). * * The model typically queries with: * - Server names when it knows the integration (e.g., "slack", "github") * - Action words when looking for functionality (
( query: string, deferredTools: Tools, tools: Tools, maxResults: number, )
| 184 | * - Tool-specific terms (e.g., "notebook", "shell", "kill") |
| 185 | */ |
| 186 | async function searchToolsWithKeywords( |
| 187 | query: string, |
| 188 | deferredTools: Tools, |
| 189 | tools: Tools, |
| 190 | maxResults: number, |
| 191 | ): Promise<string[]> { |
| 192 | const queryLower = query.toLowerCase().trim() |
| 193 | |
| 194 | // Fast path: if query matches a tool name exactly, return it directly. |
| 195 | // Handles models using a bare tool name instead of select: prefix (seen |
| 196 | // from subagents/post-compaction). Checks deferred first, then falls back |
| 197 | // to the full tool set — selecting an already-loaded tool is a harmless |
| 198 | // no-op that lets the model proceed without retry churn. |
| 199 | const exactMatch = |
| 200 | deferredTools.find(t => t.name.toLowerCase() === queryLower) ?? |
| 201 | tools.find(t => t.name.toLowerCase() === queryLower) |
| 202 | if (exactMatch) { |
| 203 | return [exactMatch.name] |
| 204 | } |
| 205 | |
| 206 | // If query looks like an MCP tool prefix (mcp__server), find matching tools. |
| 207 | // Handles models searching by server name with mcp__ prefix. |
| 208 | if (queryLower.startsWith('mcp__') && queryLower.length > 5) { |
| 209 | const prefixMatches = deferredTools |
| 210 | .filter(t => t.name.toLowerCase().startsWith(queryLower)) |
| 211 | .slice(0, maxResults) |
| 212 | .map(t => t.name) |
| 213 | if (prefixMatches.length > 0) { |
| 214 | return prefixMatches |
| 215 | } |
| 216 | } |
| 217 | |
| 218 | const queryTerms = queryLower.split(/\s+/).filter(term => term.length > 0) |
| 219 | |
| 220 | // Partition into required (+prefixed) and optional terms |
| 221 | const requiredTerms: string[] = [] |
| 222 | const optionalTerms: string[] = [] |
| 223 | for (const term of queryTerms) { |
| 224 | if (term.startsWith('+') && term.length > 1) { |
| 225 | requiredTerms.push(term.slice(1)) |
| 226 | } else { |
| 227 | optionalTerms.push(term) |
| 228 | } |
| 229 | } |
| 230 | |
| 231 | const allScoringTerms = |
| 232 | requiredTerms.length > 0 ? [...requiredTerms, ...optionalTerms] : queryTerms |
| 233 | const termPatterns = compileTermPatterns(allScoringTerms) |
| 234 | |
| 235 | // Pre-filter to tools matching ALL required terms in name or description |
| 236 | let candidateTools = deferredTools |
| 237 | if (requiredTerms.length > 0) { |
| 238 | const matches = await Promise.all( |
| 239 | deferredTools.map(async tool => { |
| 240 | const parsed = parseToolName(tool.name) |
| 241 | const description = await getToolDescriptionMemoized(tool.name, tools) |
| 242 | const descNormalized = description.toLowerCase() |
| 243 | const hintNormalized = tool.searchHint?.toLowerCase() ?? '' |
no test coverage detected