From f84bcd04879f424c8351eeafecfa645c6e429798 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20J=C3=A4gle?= Date: Thu, 26 Mar 2026 20:27:21 +0100 Subject: [PATCH] feat: improve agent UX for search_docs tool - Rename 'keywords' parameter to 'pattern' to signal regex, not keyword list - Remove 'fallback_pattern' parameter (no-match hints make it redundant) - Change context_lines default from 0 to 2 for useful context on first call - Add actionable recovery guidance when search returns no matches - Surface warning when invalid regex is auto-escaped to literal string - Make truncated message actionable (suggest narrowing pattern) BREAKING CHANGE: search_docs parameter renamed from 'keywords' to 'pattern', 'generalized_keywords'/'fallback_pattern' parameter removed entirely --- .beads/issues.jsonl | 7 ++ .beads/last-touched | 2 +- ...evelopment-plan-rename-search-parameter.md | 75 +++++++++++++++++++ index.ts | 27 ++++--- packages/core/src/__tests__/loader.test.ts | 12 +-- packages/core/src/__tests__/searcher.test.ts | 21 ------ .../src/__tests__/template-processor.test.ts | 44 ++++++----- packages/core/src/search/searcher.ts | 66 ++++++++++------ packages/core/src/templates/processor.ts | 22 ++---- packages/core/src/types.ts | 26 +++---- .../src/__tests__/integration.test.ts | 6 +- .../src/__tests__/performance.test.ts | 2 +- .../src/__tests__/web-sources.test.ts | 16 ++-- packages/mcp-server/src/server.ts | 60 ++++++--------- test/e2e/mcp-protocol-compliance.test.ts | 37 +++------ test/utils/e2e-test-setup.ts | 8 +- 16 files changed, 232 insertions(+), 199 deletions(-) create mode 100644 .vibe/development-plan-rename-search-parameter.md diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index ac3019f..f6a3f07 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -3,3 +3,10 @@ {"id":"agentic-knowledge-1.2","title":"Plan","description":"Create a detailed implementation strategy based on your exploration: - If exists: Base your strategy on requirements from it - Otherwise: Use existing task context Break down the work into specific, actionable tasks. Consider edge cases, dependencies, and potential challenges. - If architectural changes needed and exists: Document in - Otherwise: Create tasks to track architectural decisions - If exists: Adhere to the design in it - Otherwise: Elaborate design options and present them to the user Document the planning work thoroughly and create implementation tasks as part of the code phase as needed.","status":"open","priority":3,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-10T09:55:15.524416+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-10T09:55:15.524416+01:00","dependencies":[{"issue_id":"agentic-knowledge-1.2","depends_on_id":"agentic-knowledge-1","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"agentic-knowledge-1.2","depends_on_id":"agentic-knowledge-1.1","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} {"id":"agentic-knowledge-1.3","title":"Code","description":"Follow your plan to build the solution: - If exists: Follow the design from it - Otherwise: Elaborate design options and present them to the user - If exists: Build according to the architecture from it - Otherwise: Elaborate architectural options and present them to the user - If exists: Ensure requirements from it are met - Otherwise: Ensure existing requirements are met based on your task context Write clean, well-structured code with proper error handling. Prevent regression by building, linting, and executing existing tests. Stay flexible and adapt the plan as you learn more during implementation. Update task progress and create new tasks as needed.","status":"open","priority":3,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-10T09:55:15.67957+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-10T09:55:15.67957+01:00","dependencies":[{"issue_id":"agentic-knowledge-1.3","depends_on_id":"agentic-knowledge-1","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"agentic-knowledge-1.3","depends_on_id":"agentic-knowledge-1.2","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} {"id":"agentic-knowledge-1.4","title":"Commit","description":"Ensure code quality and documentation accuracy through systematic cleanup and review. **STEP 1: Code Cleanup** Systematically clean up development artifacts: 1. **Remove Debug Output**: Search for and remove all temporary debug output statements used during development. Look for language-specific debug output methods (console logging, print statements, debug output functions). Remove any debugging statements that were added for development purposes. 2. **Review TODO/FIXME Comments**: - Address each TODO/FIXME comment by either implementing the solution or documenting why it's deferred - Remove completed TODOs - Convert remaining TODOs to proper issue tracking if needed 3. **Remove Debugging Code Blocks**: - Remove temporary debugging code, test code blocks, and commented-out code - Clean up any experimental code that's no longer needed - Ensure proper error handling replaces temporary debug logging **STEP 2: Documentation Review** Review and update documentation to reflect final implementation: 1. **Update Long-Term Memory Documents**: Based on what was actually implemented: - If exists: Update it if requirements changed during development - If exists: Update it if architectural impacts were identified - If exists: Update it if design details were refined or changed - Otherwise: Document any changes in the plan file 2. **Compare Against Implementation**: Review documentation against actual implemented functionality 3. **Update Changed Sections**: Only modify documentation sections that have functional changes 4. **Remove Development Progress**: Remove references to development iterations, progress notes, and temporary decisions 5. **Focus on Final State**: Ensure documentation describes the final implemented state, not the development process 6. **Ask User to Review Document Updates** **STEP 3: Final Validation** - Run existing tests to ensure cleanup didn't break functionality - Verify documentation accuracy with a final review - Ensure code is ready for production/delivery Update task progress and mark completed work as you finalize the feature.","status":"open","priority":3,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-10T09:55:15.837066+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-10T09:55:15.837066+01:00","dependencies":[{"issue_id":"agentic-knowledge-1.4","depends_on_id":"agentic-knowledge-1","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"agentic-knowledge-1.4","depends_on_id":"agentic-knowledge-1.3","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"agentic-knowledge-1.5","title":"Improve agent UX for search_docs tool","status":"open","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-26T18:45:20.15613+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-26T18:45:20.15613+01:00","dependencies":[{"issue_id":"agentic-knowledge-1.5","depends_on_id":"agentic-knowledge-1","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"agentic-knowledge-1.5.1","title":"Rename keywords→pattern, generalized_keywords→fallback_pattern in MCP schema + handler","description":"Parameter names carry implicit semantics. 'keywords' suggests space-separated list; 'pattern' signals single regex expression","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-26T18:45:25.754448+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-26T18:56:36.44898+01:00","closed_at":"2026-03-26T18:56:36.44898+01:00","close_reason":"Closed","dependencies":[{"issue_id":"agentic-knowledge-1.5.1","depends_on_id":"agentic-knowledge-1.5","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"agentic-knowledge-1.5.2","title":"Change context_lines default from 0 to 2","description":"Default of 0 forces agents to make multiple calls. Default of 2 provides useful context on first try","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-26T18:45:30.230618+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-26T18:56:58.835616+01:00","closed_at":"2026-03-26T18:56:58.835616+01:00","close_reason":"Closed","dependencies":[{"issue_id":"agentic-knowledge-1.5.2","depends_on_id":"agentic-knowledge-1.5","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"agentic-knowledge-1.5.3","title":"Add recovery guidance to no-match response","description":"When search returns 0 matches, suggest using | alternation, relaxing pattern, or using fallback_pattern","status":"closed","priority":1,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-26T18:45:35.402586+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-26T18:57:22.864588+01:00","closed_at":"2026-03-26T18:57:22.864588+01:00","close_reason":"Closed","dependencies":[{"issue_id":"agentic-knowledge-1.5.3","depends_on_id":"agentic-knowledge-1.5","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"agentic-knowledge-1.5.4","title":"Surface invalid regex as warning in result","description":"Currently invalid regex is silently treated as literal. Agent should know pattern was escaped","status":"closed","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-26T18:45:39.290333+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-26T18:58:37.319396+01:00","closed_at":"2026-03-26T18:58:37.319396+01:00","close_reason":"Closed","dependencies":[{"issue_id":"agentic-knowledge-1.5.4","depends_on_id":"agentic-knowledge-1.5","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"agentic-knowledge-1.5.5","title":"Make truncated message actionable","description":"Suggest narrowing with more specific pattern when results are truncated at 50","status":"closed","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-26T18:45:43.069704+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-26T18:58:52.038744+01:00","closed_at":"2026-03-26T18:58:52.038744+01:00","close_reason":"Closed","dependencies":[{"issue_id":"agentic-knowledge-1.5.5","depends_on_id":"agentic-knowledge-1.5","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]} +{"id":"agentic-knowledge-1.5.6","title":"Update internal types to stay in sync with parameter rename","description":"SearchDocsParams, TemplateContext, DEFAULT_TEMPLATE still use keywords - update to pattern","status":"closed","priority":2,"issue_type":"task","owner":"github@beimir.net","created_at":"2026-03-26T18:45:46.586209+01:00","created_by":"Oliver Jägle","updated_at":"2026-03-26T19:12:40.947871+01:00","closed_at":"2026-03-26T19:12:40.947871+01:00","close_reason":"Closed","dependencies":[{"issue_id":"agentic-knowledge-1.5.6","depends_on_id":"agentic-knowledge-1.5","type":"parent-child","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"agentic-knowledge-1.5.6","depends_on_id":"agentic-knowledge-1.5.1","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]} diff --git a/.beads/last-touched b/.beads/last-touched index ddc3bfd..61d7ea1 100644 --- a/.beads/last-touched +++ b/.beads/last-touched @@ -1 +1 @@ -agentic-knowledge-1.4 +agentic-knowledge-1.5.6 diff --git a/.vibe/development-plan-rename-search-parameter.md b/.vibe/development-plan-rename-search-parameter.md new file mode 100644 index 0000000..696bbd9 --- /dev/null +++ b/.vibe/development-plan-rename-search-parameter.md @@ -0,0 +1,75 @@ +# Development Plan: agentic-knowledge (rename-search-parameter branch) + +_Generated on 2026-03-26 by Vibe Feature MCP_ +_Workflow: [epcc](https://mrsimpson.github.io/responsible-vibe-mcp/workflows/epcc)_ + +## Goal + +Improve agent UX for the `search_docs` MCP tool. Agents currently misuse the tool by sending space-delimited keyword lists (e.g., "user authentication login") instead of regex patterns (e.g., "user|auth|login"). This causes failed searches and wasted round-trips. + +**Final Scope (simplified):** + +1. ✅ Rename `keywords` → `pattern` (strongest signal for regex) +2. ✅ Remove `fallback_pattern` entirely — agents can refine patterns based on no-match hints +3. ✅ Change `context_lines` default from 0 → 2 (reduce round-trips) +4. ✅ Add recovery guidance to no-match responses (actionable feedback) +5. ✅ Surface invalid regex as warning (transparency) +6. ✅ Make truncated message actionable (guide narrowing) +7. ✅ Update internal types to stay in sync + +**Out of scope:** Exposing `include` filter to agents (intentionally internal for now) + +## Explore + + + +### Tasks + +_Tasks managed via `bd` CLI_ + +## Plan + + + +### Tasks + +_Tasks managed via `bd` CLI_ + +## Code + + + +### Tasks + +_Tasks managed via `bd` CLI_ + +## Commit + + + +### Tasks + +_Tasks managed via `bd` CLI_ + +## Key Decisions + +- **Parameter naming:** Use `pattern` (not `regex` or `query`) — familiar from grep/ripgrep, signals single expression without being intimidating +- **Removed fallback_pattern:** The no-match recovery guidance makes a separate fallback parameter redundant — agents can simply refine their pattern using the suggested techniques (|, .\*, etc.) +- **Default context_lines:** 2 lines before/after — enough for understanding, not overwhelming +- **Recovery guidance style:** Concise, actionable hints inline with the no-match message +- **Backward compatibility:** This is a breaking change to the MCP tool schema, but acceptable since tool is new + +## Notes + +**Implementation complete** — All 265 tests passing. Changes: + +1. Renamed `keywords` → `pattern` in MCP schema, handler, types, and all tests +2. Completely removed `fallback_pattern` / `fallbackPattern` from the API +3. Changed `DEFAULT_CONTEXT_LINES` from 0 to 2 +4. Added comprehensive no-match hints with regex tips (`|`, `.*`, `\b`, etc.) +5. Added warning when invalid regex is auto-escaped to literal +6. Made truncated message actionable (suggests narrowing pattern) + +--- + +_This plan is maintained by the LLM and uses beads CLI for task management. Tool responses provide guidance on which bd commands to use for task management._ diff --git a/index.ts b/index.ts index 7b893ad..aabc504 100644 --- a/index.ts +++ b/index.ts @@ -1,18 +1,17 @@ /** * Agentic Knowledge System - * + * * A standalone agentic knowledge guidance system with search_docs() interface - * that returns intelligent navigation instructions based on docset, keywords, - * and generalized_keywords - delegating language processing to the agent - * while providing structured guidance. + * that returns intelligent search results based on docset and regex pattern - + * delegating language processing to the agent while providing structured guidance. */ // Export main MCP server functionality -export { - createAgenticKnowledgeServer, - startMCPServer, - startServer -} from './packages/mcp-server/dist/index.js'; +export { + createAgenticKnowledgeServer, + startMCPServer, + startServer, +} from "./packages/mcp-server/dist/index.js"; // Export core functionality for advanced usage export { @@ -23,8 +22,8 @@ export { createTemplateContext, getEffectiveTemplate, validateTemplate, - extractVariables -} from './packages/core/dist/index.js'; + extractVariables, +} from "./packages/core/dist/index.js"; // Export types export type { @@ -34,7 +33,7 @@ export type { SearchDocsResponse, ListDocsetsResponse, TemplateContext, - ErrorType -} from './packages/core/dist/index.js'; + ErrorType, +} from "./packages/core/dist/index.js"; -export { KnowledgeError } from './packages/core/dist/index.js'; \ No newline at end of file +export { KnowledgeError } from "./packages/core/dist/index.js"; diff --git a/packages/core/src/__tests__/loader.test.ts b/packages/core/src/__tests__/loader.test.ts index 366b7a3..8daba00 100644 --- a/packages/core/src/__tests__/loader.test.ts +++ b/packages/core/src/__tests__/loader.test.ts @@ -35,13 +35,13 @@ docsets: sources: - type: "local_folder" paths: ["./docs"] - template: "Custom template for {{keywords}} in {{local_path}}" + template: "Custom template for {{pattern}} in {{local_path}}" - id: "api-docs" name: "API Documentation" sources: - type: "local_folder" paths: ["/absolute/path"] -template: "Global template for {{keywords}} in {{local_path}}"`; +template: "Global template for {{pattern}} in {{local_path}}"`; await fs.writeFile(validConfigPath, validConfig); // Create invalid config file (missing required fields) @@ -133,7 +133,7 @@ docsets: sources: - type: "local_folder" paths: ["./docs"] -template: "Global template with {{keywords}} and {{invalid_variable}}"`; +template: "Global template with {{pattern}} and {{invalid_variable}}"`; await fs.writeFile(invalidTemplateConfigPath, invalidTemplateConfig); await expect(loadConfig(invalidTemplateConfigPath)).rejects.toThrow( @@ -165,7 +165,7 @@ docsets: sources: - type: "local_folder" paths: ["./docs"] - template: "Docset template missing {{keywords}} but has {{bad_var}}"`; + template: "Docset template missing {{pattern}} but has {{bad_var}}"`; await fs.writeFile( invalidDocsetTemplateConfigPath, invalidDocsetTemplateConfig, @@ -197,8 +197,8 @@ docsets: sources: - type: "local_folder" paths: ["./docs"] - template: "Search {{keywords}} in {{local_path}}" -template: "Global: {{keywords}} in {{local_path}}"`; + template: "Search {{pattern}} in {{local_path}}" +template: "Global: {{pattern}} in {{local_path}}"`; await fs.writeFile(validTemplateConfigPath, validTemplateConfig); const config = await loadConfig(validTemplateConfigPath); diff --git a/packages/core/src/__tests__/searcher.test.ts b/packages/core/src/__tests__/searcher.test.ts index f224ec2..34301a3 100644 --- a/packages/core/src/__tests__/searcher.test.ts +++ b/packages/core/src/__tests__/searcher.test.ts @@ -190,27 +190,6 @@ describe("searchDocset – context lines", () => { }); }); -// --------------------------------------------------------------------------- -// Fallback behaviour -// --------------------------------------------------------------------------- - -describe("searchDocset – fallback pattern", () => { - test("uses fallbackPattern when primary yields no results", async () => { - const result = await searchDocset(fixtureDir, "xyzzy_no_match_ever", { - fallbackPattern: "authentication", - }); - expect(result.total_matches).toBeGreaterThan(0); - expect(result.used_pattern).toBe("authentication"); - }); - - test("does not fall back when primary yields results", async () => { - const result = await searchDocset(fixtureDir, "authentication", { - fallbackPattern: "xyzzy_no_match_ever", - }); - expect(result.used_pattern).toBe("authentication"); - }); -}); - // --------------------------------------------------------------------------- // Ignored paths // --------------------------------------------------------------------------- diff --git a/packages/core/src/__tests__/template-processor.test.ts b/packages/core/src/__tests__/template-processor.test.ts index fdaa856..9022b9d 100644 --- a/packages/core/src/__tests__/template-processor.test.ts +++ b/packages/core/src/__tests__/template-processor.test.ts @@ -22,24 +22,23 @@ describe("Template Processing", () => { id: "test-docs", name: "Test Documentation", description: "Sample documentation for testing", - local_path: "./docs", + sources: [], }; const sampleContext: TemplateContext = { local_path: "/project/docs", - keywords: "react hooks", - generalized_keywords: "react state management", + pattern: "react hooks", docset: sampleDocset, }; describe("processTemplate - core functionality", () => { test("should replace all valid template variables", () => { - const template = `Search "{{keywords}}" in {{local_path}} ({{docset_name}}). Also try: {{generalized_keywords}}. Description: {{docset_description}}`; + const template = `Search "{{pattern}}" in {{local_path}} ({{docset_name}}). Description: {{docset_description}}`; const result = processTemplate(template, sampleContext); expect(result).toBe( - 'Search "react hooks" in /project/docs (Test Documentation). Also try: react state management. Description: Sample documentation for testing', + 'Search "react hooks" in /project/docs (Test Documentation). Description: Sample documentation for testing', ); }); @@ -47,7 +46,7 @@ describe("Template Processing", () => { const docsetWithoutDescription: DocsetConfig = { id: "minimal", name: "Minimal Docs", - local_path: "./minimal", + sources: [], }; const context: TemplateContext = { @@ -62,7 +61,7 @@ describe("Template Processing", () => { }); test("should throw error for invalid variables in processed template", () => { - const template = "Valid: {{keywords}}, Invalid: {{unknown_variable}}"; + const template = "Valid: {{pattern}}, Invalid: {{unknown_variable}}"; expect(() => processTemplate(template, sampleContext)).toThrow( KnowledgeError, @@ -80,7 +79,7 @@ describe("Template Processing", () => { }); test("should handle whitespace in variable names", () => { - const template = "Search {{ keywords }} in {{ local_path }}"; + const template = "Search {{ pattern }} in {{ local_path }}"; const result = processTemplate(template, sampleContext); @@ -90,21 +89,21 @@ describe("Template Processing", () => { describe("validateTemplate - validation at startup", () => { test("should accept valid template with required variables", () => { - const validTemplate = "Search {{keywords}} in {{local_path}}"; + const validTemplate = "Search {{pattern}} in {{local_path}}"; expect(() => validateTemplate(validTemplate)).not.toThrow(); }); test("should accept template with all allowed variables", () => { const fullTemplate = - "{{keywords}} {{generalized_keywords}} {{local_path}} {{docset_id}} {{docset_name}} {{docset_description}}"; + "{{pattern}} {{local_path}} {{docset_id}} {{docset_name}} {{docset_description}}"; expect(() => validateTemplate(fullTemplate)).not.toThrow(); }); test("should reject template with invalid variables", () => { const invalidTemplate = - "Search {{keywords}} in {{local_path}} with {{invalid_variable}}"; + "Search {{pattern}} in {{local_path}} with {{invalid_variable}}"; expect(() => validateTemplate(invalidTemplate)).toThrow(KnowledgeError); @@ -123,7 +122,7 @@ describe("Template Processing", () => { }); test("should reject template missing required variables", () => { - const incompleteTemplate = "Only has {{keywords}}"; + const incompleteTemplate = "Only has {{pattern}}"; expect(() => validateTemplate(incompleteTemplate)).toThrow( KnowledgeError, @@ -140,7 +139,7 @@ describe("Template Processing", () => { }); test("should reject template with multiple invalid variables", () => { - const template = "{{keywords}} {{local_path}} {{bad_var1}} {{bad_var2}}"; + const template = "{{pattern}} {{local_path}} {{bad_var1}} {{bad_var2}}"; expect(() => validateTemplate(template)).toThrow(KnowledgeError); @@ -156,12 +155,12 @@ describe("Template Processing", () => { describe("extractVariables - parsing functionality", () => { test("should extract all variables from complex template", () => { const template = - "Search {{keywords}} in {{local_path}} for {{docset_name}} ({{docset_id}})"; + "Search {{pattern}} in {{local_path}} for {{docset_name}} ({{docset_id}})"; const result = extractVariables(template); expect(result).toEqual([ - "keywords", + "pattern", "local_path", "docset_name", "docset_id", @@ -169,11 +168,11 @@ describe("Template Processing", () => { }); test("should handle variables with whitespace", () => { - const template = "Search {{ keywords }} in {{ local_path }}"; + const template = "Search {{ pattern }} in {{ local_path }}"; const result = extractVariables(template); - expect(result).toEqual(["keywords", "local_path"]); + expect(result).toEqual(["pattern", "local_path"]); }); test("should return empty array for template without variables", () => { @@ -185,11 +184,11 @@ describe("Template Processing", () => { }); test("should preserve duplicate variables", () => { - const template = "Search {{keywords}} and also {{keywords}} again"; + const template = "Search {{pattern}} and also {{pattern}} again"; const result = extractVariables(template); - expect(result).toEqual(["keywords", "keywords"]); + expect(result).toEqual(["pattern", "pattern"]); }); }); @@ -229,9 +228,8 @@ describe("Template Processing", () => { test("should validate that all required variables are in default template", () => { const variables = extractVariables(DEFAULT_TEMPLATE); - expect(variables).toContain("keywords"); + expect(variables).toContain("pattern"); expect(variables).toContain("local_path"); - // Note: generalized_keywords no longer required in template since it's provided in structured response }); test("should ensure all variables in default template are allowed", () => { @@ -245,7 +243,7 @@ describe("Template Processing", () => { describe("edge cases and error handling", () => { test("should handle template with only required variables", () => { - const minimalTemplate = "Search {{keywords}} in {{local_path}}"; + const minimalTemplate = "Search {{pattern}} in {{local_path}}"; expect(() => validateTemplate(minimalTemplate)).not.toThrow(); @@ -258,7 +256,7 @@ describe("Template Processing", () => { }); test("should handle malformed variable syntax", () => { - const malformed = "Search {keywords} in {{local_path}}"; + const malformed = "Search {pattern} in {{local_path}}"; expect(() => validateTemplate(malformed)).toThrow(); }); diff --git a/packages/core/src/search/searcher.ts b/packages/core/src/search/searcher.ts index f1fa561..fc9f468 100644 --- a/packages/core/src/search/searcher.ts +++ b/packages/core/src/search/searcher.ts @@ -23,7 +23,7 @@ import type { SearchDocsResult, SearchMatch, SearchOptions } from "../types.js"; // Constants // --------------------------------------------------------------------------- -const DEFAULT_CONTEXT_LINES = 0; +const DEFAULT_CONTEXT_LINES = 2; const DEFAULT_MAX_MATCHES = 50; /** Directories / files that are never useful to search inside a docset. */ @@ -133,7 +133,7 @@ export async function buildFileIndex(rootPath: string): Promise { * Search `rootPath` for lines matching `pattern` (a regex string). * * @param rootPath Absolute path to the docset directory. - * @param pattern Primary search pattern. Supports full JS regex syntax + * @param pattern Search pattern. Supports full JS regex syntax * (e.g. `"auth|login"`, `"function\\s+\\w+"`, `"TODO.*fix"`). * The match is always case-insensitive. * @param options Optional tuning parameters. @@ -149,27 +149,12 @@ export async function searchDocset( const contextLines = options.contextLines ?? DEFAULT_CONTEXT_LINES; const maxMatches = options.maxMatches ?? DEFAULT_MAX_MATCHES; - // --- primary search --- - const primary = await runSearch( + return runSearch( rootPath, pattern, { contextLines, maxMatches, include: options.include }, index, ); - - if (primary.total_matches > 0 || !options.fallbackPattern?.trim()) { - return primary; - } - - // --- fallback search --- - const fallback = await runSearch( - rootPath, - options.fallbackPattern.trim(), - { contextLines, maxMatches, include: options.include }, - index, - ); - - return fallback; } // --------------------------------------------------------------------------- @@ -189,11 +174,13 @@ async function runSearch( index?: DocsetIndex, ): Promise { let regex: RegExp; + let patternWasEscaped = false; try { regex = new RegExp(pattern, "i"); } catch { - // Invalid regex: treat as literal string + // Invalid regex: treat as literal string and flag it regex = new RegExp(escapeRegex(pattern), "i"); + patternWasEscaped = true; } // Decide which files to scan @@ -251,13 +238,17 @@ async function runSearch( } } - return { + const result: SearchDocsResult = { matches, total_matches: totalMatches, searched_files: searchedFiles, used_pattern: pattern, truncated, }; + if (patternWasEscaped) { + result.pattern_was_escaped = true; + } + return result; } // --------------------------------------------------------------------------- @@ -455,11 +446,36 @@ function matchGlob(filePath: string, pattern: string): boolean { * suitable for returning as MCP tool content. */ export function formatSearchResult(result: SearchDocsResult): string { + // Warning header for escaped patterns + const warnings: string[] = []; + if (result.pattern_was_escaped) { + warnings.push( + `⚠️ Pattern "${result.used_pattern}" contained invalid regex syntax and was treated as a literal string.`, + ` If you intended to use regex, check for unescaped special characters like ( ) [ ] { } etc.`, + ``, + ); + } + if (result.matches.length === 0) { - return `No matches found for pattern: ${result.used_pattern}\n(searched ${result.searched_files} file${result.searched_files === 1 ? "" : "s"})`; + const hints = [ + `No matches found for pattern: "${result.used_pattern}"`, + `(searched ${result.searched_files} file${result.searched_files === 1 ? "" : "s"})`, + ``, + `💡 Tips to improve your search:`, + `• Use | for alternatives: "auth|login|session" matches any of these`, + `• Use .* for flexible matching: "config.*timeout" matches "configTimeout", "config_timeout", etc.`, + `• Use \\b for word boundaries: "\\bapi\\b" avoids matching "capital"`, + `• Simplify: try a single distinctive term instead of a phrase`, + ]; + if (!result.used_pattern.includes("|")) { + hints.push( + `• If you used spaces, note they are literal — use | to search for multiple terms`, + ); + } + return [...warnings, ...hints].join("\n"); } - const lines: string[] = []; + const lines: string[] = [...warnings]; let currentFile = ""; for (const match of result.matches) { @@ -478,9 +494,13 @@ export function formatSearchResult(result: SearchDocsResult): string { } } + const truncatedHint = result.truncated + ? ` [truncated at ${DEFAULT_MAX_MATCHES} — use a more specific pattern to narrow results]` + : ""; + const summary = [ ``, - `--- ${result.total_matches} match${result.total_matches === 1 ? "" : "es"} in ${result.searched_files} file${result.searched_files === 1 ? "" : "s"} (pattern: "${result.used_pattern}")${result.truncated ? ` [truncated at ${DEFAULT_MAX_MATCHES}]` : ""}`, + `--- ${result.total_matches} match${result.total_matches === 1 ? "" : "es"} in ${result.searched_files} file${result.searched_files === 1 ? "" : "s"} (pattern: "${result.used_pattern}")${truncatedHint}`, ]; return [...lines, ...summary].join("\n"); diff --git a/packages/core/src/templates/processor.ts b/packages/core/src/templates/processor.ts index 22d117e..0d799e5 100644 --- a/packages/core/src/templates/processor.ts +++ b/packages/core/src/templates/processor.ts @@ -27,8 +27,7 @@ export function processTemplate( // Replace template variables using double curly brace syntax const variables: Record = { local_path: context.local_path, - keywords: context.keywords, - generalized_keywords: context.generalized_keywords, + pattern: context.pattern, docset_id: context.docset.id, docset_name: context.docset.name, docset_description: context.docset.description || "", @@ -143,21 +142,18 @@ export function extractVariables(template: string): string[] { /** * Create template context from search parameters * @param localPath - Calculated local path - * @param keywords - Search keywords - * @param generalizedKeywords - Generalized keywords + * @param pattern - Search pattern * @param docset - Docset configuration * @returns Template context object */ export function createTemplateContext( localPath: string, - keywords: string, - generalizedKeywords: string, + pattern: string, docset: DocsetConfig, ): TemplateContext { return { local_path: localPath, - keywords, - generalized_keywords: generalizedKeywords, + pattern, docset, }; } @@ -165,21 +161,19 @@ export function createTemplateContext( /** * Create structured search response * @param instructions - Processed instruction text - * @param keywords - Search keywords - * @param generalizedKeywords - Generalized keywords + * @param pattern - Search pattern * @param localPath - Calculated local path * @returns Structured response object */ export function createStructuredResponse( instructions: string, - keywords: string, - generalizedKeywords: string, + pattern: string, localPath: string, ): import("../types.js").SearchDocsResponse { return { instructions, - search_terms: keywords, - generalized_search_terms: generalizedKeywords, + search_terms: pattern, + generalized_search_terms: "", path: localPath, }; } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index f708996..30cd113 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -89,10 +89,8 @@ export interface KnowledgeConfig { export interface SearchDocsParams { /** ID of the docset to search */ docset: string; - /** Specific keywords to search for */ - keywords: string; - /** Generalized keywords for broader search context */ - generalized_keywords: string; + /** Search pattern (regex) to find in the documentation */ + pattern: string; } /** @@ -140,17 +138,14 @@ export interface SearchDocsResult { used_pattern: string; /** True when results were capped at the maximum match limit */ truncated: boolean; + /** True when the input pattern was invalid regex and was escaped to a literal string */ + pattern_was_escaped?: boolean; } /** * Options controlling search behaviour */ export interface SearchOptions { - /** - * Fallback pattern used when the primary pattern yields no results. - * Typically the value of the `generalized_keywords` tool parameter. - */ - fallbackPattern?: string; /** Number of context lines to include before and after each match (default: 2) */ contextLines?: number; /** Maximum number of matches to return before truncating (default: 50) */ @@ -178,10 +173,8 @@ export interface ListDocsetsResponse { export interface TemplateContext { /** Path to search in */ local_path: string; - /** Keywords to search for */ - keywords: string; - /** Generalized keywords */ - generalized_keywords: string; + /** Search pattern */ + pattern: string; /** Docset information */ docset: DocsetConfig; } @@ -217,15 +210,14 @@ export class KnowledgeError extends Error { /** * Default instruction template */ -export const DEFAULT_TEMPLATE = `Use available text search tools to search for {{keywords}} in {{local_path}} and its sub folders. Try broader terms if needed. Skip: node_modules/, .git/, build/, dist/.`; +export const DEFAULT_TEMPLATE = `Use available text search tools to search for {{pattern}} in {{local_path}} and its sub folders. Try broader terms if needed. Skip: node_modules/, .git/, build/, dist/.`; /** * Allowed template variables that can be used in instruction templates */ export const ALLOWED_TEMPLATE_VARIABLES = [ "local_path", - "keywords", - "generalized_keywords", + "pattern", "docset_id", "docset_name", "docset_description", @@ -234,7 +226,7 @@ export const ALLOWED_TEMPLATE_VARIABLES = [ /** * Required template variables that must be present in every template */ -export const REQUIRED_TEMPLATE_VARIABLES = ["local_path", "keywords"] as const; +export const REQUIRED_TEMPLATE_VARIABLES = ["local_path", "pattern"] as const; /** * Configuration file name pattern diff --git a/packages/mcp-server/src/__tests__/integration.test.ts b/packages/mcp-server/src/__tests__/integration.test.ts index 80d56ea..a391841 100644 --- a/packages/mcp-server/src/__tests__/integration.test.ts +++ b/packages/mcp-server/src/__tests__/integration.test.ts @@ -27,7 +27,7 @@ docsets: name: "Test Documentation" description: "Test documentation for integration tests" local_path: "./docs" -template: "Search for '{{keywords}}' in {{local_path}}. Also consider: {{generalized_keywords}}" +template: "Search for '{{pattern}}' in {{local_path}}." `; await fs.writeFile(tempConfigPath, testConfig); @@ -60,7 +60,7 @@ template: "Search for '{{keywords}}' in {{local_path}}. Also consider: {{general expect(server).toBeDefined(); }); - it("should distinguish between keywords and generalized_keywords in descriptions", () => { + it("should distinguish between pattern and fallback_pattern in descriptions", () => { // This validates that our implementation follows the user requirements // for clear parameter distinction in the tool metadata const server = createAgenticKnowledgeServer(); @@ -189,7 +189,7 @@ docsets: name: "search_docs", arguments: { docset_id: "uninitialized-docs", - keywords: "test", + pattern: "test", }, }, }); diff --git a/packages/mcp-server/src/__tests__/performance.test.ts b/packages/mcp-server/src/__tests__/performance.test.ts index 8ff2b5d..8862512 100644 --- a/packages/mcp-server/src/__tests__/performance.test.ts +++ b/packages/mcp-server/src/__tests__/performance.test.ts @@ -27,7 +27,7 @@ docsets: name: "Test Documentation" description: "Test documentation for performance tests" local_path: "./docs" -template: "Search for '{{keywords}}' in {{local_path}}. Related: {{generalized_keywords}}" +template: "Search for '{{pattern}}' in {{local_path}}." `; await fs.writeFile(tempConfigPath, testConfig); diff --git a/packages/mcp-server/src/__tests__/web-sources.test.ts b/packages/mcp-server/src/__tests__/web-sources.test.ts index c446412..3419f15 100644 --- a/packages/mcp-server/src/__tests__/web-sources.test.ts +++ b/packages/mcp-server/src/__tests__/web-sources.test.ts @@ -43,7 +43,7 @@ docsets: - type: "local_folder" paths: ["./docs/local"] -template: "Search for '{{keywords}}' in {{local_path}}. Also consider: {{generalized_keywords}}" +template: "Search for '{{pattern}}' in {{local_path}}." `; await fs.writeFile(join(knowledgeDir, "config.yaml"), testConfig); @@ -117,8 +117,7 @@ template: "Search for '{{keywords}}' in {{local_path}}. Also consider: {{general name: "search_docs", arguments: { docset_id: "web-source-docs", - keywords: "Test Documentation", - generalized_keywords: "content", + pattern: "Test Documentation", }, }, }; @@ -147,7 +146,7 @@ template: "Search for '{{keywords}}' in {{local_path}}. Also consider: {{general name: "search_docs", arguments: { docset_id: "local-docs", - keywords: "Local Guide", + pattern: "Local Guide", }, }, }; @@ -199,7 +198,7 @@ template: "Search for '{{keywords}}' in {{local_path}}. Also consider: {{general name: "search_docs", arguments: { docset_id: "unknown-docset", - keywords: "test search", + pattern: "test search", }, }, }; @@ -225,7 +224,7 @@ template: "Search for '{{keywords}}' in {{local_path}}. Also consider: {{general name: "search_docs", arguments: { docset_id: "web-source-docs", - // Missing required 'keywords' parameter + // Missing required 'pattern' parameter }, }, }; @@ -238,7 +237,7 @@ template: "Search for '{{keywords}}' in {{local_path}}. Also consider: {{general expect(result.isError).toBe(true); const errorMessage = result.content[0]!.text; - expect(errorMessage).toContain("keywords"); + expect(errorMessage).toContain("pattern"); expect(errorMessage).toContain("required"); }); }); @@ -251,8 +250,7 @@ template: "Search for '{{keywords}}' in {{local_path}}. Also consider: {{general name: "search_docs", arguments: { docset_id: "web-source-docs", - keywords: "simulates", - generalized_keywords: "content documentation", + pattern: "simulates", }, }, }; diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index 2a58f68..a9afa0a 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -25,17 +25,15 @@ import { initDocset } from "@codemcp/knowledge-content-loader"; import { existsSync } from "node:fs"; import { resolve, dirname } from "node:path"; -/** Shared keywords parameter description advertised to agents */ -const KEYWORDS_DESCRIPTION = - "Primary search terms or concepts you're looking for. " + +/** Shared pattern parameter description advertised to agents */ +const PATTERN_DESCRIPTION = + "The search pattern (regex) to find in the documentation. " + 'Supports full regex syntax (e.g. "log.*Error", "function\\s+\\w+", "auth|login"). ' + "Returns file path, line number, matched line, and surrounding context lines. " + - 'Be specific: "authentication middleware", "useData hook", "sidebar.items".'; - -const GENERALIZED_KEYWORDS_DESCRIPTION = - "Broader synonyms or related terms used as a fallback when the primary keywords " + - 'return no results (e.g. for "authentication" you might include "login|signin|oauth"). ' + - "Also supports regex syntax."; + "⚠️ IMPORTANT: This is a regex pattern, NOT a list of keywords. " + + "Spaces are literal (match a space character). " + + "To search for multiple alternative terms, use | (pipe): " + + 'e.g. "auth|login" matches lines containing either "auth" OR "login".'; /** * Create an agentic knowledge MCP server @@ -200,16 +198,12 @@ After configuring, the tool will show available docsets here.`, description: "The identifier of the docset to search in. (No docsets configured - see tool description for setup instructions)", }, - keywords: { - type: "string", - description: KEYWORDS_DESCRIPTION, - }, - generalized_keywords: { + pattern: { type: "string", - description: GENERALIZED_KEYWORDS_DESCRIPTION, + description: PATTERN_DESCRIPTION, }, }, - required: ["docset_id", "keywords"], + required: ["docset_id", "pattern"], additionalProperties: false, }, }, @@ -276,22 +270,18 @@ After configuring, the tool will show available docsets here.`, description: "Choose the docset to search in.", enum: config.docsets.map((d) => d.id), }, - keywords: { - type: "string", - description: KEYWORDS_DESCRIPTION, - }, - generalized_keywords: { + pattern: { type: "string", - description: GENERALIZED_KEYWORDS_DESCRIPTION, + description: PATTERN_DESCRIPTION, }, context_lines: { type: "number", description: - "Number of lines to show before and after each matching line (default: 0). " + - "Increase to 1–3 when you need surrounding context to understand a match.", + "Number of lines to show before and after each matching line (default: 2). " + + "Set to 0 for just the matching lines, or increase for more context.", }, }, - required: ["docset_id", "keywords"], + required: ["docset_id", "pattern"], additionalProperties: false, }, }, @@ -339,20 +329,18 @@ ${config.docsets.map((d) => `• **${d.id}** (${d.name})`).join("\n")}`, try { switch (name) { case "search_docs": { - const { docset_id, keywords, generalized_keywords, context_lines } = - args as { - docset_id: string; - keywords: string; - generalized_keywords?: string; - context_lines?: number; - }; + const { docset_id, pattern, context_lines } = args as { + docset_id: string; + pattern: string; + context_lines?: number; + }; // Validate required parameters if (!docset_id || typeof docset_id !== "string") { throw new Error("docset_id is required and must be a string"); } - if (!keywords || typeof keywords !== "string") { - throw new Error("keywords is required and must be a string"); + if (!pattern || typeof pattern !== "string") { + throw new Error("pattern is required and must be a string"); } // Load configuration @@ -392,14 +380,12 @@ ${config.docsets.map((d) => `• **${d.id}** (${d.name})`).join("\n")}`, } // Perform the search - const fallbackPattern = generalized_keywords?.trim(); const searchOptions: SearchOptions = {}; - if (fallbackPattern) searchOptions.fallbackPattern = fallbackPattern; if (typeof context_lines === "number") searchOptions.contextLines = context_lines; const result = await searchDocset( absoluteLocalPath, - keywords.trim(), + pattern.trim(), searchOptions, index, ); diff --git a/test/e2e/mcp-protocol-compliance.test.ts b/test/e2e/mcp-protocol-compliance.test.ts index ee6277b..e616ac3 100644 --- a/test/e2e/mcp-protocol-compliance.test.ts +++ b/test/e2e/mcp-protocol-compliance.test.ts @@ -97,27 +97,16 @@ describe("MCP Protocol Compliance E2E Tests", () => { "Choose the docset to search", ); - // keywords parameter (primary search terms) - expect(properties.keywords).toBeDefined(); - expect(properties.keywords.type).toBe("string"); - expect(properties.keywords.description).toContain("Primary search terms"); + // pattern parameter (primary search pattern) + expect(properties.pattern).toBeDefined(); + expect(properties.pattern.type).toBe("string"); + expect(properties.pattern.description).toContain("search pattern"); // description advertises regex support - expect(properties.keywords.description).toMatch(/regex|pattern/i); - - // generalized_keywords parameter (related terms) - expect(properties.generalized_keywords).toBeDefined(); - expect(properties.generalized_keywords.type).toBe("string"); - expect(properties.generalized_keywords.description).toMatch( - /related terms|synonyms|broader/i, - ); - expect(properties.generalized_keywords.description).toMatch( - /fallback|no results|broaden/i, - ); + expect(properties.pattern.description).toMatch(/regex|pattern/i); // Required parameters expect(schema.required).toContain("docset_id"); - expect(schema.required).toContain("keywords"); - expect(schema.required).not.toContain("generalized_keywords"); // Optional + expect(schema.required).toContain("pattern"); }); it("should have correct list_docsets tool schema", async () => { @@ -171,8 +160,7 @@ describe("MCP Protocol Compliance E2E Tests", () => { name: "search_docs", arguments: { docset_id: "test-docs", - keywords: "authentication middleware", - generalized_keywords: "login signin oauth credentials", + pattern: "authentication middleware", }, }); @@ -191,7 +179,7 @@ describe("MCP Protocol Compliance E2E Tests", () => { name: "search_docs", arguments: { docset_id: "api-docs", - keywords: "rate limiting", + pattern: "rate limiting", }, }); @@ -216,7 +204,7 @@ describe("MCP Protocol Compliance E2E Tests", () => { name: "search_docs", arguments: { docset_id: "nonexistent-docset", - keywords: "test", + pattern: "test", }, }); @@ -232,14 +220,14 @@ describe("MCP Protocol Compliance E2E Tests", () => { name: "search_docs", arguments: { docset_id: "test-docs", - // Missing required 'keywords' parameter + // Missing required 'pattern' parameter }, }); expect(result).toBeDefined(); const content = result.content as Array<{ type: string; text: string }>; expect(content[0]?.text).toContain("Error:"); - expect(content[0]?.text).toContain("keywords is required"); + expect(content[0]?.text).toContain("pattern is required"); }); it("should handle invalid tool name", async () => { @@ -274,8 +262,7 @@ describe("MCP Protocol Compliance E2E Tests", () => { name: "search_docs", arguments: { docset_id: "react-docs", - keywords: "useState hook", - generalized_keywords: "state management react hooks", + pattern: "useState hook", }, }); diff --git a/test/utils/e2e-test-setup.ts b/test/utils/e2e-test-setup.ts index 8c6798d..830cb2b 100644 --- a/test/utils/e2e-test-setup.ts +++ b/test/utils/e2e-test-setup.ts @@ -161,11 +161,10 @@ docsets: - type: "local_folder" paths: ["./api"] template: | - Search for '{{keywords}}' in {{docset_name}} ({{docset_description}}). + Search for '{{pattern}}' in {{docset_name}} ({{docset_description}}). **Location**: {{local_path}} - **Primary Terms**: {{keywords}} - **Related Terms**: {{generalized_keywords}} + **Search Pattern**: {{pattern}} Use tools like grep, rg, or find to search for these terms. `, @@ -218,9 +217,8 @@ docsets: - type: "local_folder" paths: ["./react-docs"] template: | - Looking for React information about '{{keywords}}' in {{local_path}}. + Looking for React information about '{{pattern}}' in {{local_path}}. Search for component names, props, hooks, and patterns. - Related concepts: {{generalized_keywords}} Focus on: 1. Component documentation