diff --git a/.changeset/mcp-phase1-tools-improvement.md b/.changeset/mcp-phase1-tools-improvement.md new file mode 100644 index 0000000..640fe5e --- /dev/null +++ b/.changeset/mcp-phase1-tools-improvement.md @@ -0,0 +1,13 @@ +--- +'@prosdevlab/dev-agent': patch +--- + +MCP tools improvement: faster pattern analysis, merged health into status, agent usability + +- `dev_patterns` is 10-30x faster — reads from Antfly index instead of re-scanning with ts-morph +- `dev_health` merged into `dev_status` (use `section="health"`) — 6 tools reduced to 5 +- `dev_patterns` parameter renamed from `query` to `filePath` to prevent LLM misuse +- New `format: "json"` option on `dev_patterns` for token-efficient agent workflows +- All tools now return `suggestion` field on errors for agent recovery guidance +- Removed stale GitHub code from health adapter +- Extracted pure pattern analyzers for testability diff --git a/.claude/scratchpad.md b/.claude/scratchpad.md new file mode 100644 index 0000000..8bbea4b --- /dev/null +++ b/.claude/scratchpad.md @@ -0,0 +1,22 @@ +# Scratchpad + +## Known Limitations + +- **`getDocsByFilePath` fetches all docs client-side (capped at 5k).** Uses `getAll(limit: 5000)` + exact path filter. Fine for single repos (dev-agent has ~2,200 docs). Won't scale to monorepos with 50k+ files. Future fix: server-side path filter in Antfly SDK. + +## Open Questions + +- Can Antfly SDK support server-side path filtering? Would eliminate the 5k doc cap in `getDocsByFilePath`. Worth raising with Antfly team after MCP Phase 1 ships. + +## Future Work + +- Antfly SDK: server-side path filter for `getDocsByFilePath` (eliminates 5k cap) +- `dev_patterns format: "json"` for token-efficient agent output (MCP Phase 1, Part 1.4) +- ast-grep as optional dep for pattern analysis (MCP Phase 1, Part 1.5) +- PageRank for `dev_map` hot paths (MCP Phase 1, Part 1.6) +- E2E tests in CI — blocked on Antfly memory requirements vs GitHub runner limits (7GB) + +## Notes + +- Both pattern analysis paths (index vs scan) must use the same pure extractors from 1.1 to avoid drift. Test this explicitly. +- Log which path is used (index vs scanner) at debug level so we can verify the fast path fires in production. diff --git a/CLAUDE.md b/CLAUDE.md index 5f95ba8..c97e886 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,7 +23,7 @@ Everything runs on your machine. No data leaves. packages/ core/ # Scanner (ts-morph, tree-sitter), vector storage (Antfly), services cli/ # Commander.js CLI — dev index, dev mcp install, etc. - mcp-server/ # MCP server with 6 built-in adapters + mcp-server/ # MCP server with 5 built-in adapters subagents/ # Coordinator, explorer, planner, PR agents integrations/ # Claude Code, VS Code, Cursor logger/ # @prosdevlab/kero — centralized logging @@ -139,16 +139,15 @@ See `.claude/da-plans/README.md` for status and format details. --- -## MCP tools (6 adapters) +## MCP tools (5 adapters) | Tool | Purpose | |------|---------| | `dev_search` | Hybrid code search — BM25 + vector + RRF (use FIRST for conceptual queries) | | `dev_refs` | Find callers/callees of functions | | `dev_map` | Codebase structure with change frequency | -| `dev_patterns` | File pattern analysis (similar code, error handling, types) | -| `dev_status` | Repository indexing status + Antfly stats + watcher status | -| `dev_health` | Server health checks (Antfly connectivity) | +| `dev_patterns` | File pattern analysis (similar code, error handling, types). Takes `filePath`, not `query`. | +| `dev_status` | Repository indexing status + Antfly stats + health checks (`section="health"`) | --- diff --git a/PLAN.md b/PLAN.md index 7620575..e1bb8b3 100644 --- a/PLAN.md +++ b/PLAN.md @@ -47,7 +47,7 @@ Dev-agent provides semantic code search, codebase intelligence, and GitHub integ | `dev_patterns` - File analysis | ✅ Done | MCP adapter | | `dev_plan` - Issue planning | ✅ Done | MCP adapter | | `dev_gh` - GitHub search | ✅ Done | MCP adapter | -| `dev_health` - Health checks | ✅ Done | MCP adapter | +| `dev_health` - Health checks | ✅ Merged into `dev_status` | MCP adapter | | Cursor integration | ✅ Done | CLI command | | Claude Code integration | ✅ Done | CLI command | | Rate limiting (token bucket) | ✅ Done | MCP server | diff --git a/README.md b/README.md index 833ef96..b701945 100644 --- a/README.md +++ b/README.md @@ -10,14 +10,13 @@ ## What it does -dev-agent indexes your codebase and provides 6 MCP tools to AI assistants. Instead of grepping through files, they can ask conceptual questions like "where do we handle authentication?" +dev-agent indexes your codebase and provides 5 MCP tools to AI assistants. Instead of grepping through files, they can ask conceptual questions like "where do we handle authentication?" - `dev_search` — Hybrid search (BM25 + vector + RRF) — returns code snippets, not just paths - `dev_refs` — Find callers/callees of any function - `dev_map` — Codebase structure with hot paths (most referenced files) - `dev_patterns` — Compare coding patterns against similar files -- `dev_status` — Repository indexing status and health -- `dev_health` — Server health checks +- `dev_status` — Repository indexing status, health checks, and Antfly stats ## Quick Start @@ -110,13 +109,9 @@ Compare a file's coding patterns (imports, error handling, type coverage, testin Analyze patterns in src/auth/middleware.ts ``` -### `dev_status` — Repository Status +### `dev_status` — Repository Status & Health -Indexing status, document counts, Antfly stats, and file watcher state. - -### `dev_health` — Health Checks - -Server health, Antfly connectivity, and repository access. +Indexing status, document counts, Antfly stats, file watcher state, and health checks (`section="health"`). ## Supported Languages @@ -155,7 +150,7 @@ pnpm lint packages/ core/ # Scanner, vector storage, indexer, services cli/ # Commander.js CLI - mcp-server/ # MCP server with 6 tool adapters + mcp-server/ # MCP server with 5 tool adapters subagents/ # Explorer, planner, PR agents integrations/ # Claude Code, VS Code, Cursor logger/ # @prosdevlab/kero centralized logging diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index 04a068d..d765dbf 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -252,7 +252,7 @@ killall dev 2. **Check health:** ``` - Use dev_health tool to see rate limit status + Use dev_status section="health" tool to see rate limit status ``` 3. **If persistent:** @@ -446,7 +446,7 @@ Failed to fetch issues: spawnSync /bin/sh ENOBUFS 3. **Monitor health:** ``` - dev_health --verbose + dev_status section="health" format="verbose" ``` --- @@ -466,7 +466,7 @@ Failed to fetch issues: spawnSync /bin/sh ENOBUFS **Diagnosis:** ``` -Use dev_health tool to check component status +Use dev_status section="health" tool to check component status ``` **Common causes:** @@ -505,7 +505,7 @@ Check the tool's input schema: 3. Check server health: ``` - dev_health + dev_status section="health" ``` --- @@ -573,9 +573,9 @@ claude mcp add --env LOG_LEVEL=debug dev-agent -- dev mcp start ### Check component health ``` -Use dev_health tool with verbose flag: +Use dev_status section="health" tool with verbose flag: -dev_health --verbose +dev_status section="health" format="verbose" Shows: - Vector storage status @@ -596,7 +596,7 @@ dev mcp start --verbose # In another terminal, send test message echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | dev mcp start -# Should list all 6 tools: dev_search, dev_refs, dev_map, dev_patterns, dev_status, dev_health +# Should list all 5 tools: dev_search, dev_refs, dev_map, dev_patterns, dev_status ``` ### Inspect storage @@ -666,7 +666,7 @@ If you encounter a bug: 2. **Get diagnostics:** ``` - Use dev_health --verbose + Use dev_status section="health" format="verbose" ``` 3. **Enable debug logs:** @@ -720,7 +720,7 @@ dev index dev_status # Verify health -dev_health +dev_status section="health" ``` ### Debugging search quality @@ -855,7 +855,7 @@ dev index ## Health Check Guide -The `dev_health` tool provides comprehensive diagnostics: +The `dev_status section="health"` tool provides comprehensive diagnostics: ### Interpreting Health Status @@ -919,7 +919,7 @@ du -sh ~/.dev-agent/indexes/ - After major code changes (adding features, refactoring) - Weekly for active projects - Monthly for stable projects -- Use `dev_health` to check if index is stale +- Use `dev_status section="health"` to check if index is stale ### Q: Can multiple AI tools use dev-agent simultaneously? @@ -942,7 +942,7 @@ du -sh ~/.dev-agent/indexes/ 2. **Use health check:** ``` - dev_health --verbose + dev_status section="health" format="verbose" ``` 3. **Enable debug logging:** @@ -953,7 +953,7 @@ du -sh ~/.dev-agent/indexes/ Include: - `dev --version` -- `dev_health --verbose` output +- `dev_status section="health" format="verbose"` output - Steps to reproduce - Expected vs actual behavior - Debug logs (if applicable) diff --git a/examples/README.md b/examples/README.md index 86bb7e0..106e5a3 100644 --- a/examples/README.md +++ b/examples/README.md @@ -150,16 +150,18 @@ dev_status: --- -### `dev_health` - Health Check +### Health Check (via `dev_status`) Diagnose issues: ``` -dev_health +dev_status: + section: health # Verbose output -dev_health: - verbose: true +dev_status: + section: health + format: verbose ``` --- @@ -230,7 +232,7 @@ dev_health: dev index # Check health -dev_health +dev_status section="health" ``` --- @@ -292,7 +294,7 @@ dev mcp install --cursor - Reduce `limit` - Use `compact` format -- Check `dev_health` +- Check `dev_status section="health"` --- diff --git a/packages/cli/src/commands/mcp.ts b/packages/cli/src/commands/mcp.ts index b3553e4..5a5ae4c 100644 --- a/packages/cli/src/commands/mcp.ts +++ b/packages/cli/src/commands/mcp.ts @@ -13,7 +13,6 @@ import { SearchService, } from '@prosdevlab/dev-agent-core'; import { - HealthAdapter, InspectAdapter, MapAdapter, MCPServer, @@ -49,9 +48,9 @@ Setup: 2. Install MCP integration: dev mcp install --cursor 3. Restart Cursor to activate -Available Tools (6): +Available Tools (5): dev_search, dev_status, dev_patterns, - dev_health, dev_refs, dev_map + dev_refs, dev_map ` ) .addCommand( @@ -121,11 +120,6 @@ Available Tools (6): defaultFormat: 'compact', }); - const healthAdapter = new HealthAdapter({ - repositoryPath, - vectorStorePath: vectors, - }); - const refsAdapter = new RefsAdapter({ searchService, defaultLimit: 20, @@ -138,7 +132,7 @@ Available Tools (6): defaultTokenBudget: 2000, }); - // Create MCP server with 6 adapters + // Create MCP server with 5 adapters (health merged into status) const server = new MCPServer({ serverInfo: { name: 'dev-agent', @@ -149,14 +143,7 @@ Available Tools (6): logLevel: logLevel as 'debug' | 'info' | 'warn' | 'error', }, transport: options.transport === 'stdio' ? 'stdio' : undefined, - adapters: [ - searchAdapter, - statusAdapter, - inspectAdapter, - healthAdapter, - refsAdapter, - mapAdapter, - ], + adapters: [searchAdapter, statusAdapter, inspectAdapter, refsAdapter, mapAdapter], }); // Handle graceful shutdown @@ -174,9 +161,7 @@ Available Tools (6): await server.start(); logger.info(chalk.green('MCP server started successfully!')); - logger.info( - 'Available tools: dev_search, dev_status, dev_patterns, dev_health, dev_refs, dev_map' - ); + logger.info('Available tools: dev_search, dev_status, dev_patterns, dev_refs, dev_map'); if (options.transport === 'stdio') { logger.info('Server running on stdio transport (for AI tools)'); diff --git a/packages/core/src/services/__tests__/pattern-analysis-service.test.ts b/packages/core/src/services/__tests__/pattern-analysis-service.test.ts index a2717fc..6092f77 100644 --- a/packages/core/src/services/__tests__/pattern-analysis-service.test.ts +++ b/packages/core/src/services/__tests__/pattern-analysis-service.test.ts @@ -7,8 +7,259 @@ import * as fs from 'node:fs/promises'; import * as os from 'node:os'; import * as path from 'node:path'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { PatternAnalysisService } from '../pattern-analysis-service'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { + extractErrorHandlingFromContent, + extractImportStyleFromContent, + extractTypeCoverageFromSignatures, + PatternAnalysisService, +} from '../pattern-analysis-service'; + +// ======================================================================== +// Pure Pattern Extractors (no I/O) +// ======================================================================== + +describe('Pure Pattern Extractors', () => { + describe('extractImportStyleFromContent', () => { + it('should detect ESM imports', () => { + const content = 'import { foo } from "./bar";\nimport * as baz from "baz";'; + const result = extractImportStyleFromContent(content); + expect(result).toEqual({ style: 'esm', importCount: 2 }); + }); + + it('should detect CJS requires', () => { + const content = 'const foo = require("bar");\nconst baz = require("baz");'; + const result = extractImportStyleFromContent(content); + expect(result).toEqual({ style: 'cjs', importCount: 2 }); + }); + + it('should detect mixed imports', () => { + const content = 'import { foo } from "./bar";\nconst baz = require("baz");'; + const result = extractImportStyleFromContent(content); + expect(result).toEqual({ style: 'mixed', importCount: 2 }); + }); + + it('should return unknown for no imports', () => { + const content = 'const x = 1;'; + const result = extractImportStyleFromContent(content); + expect(result).toEqual({ style: 'unknown', importCount: 0 }); + }); + }); + + describe('extractErrorHandlingFromContent', () => { + it('should detect throw style', () => { + const content = 'throw new Error("oops");\nthrow new TypeError("bad");'; + const result = extractErrorHandlingFromContent(content); + expect(result.style).toBe('throw'); + }); + + it('should detect result style', () => { + const content = 'function foo(): Result { return { ok: true, value: "x" }; }'; + const result = extractErrorHandlingFromContent(content); + expect(result.style).toBe('result'); + }); + + it('should detect mixed style', () => { + const content = 'throw new Error("oops");\nfunction foo(): Result {}'; + const result = extractErrorHandlingFromContent(content); + expect(result.style).toBe('mixed'); + }); + + it('should return unknown for no error handling', () => { + const content = 'const x = 1;'; + const result = extractErrorHandlingFromContent(content); + expect(result.style).toBe('unknown'); + }); + }); + + describe('extractTypeCoverageFromSignatures', () => { + it('should detect full coverage', () => { + const signatures = ['function foo(x: string): number', 'function bar(y: boolean): void']; + const result = extractTypeCoverageFromSignatures(signatures); + expect(result.coverage).toBe('full'); + expect(result.annotatedCount).toBe(2); + expect(result.totalCount).toBe(2); + }); + + it('should detect partial coverage', () => { + const signatures = ['function foo(x: string): number', 'function bar(y)']; + const result = extractTypeCoverageFromSignatures(signatures); + expect(result.coverage).toBe('partial'); + }); + + it('should detect minimal coverage', () => { + const signatures = ['function foo(x)', 'function bar(y)', 'function baz(z: string): number']; + const result = extractTypeCoverageFromSignatures(signatures); + expect(result.coverage).toBe('minimal'); + expect(result.annotatedCount).toBe(1); + expect(result.totalCount).toBe(3); + }); + + it('should return none for no signatures', () => { + const result = extractTypeCoverageFromSignatures([]); + expect(result.coverage).toBe('none'); + expect(result.annotatedCount).toBe(0); + expect(result.totalCount).toBe(0); + }); + + it('should return none when no signatures have types', () => { + const signatures = ['function foo(x)', 'function bar(y)']; + const result = extractTypeCoverageFromSignatures(signatures); + expect(result.coverage).toBe('none'); + }); + }); +}); + +// ======================================================================== +// analyzeFileFromIndex (index-based, no ts-morph) +// ======================================================================== + +describe('analyzeFileFromIndex', () => { + let tempDir: string; + let service: PatternAnalysisService; + + beforeEach(async () => { + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'analyze-from-index-')); + service = new PatternAnalysisService({ repositoryPath: tempDir }); + }); + + afterEach(async () => { + await fs.rm(tempDir, { recursive: true, force: true }); + }); + + it('should extract patterns from indexed metadata', async () => { + await fs.mkdir(path.join(tempDir, 'src'), { recursive: true }); + await fs.writeFile( + path.join(tempDir, 'src/test.ts'), + 'import { foo } from "./bar";\n\nexport function test(): string {\n throw new Error("oops");\n return "hello";\n}\n' + ); + + const indexedDocs = [ + { + id: 'src/test.ts:test:3', + score: 0.9, + metadata: { + path: 'src/test.ts', + type: 'function', + name: 'test', + signature: 'function test(): string', + exported: true, + }, + }, + ]; + + const result = await service.analyzeFileFromIndex('src/test.ts', indexedDocs); + expect(result.importStyle.style).toBe('esm'); + expect(result.typeAnnotations.coverage).toBe('full'); + expect(result.fileSize.lines).toBe(7); + expect(result.errorHandling.style).toBe('throw'); + }); + + it('should handle files with no indexed docs', async () => { + await fs.writeFile(path.join(tempDir, 'empty.ts'), 'const x = 1;\n'); + const result = await service.analyzeFileFromIndex('empty.ts', []); + expect(result.typeAnnotations.coverage).toBe('none'); + expect(result.typeAnnotations.totalCount).toBe(0); + }); + + it('should handle deleted files gracefully (ENOENT)', async () => { + const result = await service.analyzeFileFromIndex('nonexistent.ts', []); + expect(result.fileSize.lines).toBe(0); + expect(result.fileSize.bytes).toBe(0); + expect(result.importStyle.style).toBe('unknown'); + expect(result.errorHandling.style).toBe('unknown'); + }); +}); + +// ======================================================================== +// comparePatterns with vectorStorage (fast path) +// ======================================================================== + +describe('comparePatterns with vectorStorage', () => { + let tempDir: string; + + beforeEach(async () => { + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'compare-index-')); + }); + + afterEach(async () => { + await fs.rm(tempDir, { recursive: true, force: true }); + }); + + it('should use index path when vectorStorage is provided', async () => { + await fs.mkdir(path.join(tempDir, 'src'), { recursive: true }); + await fs.writeFile( + path.join(tempDir, 'src/target.ts'), + 'import { x } from "./y";\nexport function target(): string { throw new Error("oops"); }\n' + ); + await fs.writeFile( + path.join(tempDir, 'src/similar.ts'), + 'import { a } from "./b";\nexport function similar(): number { throw new Error("bad"); }\n' + ); + + const mockGetDocsByFilePath = vi.fn().mockResolvedValue( + new Map([ + [ + 'src/target.ts', + [ + { + id: 'src/target.ts:target:2', + score: 0.9, + metadata: { + path: 'src/target.ts', + type: 'function', + signature: 'function target(): string', + }, + }, + ], + ], + [ + 'src/similar.ts', + [ + { + id: 'src/similar.ts:similar:2', + score: 0.9, + metadata: { + path: 'src/similar.ts', + type: 'function', + signature: 'function similar(): number', + }, + }, + ], + ], + ]) + ); + + const mockVectorStorage = { getDocsByFilePath: mockGetDocsByFilePath } as any; + const serviceWithIndex = new PatternAnalysisService({ + repositoryPath: tempDir, + vectorStorage: mockVectorStorage, + }); + + const result = await serviceWithIndex.comparePatterns('src/target.ts', ['src/similar.ts']); + expect(mockGetDocsByFilePath).toHaveBeenCalledWith(['src/target.ts', 'src/similar.ts']); + expect(result.importStyle.yourFile).toBe('esm'); + expect(result.typeAnnotations.yourFile).toBe('full'); + }); + + it('should handle empty results from index gracefully', async () => { + await fs.writeFile(path.join(tempDir, 'solo.ts'), 'const x = 1;\n'); + + const mockGetDocsByFilePath = vi.fn().mockResolvedValue(new Map()); + const mockVectorStorage = { getDocsByFilePath: mockGetDocsByFilePath } as any; + const serviceWithIndex = new PatternAnalysisService({ + repositoryPath: tempDir, + vectorStorage: mockVectorStorage, + }); + + const result = await serviceWithIndex.comparePatterns('solo.ts', []); + expect(result.fileSize.yourFile).toBeGreaterThan(0); + }); +}); + +// ======================================================================== +// PatternAnalysisService (integration — uses file I/O) +// ======================================================================== describe('PatternAnalysisService', () => { let tempDir: string; diff --git a/packages/core/src/services/pattern-analysis-service.ts b/packages/core/src/services/pattern-analysis-service.ts index 6b3d9c7..e85b6d4 100644 --- a/packages/core/src/services/pattern-analysis-service.ts +++ b/packages/core/src/services/pattern-analysis-service.ts @@ -10,6 +10,7 @@ import * as path from 'node:path'; import { scanRepository } from '../scanner'; import type { Document } from '../scanner/types'; import { findTestFile, isTestFile } from '../utils/test-utils'; +import type { SearchResult } from '../vector/types'; import type { ErrorHandlingComparison, ErrorHandlingPattern, @@ -43,6 +44,64 @@ export type { TypeAnnotationPattern, } from './pattern-analysis-types'; +// ======================================================================== +// Pure Pattern Extractors — no I/O, fully testable +// ======================================================================== + +/** + * Extract import style from raw file content. + */ +export function extractImportStyleFromContent(content: string): ImportStylePattern { + const esmImports = content.match(/^import\s/gm) || []; + const cjsImports = content.match(/require\s*\(/g) || []; + const hasESM = esmImports.length > 0; + const hasCJS = cjsImports.length > 0; + + if (!hasESM && !hasCJS) return { style: 'unknown', importCount: 0 }; + + const importCount = esmImports.length + cjsImports.length; + const style: ImportStylePattern['style'] = hasESM && hasCJS ? 'mixed' : hasESM ? 'esm' : 'cjs'; + return { style, importCount }; +} + +/** + * Extract error handling pattern from raw file content. + */ +export function extractErrorHandlingFromContent(content: string): ErrorHandlingPattern { + const counts = { + throw: [...content.matchAll(/throw\s+new\s+\w*Error/g)].length, + result: [...content.matchAll(/Result<|{\s*ok:\s*(true|false)/g)].length, + errorReturn: [...content.matchAll(/\)\s*:\s*\([^)]*,\s*error\)/g)].length, + }; + const total = counts.throw + counts.result + counts.errorReturn; + if (total === 0) return { style: 'unknown', examples: [] }; + + const max = Math.max(counts.throw, counts.result, counts.errorReturn); + const hasMultiple = Object.values(counts).filter((c) => c > 0).length > 1; + let style: ErrorHandlingPattern['style'] = 'unknown'; + if (hasMultiple) style = 'mixed'; + else if (counts.throw === max) style = 'throw'; + else if (counts.result === max) style = 'result'; + else if (counts.errorReturn === max) style = 'error-return'; + return { style, examples: [] }; +} + +/** + * Extract type coverage from function/method signatures. + */ +export function extractTypeCoverageFromSignatures(signatures: string[]): TypeAnnotationPattern { + if (signatures.length === 0) return { coverage: 'none', annotatedCount: 0, totalCount: 0 }; + + const annotated = signatures.filter((sig) => /(\)|=>)\s*:\s*\w+/.test(sig)); + const ratio = annotated.length / signatures.length; + let coverage: TypeAnnotationPattern['coverage']; + if (ratio >= 0.9) coverage = 'full'; + else if (ratio >= 0.5) coverage = 'partial'; + else if (ratio > 0) coverage = 'minimal'; + else coverage = 'none'; + return { coverage, annotatedCount: annotated.length, totalCount: signatures.length }; +} + /** * Pattern Analysis Service * @@ -71,45 +130,88 @@ export class PatternAnalysisService { } /** - * Compare patterns between target file and similar files + * Analyze file patterns using indexed metadata (fast — no ts-morph). * - * OPTIMIZED: Batch scans all files in one pass to avoid repeated ts-morph initialization + * Reads signatures from the Antfly index, content from disk (for line count + * and error handling regex). Falls back gracefully on ENOENT (deleted file). + */ + async analyzeFileFromIndex(filePath: string, indexedDocs: SearchResult[]): Promise { + const fullPath = path.join(this.config.repositoryPath, filePath); + + let content = ''; + let bytes = 0; + let lines = 0; + try { + const [fileContent, stat] = await Promise.all([ + fs.readFile(fullPath, 'utf-8'), + fs.stat(fullPath), + ]); + content = fileContent; + bytes = stat.size; + lines = content.split('\n').length; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; + // File deleted between index and analysis — return empty patterns + } + + const testing = await this.analyzeTesting(filePath); + const signatures = indexedDocs + .filter((d) => d.metadata.type === 'function' || d.metadata.type === 'method') + .map((d) => (d.metadata.signature as string) || '') + .filter(Boolean); + + return { + fileSize: { lines, bytes }, + testing, + importStyle: extractImportStyleFromContent(content), + errorHandling: extractErrorHandlingFromContent(content), + typeAnnotations: extractTypeCoverageFromSignatures(signatures), + }; + } + + /** + * Compare patterns between target file and similar files * - * @param targetFile - Target file to analyze - * @param similarFiles - Array of similar file paths - * @returns Pattern comparison results + * Uses Antfly index when vectorStorage is available (fast path, ~100ms). + * Falls back to ts-morph scanning when not (tests, offline). */ async comparePatterns(targetFile: string, similarFiles: string[]): Promise { - // OPTIMIZATION: Batch scan all files at once (5-10x faster than individual scans) const allFiles = [targetFile, ...similarFiles]; - const batchResult = await scanRepository({ - repoRoot: this.config.repositoryPath, - include: allFiles, - }); - - // Group documents by file for fast lookup - const docsByFile = new Map(); - for (const doc of batchResult.documents) { - const file = doc.metadata.file; - if (!docsByFile.has(file)) { - docsByFile.set(file, []); - } - const docs = docsByFile.get(file); - if (docs) { - docs.push(doc); + let targetPatterns: FilePatterns; + let similarPatterns: FilePatterns[]; + + if (this.config.vectorStorage) { + // FAST PATH: read from Antfly index + // Fast path: index-based analysis (~100ms vs 1-3s) + const docsByFile = await this.config.vectorStorage.getDocsByFilePath(allFiles); + + targetPatterns = await this.analyzeFileFromIndex( + targetFile, + docsByFile.get(targetFile) || [] + ); + similarPatterns = await Promise.all( + similarFiles.map((f) => this.analyzeFileFromIndex(f, docsByFile.get(f) || [])) + ); + } else { + // FALLBACK: scan files with ts-morph + // Fallback: ts-morph scan (for tests/offline) + const batchResult = await scanRepository({ + repoRoot: this.config.repositoryPath, + include: allFiles, + }); + + const docsByFile = new Map(); + for (const doc of batchResult.documents) { + const file = doc.metadata.file; + if (!docsByFile.has(file)) docsByFile.set(file, []); + docsByFile.get(file)!.push(doc); } - } - - // Analyze target file with cached documents - const targetPatterns = await this.analyzeFileWithDocs( - targetFile, - docsByFile.get(targetFile) || [] - ); - // Analyze similar files in parallel with cached documents - const similarPatterns = await Promise.all( - similarFiles.map((f) => this.analyzeFileWithDocs(f, docsByFile.get(f) || [])) - ); + targetPatterns = await this.analyzeFileWithDocs(targetFile, docsByFile.get(targetFile) || []); + similarPatterns = await Promise.all( + similarFiles.map((f) => this.analyzeFileWithDocs(f, docsByFile.get(f) || [])) + ); + } return { fileSize: this.compareFileSize( @@ -136,46 +238,37 @@ export class PatternAnalysisService { } /** - * Analyze file patterns using pre-scanned documents (faster) - * - * @param filePath - Relative path from repository root - * @param documents - Pre-scanned documents for this file - * @returns Pattern analysis results + * Analyze file patterns using pre-scanned documents (fallback path). */ private async analyzeFileWithDocs( filePath: string, documents: Document[] ): Promise { - // Step 1: Get file stats and content const fullPath = path.join(this.config.repositoryPath, filePath); - const [stat, content] = await Promise.all([fs.stat(fullPath), fs.readFile(fullPath, 'utf-8')]); + const [content, stat, testing] = await Promise.all([ + fs.readFile(fullPath, 'utf-8'), + fs.stat(fullPath), + this.analyzeTesting(filePath), + ]); - const lines = content.split('\n').length; + const signatures = documents + .filter((d) => d.type === 'function' || d.type === 'method') + .map((d) => d.metadata.signature || '') + .filter(Boolean); - // Step 2: Extract all patterns (using cached documents) return { - fileSize: { - lines, - bytes: stat.size, - }, - testing: await this.analyzeTesting(filePath), - importStyle: await this.analyzeImportsFromFile(filePath, documents), - errorHandling: this.analyzeErrorHandling(content), - typeAnnotations: this.analyzeTypes(documents), + fileSize: { lines: content.split('\n').length, bytes: stat.size }, + testing, + importStyle: extractImportStyleFromContent(content), + errorHandling: extractErrorHandlingFromContent(content), + typeAnnotations: extractTypeCoverageFromSignatures(signatures), }; } - // ======================================================================== - // Pattern Extractors (MVP: 5 core patterns) - // ======================================================================== - /** * Analyze test coverage for a file - * - * Checks for co-located test files (*.test.*, *.spec.*) */ private async analyzeTesting(filePath: string): Promise { - // Skip if already a test file if (isTestFile(filePath)) { return { hasTest: false }; } @@ -187,137 +280,6 @@ export class PatternAnalysisService { }; } - /** - * Analyze import style from documents - * - * Always uses content analysis for reliability (scanner may not extract imports from all files). - */ - private async analyzeImportsFromFile( - filePath: string, - _documents: Document[] - ): Promise { - // Always analyze raw content for maximum reliability - // Scanner extraction can be incomplete for test files or unusual syntax - const fullPath = path.join(this.config.repositoryPath, filePath); - const content = await fs.readFile(fullPath, 'utf-8'); - - return this.analyzeImportsFromContent(content); - } - - /** - * Analyze imports from raw file content (fallback method) - */ - private analyzeImportsFromContent(content: string): ImportStylePattern { - // Count actual imports (not exports) - const esmImports = content.match(/^import\s/gm) || []; - const cjsImports = content.match(/require\s*\(/g) || []; - - const hasESM = esmImports.length > 0; - const hasCJS = cjsImports.length > 0; - - if (!hasESM && !hasCJS) { - return { style: 'unknown', importCount: 0 }; - } - - const importCount = esmImports.length + cjsImports.length; - - let style: ImportStylePattern['style']; - if (hasESM && hasCJS) { - style = 'mixed'; - } else if (hasESM) { - style = 'esm'; - } else { - style = 'cjs'; - } - - return { style, importCount }; - } - - /** - * Analyze error handling patterns in file content - * - * Detects: throw, Result, error returns (Go style) - */ - private analyzeErrorHandling(content: string): ErrorHandlingPattern { - const patterns = { - throw: /throw\s+new\s+\w*Error/g, - result: /Result<|{\s*ok:\s*(true|false)/g, - errorReturn: /\)\s*:\s*\([^)]*,\s*error\)/g, // Go: (val, error) - }; - - const matches = { - throw: [...content.matchAll(patterns.throw)], - result: [...content.matchAll(patterns.result)], - errorReturn: [...content.matchAll(patterns.errorReturn)], - }; - - const counts = { - throw: matches.throw.length, - result: matches.result.length, - errorReturn: matches.errorReturn.length, - }; - - // Determine primary style - const total = counts.throw + counts.result + counts.errorReturn; - if (total === 0) { - return { style: 'unknown', examples: [] }; - } - - const max = Math.max(counts.throw, counts.result, counts.errorReturn); - const hasMultiple = Object.values(counts).filter((c) => c > 0).length > 1; - - let style: ErrorHandlingPattern['style'] = 'unknown'; - if (hasMultiple) { - style = 'mixed'; - } else if (counts.throw === max) { - style = 'throw'; - } else if (counts.result === max) { - style = 'result'; - } else if (counts.errorReturn === max) { - style = 'error-return'; - } - - return { style, examples: [] }; - } - - /** - * Analyze type annotation coverage from documents - * - * Checks function/method signatures for explicit types. - */ - private analyzeTypes(documents: Document[]): TypeAnnotationPattern { - const functions = documents.filter((d) => d.type === 'function' || d.type === 'method'); - - if (functions.length === 0) { - return { coverage: 'none', annotatedCount: 0, totalCount: 0 }; - } - - // Check if signatures have explicit return types (contains ': ' after params) - const annotated = functions.filter((d) => { - const sig = d.metadata.signature || ''; - // Look for ': Type' pattern after closing paren or arrow - return /(\)|=>)\s*:\s*\w+/.test(sig); - }); - - const coverage = annotated.length / functions.length; - let coverageLevel: TypeAnnotationPattern['coverage']; - if (coverage >= 0.9) { - coverageLevel = 'full'; - } else if (coverage >= 0.5) { - coverageLevel = 'partial'; - } else if (coverage > 0) { - coverageLevel = 'minimal'; - } else { - coverageLevel = 'none'; - } - - return { - coverage: coverageLevel, - annotatedCount: annotated.length, - totalCount: functions.length, - }; - } - // ======================================================================== // Pattern Comparisons // ======================================================================== diff --git a/packages/core/src/services/pattern-analysis-types.ts b/packages/core/src/services/pattern-analysis-types.ts index 52099b8..e035275 100644 --- a/packages/core/src/services/pattern-analysis-types.ts +++ b/packages/core/src/services/pattern-analysis-types.ts @@ -122,4 +122,5 @@ export interface PatternComparison { */ export interface PatternAnalysisConfig { repositoryPath: string; + vectorStorage?: import('../vector/index.js').VectorStorage; } diff --git a/packages/core/src/vector/index.ts b/packages/core/src/vector/index.ts index 2f70f6a..4108b12 100644 --- a/packages/core/src/vector/index.ts +++ b/packages/core/src/vector/index.ts @@ -110,6 +110,38 @@ export class VectorStorage { return this.store.getAll(options); } + /** + * Get indexed documents grouped by file path. + * + * Uses getAll with a capped limit + client-side exact path filter. + * More reliable than BM25 search which tokenizes paths unpredictably. + * + * Note: Fetches up to 5,000 docs client-side. Fine for single repos, + * won't scale to monorepos with 50k+ files. See .claude/scratchpad.md. + */ + async getDocsByFilePath(filePaths: string[]): Promise> { + this.assertReady(); + const DOC_LIMIT = 5000; + const pathSet = new Set(filePaths); + const allDocs = await this.getAll({ limit: DOC_LIMIT }); + + if (allDocs.length >= DOC_LIMIT) { + console.error( + `[dev-agent] Warning: getDocsByFilePath hit ${DOC_LIMIT} doc limit. Some files may be missing.` + ); + } + + const byFile = new Map(); + for (const doc of allDocs) { + const docPath = doc.metadata.path as string; + if (pathSet.has(docPath)) { + if (!byFile.has(docPath)) byFile.set(docPath, []); + byFile.get(docPath)!.push(doc); + } + } + return byFile; + } + /** * Get a document by ID */ diff --git a/packages/dev-agent/README.md b/packages/dev-agent/README.md index 747bec3..0f13a77 100644 --- a/packages/dev-agent/README.md +++ b/packages/dev-agent/README.md @@ -39,8 +39,7 @@ When integrated with Cursor or Claude Code, you get 6 powerful tools: - `dev_refs` - Find callers/callees of functions - `dev_map` - Codebase structure with change frequency - `dev_patterns` - File analysis and pattern checking -- `dev_status` - Repository status and health -- `dev_health` - Component health checks +- `dev_status` - Repository status, health checks, and Antfly stats ## Requirements @@ -110,11 +109,11 @@ All processing happens on your machine: # Find authentication-related code dev_search: "JWT token validation middleware" -# Find similar code patterns -dev_patterns: { action: "compare", query: "src/auth/middleware.ts" } +# Analyze coding patterns +dev_patterns: { filePath: "src/auth/middleware.ts" } # Check system health -dev_health: verbose +dev_status: { section: "health" } ``` ## Support diff --git a/packages/mcp-server/CURSOR_SETUP.md b/packages/mcp-server/CURSOR_SETUP.md index 8efd776..048bf28 100644 --- a/packages/mcp-server/CURSOR_SETUP.md +++ b/packages/mcp-server/CURSOR_SETUP.md @@ -66,20 +66,17 @@ Compare src/auth/middleware.ts with similar implementations - `limit`: Number of results (default: 10, for compare action) - `format`: Output format (`compact` or `verbose`) -### `dev_health` - Server Health Check -Check the health of dev-agent MCP server and its components. +### Health Checks (via `dev_status`) + +Use `dev_status` with `section="health"` for server diagnostics: ``` -Check server health +Check server health status ``` -**Parameters:** -- `verbose`: Include detailed diagnostics (default: false) - **Checks:** -- Vector storage (indexed code) -- Repository accessibility -- GitHub index status and age +- Repository access +- Antfly connectivity ## Management Commands diff --git a/packages/mcp-server/bin/dev-agent-mcp.ts b/packages/mcp-server/bin/dev-agent-mcp.ts index 67b47aa..a252e7a 100644 --- a/packages/mcp-server/bin/dev-agent-mcp.ts +++ b/packages/mcp-server/bin/dev-agent-mcp.ts @@ -13,7 +13,6 @@ import { saveMetadata, } from '@prosdevlab/dev-agent-core'; import { - HealthAdapter, InspectAdapter, MapAdapter, RefsAdapter, @@ -268,16 +267,12 @@ async function main() { const inspectAdapter = new InspectAdapter({ repositoryPath, searchService, + vectorStorage: indexer.getVectorStorage(), defaultLimit: 10, defaultThreshold: 0.7, defaultFormat: 'compact', }); - const healthAdapter = new HealthAdapter({ - repositoryPath, - vectorStorePath: filePaths.vectors, - }); - const refsAdapter = new RefsAdapter({ searchService, defaultLimit: 20, @@ -290,7 +285,7 @@ async function main() { defaultTokenBudget: 2000, }); - // Create MCP server with 6 adapters + // Create MCP server with 5 adapters (health merged into status) const server = new MCPServer({ serverInfo: { name: 'dev-agent', @@ -301,14 +296,7 @@ async function main() { logLevel, }, transport: 'stdio', - adapters: [ - searchAdapter, - statusAdapter, - inspectAdapter, - healthAdapter, - refsAdapter, - mapAdapter, - ], + adapters: [searchAdapter, statusAdapter, inspectAdapter, refsAdapter, mapAdapter], }); // Start server diff --git a/packages/mcp-server/src/adapters/__tests__/health-adapter.test.ts b/packages/mcp-server/src/adapters/__tests__/health-adapter.test.ts deleted file mode 100644 index 78de9f0..0000000 --- a/packages/mcp-server/src/adapters/__tests__/health-adapter.test.ts +++ /dev/null @@ -1,369 +0,0 @@ -import * as fs from 'node:fs/promises'; -import * as os from 'node:os'; -import * as path from 'node:path'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { HealthAdapter } from '../built-in/health-adapter'; -import type { AdapterContext, ToolExecutionContext } from '../types'; - -describe('HealthAdapter', () => { - let testDir: string; - let vectorStorePath: string; - let repositoryPath: string; - let githubStatePath: string; - let adapter: HealthAdapter; - let context: AdapterContext; - let execContext: ToolExecutionContext; - - beforeEach(async () => { - // Create temporary directories for testing - testDir = path.join(os.tmpdir(), `health-adapter-test-${Date.now()}`); - vectorStorePath = path.join(testDir, 'vectors'); - repositoryPath = path.join(testDir, 'repo'); - githubStatePath = path.join(testDir, 'github-state.json'); - - await fs.mkdir(testDir, { recursive: true }); - await fs.mkdir(vectorStorePath, { recursive: true }); - await fs.mkdir(repositoryPath, { recursive: true }); - - adapter = new HealthAdapter({ - repositoryPath, - vectorStorePath, - githubStatePath, - }); - - context = { - logger: { - debug: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - }, - config: { - repositoryPath, - }, - }; - - execContext = { - logger: context.logger, - config: context.config, - }; - - await adapter.initialize(context); - }); - - afterEach(async () => { - await adapter.shutdown(); - await fs.rm(testDir, { recursive: true, force: true }); - }); - - describe('Tool Definition', () => { - it('should provide valid tool definition', () => { - const definition = adapter.getToolDefinition(); - - expect(definition.name).toBe('dev_health'); - expect(definition.description).toContain('health status'); - expect(definition.inputSchema.type).toBe('object'); - expect(definition.inputSchema.properties).toHaveProperty('verbose'); - }); - }); - - describe('Health Checks', () => { - it('should report healthy when all components are operational', async () => { - // Setup: Create vector storage with some files - await fs.writeFile(path.join(vectorStorePath, 'data.db'), 'test data'); - - // Setup: Create git repository - await fs.mkdir(path.join(repositoryPath, '.git')); - - // Setup: Create GitHub index - await fs.writeFile( - githubStatePath, - JSON.stringify({ - version: '1.0.0', - lastIndexed: new Date().toISOString(), - items: [{ id: 1 }, { id: 2 }], - }) - ); - - const result = await adapter.execute({}, execContext); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - - // Check formatted string output - expect(result.data).toContain('✅'); - expect(result.data).toContain('HEALTHY'); - expect(result.data).toContain('Vector Storage'); - expect(result.data).toContain('Repository'); - expect(result.data).toContain('Github Index'); - }); - - it('should report degraded when components have warnings', async () => { - // Vector storage is empty (warning) - // Repository exists but no .git (warning) - // No GitHub index - - const result = await adapter.execute({}, execContext); - - expect(result.success).toBe(true); - expect(result.data).toContain('⚠️'); - expect(result.data).toContain('DEGRADED'); - }); - - it('should report unhealthy when components fail', async () => { - // Delete vector storage to cause failure - await fs.rm(vectorStorePath, { recursive: true }); - - const result = await adapter.execute({}, execContext); - - expect(result.success).toBe(true); - expect(result.data).toContain('❌'); - expect(result.data).toContain('UNHEALTHY'); - }); - }); - - describe('Vector Storage Check', () => { - it('should pass when vector storage has data', async () => { - await fs.writeFile(path.join(vectorStorePath, 'index.db'), 'data'); - await fs.writeFile(path.join(vectorStorePath, 'vectors.db'), 'data'); - - const result = await adapter.execute({}, execContext); - - expect(result.success).toBe(true); - expect(result.data).toContain('Vector Storage'); - expect(result.data).toContain('2 files'); - }); - - it('should warn when vector storage is empty', async () => { - const result = await adapter.execute({}, execContext); - - expect(result.success).toBe(true); - expect(result.data).toContain('empty'); - }); - - it('should fail when vector storage does not exist', async () => { - await fs.rm(vectorStorePath, { recursive: true }); - - const result = await adapter.execute({}, execContext); - - expect(result.success).toBe(true); - expect(result.data).toContain('not accessible'); - }); - - it('should include details in verbose mode', async () => { - await fs.writeFile(path.join(vectorStorePath, 'data.db'), 'test'); - - const result = await adapter.execute({ verbose: true }, execContext); - expect(result.success).toBe(true); - // Verbose mode includes more details in the formatted output - expect(result.data).toContain('Vector Storage'); - }); - }); - - describe('Repository Check', () => { - it('should pass when repository is a git repo', async () => { - await fs.mkdir(path.join(repositoryPath, '.git')); - - const result = await adapter.execute({}, execContext); - - expect(result.success).toBe(true); - expect(result.data).toContain('Repository'); - expect(result.data).toContain('Git repository'); - }); - - it('should warn when repository exists but is not a git repo', async () => { - const result = await adapter.execute({}, execContext); - - expect(result.success).toBe(true); - expect(result.data).toContain('not a Git repository'); - }); - - it('should fail when repository does not exist', async () => { - await fs.rm(repositoryPath, { recursive: true }); - - const result = await adapter.execute({}, execContext); - - expect(result.success).toBe(true); - expect(result.data).toContain('not accessible'); - }); - }); - - describe('GitHub Index Check', () => { - it('should pass when index is recent', async () => { - await fs.writeFile( - githubStatePath, - JSON.stringify({ - version: '1.0.0', - lastIndexed: new Date().toISOString(), - items: [{ id: 1 }, { id: 2 }, { id: 3 }], - }) - ); - - const result = await adapter.execute({}, execContext); - - expect(result.success).toBe(true); - expect(result.data).toContain('Github Index'); - expect(result.data).toContain('3 items'); - }); - - it('should warn when index is stale (>24 hours)', async () => { - const yesterday = new Date(Date.now() - 25 * 60 * 60 * 1000); - await fs.writeFile( - githubStatePath, - JSON.stringify({ - version: '1.0.0', - lastIndexed: yesterday.toISOString(), - items: [{ id: 1 }], - }) - ); - - const result = await adapter.execute({}, execContext); - - expect(result.success).toBe(true); - expect(result.data).toContain('Github Index'); - expect(result.data).toContain('old'); - }); - - it('should warn when index file does not exist', async () => { - const result = await adapter.execute({}, execContext); - - expect(result.success).toBe(true); - expect(result.data).toContain('not accessible'); - }); - - it('should not check GitHub index when not configured', async () => { - const adapterWithoutGithub = new HealthAdapter({ - repositoryPath, - vectorStorePath, - }); - - await adapterWithoutGithub.initialize(context); - - const result = await adapterWithoutGithub.execute({}, execContext); - - expect(result.success).toBe(true); - // Github Index section should not appear when not configured - expect(result.data).not.toContain('Github Index'); - - await adapterWithoutGithub.shutdown(); - }); - }); - - describe('Output Formatting', () => { - it('should format uptime correctly', async () => { - // Wait a moment to accumulate uptime - await new Promise((resolve) => setTimeout(resolve, 10)); - - const result = await adapter.execute({}, execContext); - - expect(result.success).toBe(true); - expect(result.data).toContain('Uptime:'); - }); - - it('should include timestamp', async () => { - const result = await adapter.execute({}, execContext); - - expect(result.success).toBe(true); - expect(result.data).toContain('Timestamp:'); - }); - - it('should format component names nicely', async () => { - const result = await adapter.execute({}, execContext); - - expect(result.success).toBe(true); - expect(result.data).toContain('Vector Storage'); - expect(result.data).toContain('Repository'); - }); - - it('should use appropriate emojis', async () => { - await fs.writeFile(path.join(vectorStorePath, 'data.db'), 'test'); - await fs.mkdir(path.join(repositoryPath, '.git')); - - const result = await adapter.execute({}, execContext); - - expect(result.success).toBe(true); - expect(result.data).toContain('✅'); - }); - - it('should include details in verbose mode', async () => { - await fs.writeFile(path.join(vectorStorePath, 'data.db'), 'test'); - - const result = await adapter.execute({ verbose: true }, execContext); - expect(result.success).toBe(true); - - expect(result.data).toContain('Details:'); - }); - - it('should not include details in non-verbose mode', async () => { - await fs.writeFile(path.join(vectorStorePath, 'data.db'), 'test'); - - const result = await adapter.execute({ verbose: false }, execContext); - - expect(result.success).toBe(true); - expect(result.data).not.toContain('Details:'); - }); - }); - - describe('Adapter Health Check Method', () => { - it('should return true when healthy', async () => { - await fs.writeFile(path.join(vectorStorePath, 'data.db'), 'test'); - await fs.mkdir(path.join(repositoryPath, '.git')); - - // Add GitHub index to make it fully healthy - await fs.writeFile( - githubStatePath, - JSON.stringify({ - version: '1.0.0', - lastIndexed: new Date().toISOString(), - items: [{ id: 1 }], - }) - ); - - const healthy = await adapter.healthCheck(); - - expect(healthy).toBe(true); - }); - - it('should return false when unhealthy', async () => { - await fs.rm(vectorStorePath, { recursive: true }); - - const healthy = await adapter.healthCheck(); - - expect(healthy).toBe(false); - }); - }); - - describe('Error Handling', () => { - it('should handle invalid GitHub state JSON', async () => { - await fs.writeFile(githubStatePath, 'invalid json'); - - const result = await adapter.execute({}, execContext); - - expect(result.success).toBe(true); - expect(result.data).toContain('⚠️'); - }); - - it('should handle permission errors gracefully', async () => { - // This test is platform-dependent, so we'll skip it if we can't set permissions - if (process.platform !== 'win32') { - await fs.chmod(vectorStorePath, 0o000); - - const result = await adapter.execute({}, execContext); - - expect(result.success).toBe(true); - expect(result.data).toBeDefined(); - - // Restore permissions for cleanup - await fs.chmod(vectorStorePath, 0o755); - } - }); - }); - - describe('Metadata', () => { - it('should include correct metadata', () => { - expect(adapter.metadata.name).toBe('health-adapter'); - expect(adapter.metadata.version).toBe('1.0.0'); - expect(adapter.metadata.description).toContain('health'); - }); - }); -}); diff --git a/packages/mcp-server/src/adapters/__tests__/inspect-adapter.test.ts b/packages/mcp-server/src/adapters/__tests__/inspect-adapter.test.ts index ec52d20..5d8e4df 100644 --- a/packages/mcp-server/src/adapters/__tests__/inspect-adapter.test.ts +++ b/packages/mcp-server/src/adapters/__tests__/inspect-adapter.test.ts @@ -54,15 +54,21 @@ describe('InspectAdapter', () => { expect(definition.name).toBe('dev_patterns'); expect(definition.description).toContain('pattern'); expect(definition.description).toContain('similar'); - expect(definition.inputSchema.required).toContain('query'); - expect(definition.inputSchema.required).not.toContain('action'); + expect(definition.inputSchema.required).toContain('filePath'); }); - it('should have file path description in query field', () => { + it('should have file path description in filePath field', () => { const definition = adapter.getToolDefinition(); - const queryProp = (definition.inputSchema.properties as any)?.query; + const filePathProp = (definition.inputSchema.properties as any)?.filePath; - expect(queryProp.description.toLowerCase()).toContain('file path'); + expect(filePathProp.description.toLowerCase()).toContain('file path'); + }); + + it('should include negative guidance in description', () => { + const definition = adapter.getToolDefinition(); + + expect(definition.description).toContain('NOT for finding code'); + expect(definition.description).toContain('dev_search'); }); it('should not have an output schema (returns plain markdown)', () => { @@ -74,10 +80,10 @@ describe('InspectAdapter', () => { }); describe('Input Validation', () => { - it('should reject empty query', async () => { + it('should reject empty filePath', async () => { const result = await adapter.execute( { - query: '', + filePath: '', }, mockContext ); @@ -89,7 +95,7 @@ describe('InspectAdapter', () => { it('should reject invalid limit', async () => { const result = await adapter.execute( { - query: 'src/test.ts', + filePath: 'src/test.ts', limit: -1, }, mockContext @@ -111,7 +117,7 @@ describe('InspectAdapter', () => { const result = await adapter.execute( { - query: 'modern-typescript.ts', + filePath: 'modern-typescript.ts', limit: 10, format: 'compact', }, @@ -146,7 +152,7 @@ describe('InspectAdapter', () => { const result = await adapter.execute( { - query: 'modern-typescript.ts', + filePath: 'modern-typescript.ts', }, mockContext ); @@ -181,7 +187,7 @@ describe('InspectAdapter', () => { const result = await adapter.execute( { - query: 'modern-typescript.ts', + filePath: 'modern-typescript.ts', }, mockContext ); @@ -202,7 +208,7 @@ describe('InspectAdapter', () => { const result = await adapter.execute( { - query: 'README.md', + filePath: 'README.md', }, mockContext ); @@ -246,7 +252,7 @@ describe('InspectAdapter', () => { const result = await adapter.execute( { - query: 'modern-typescript.ts', + filePath: 'modern-typescript.ts', limit: 5, }, mockContext @@ -276,7 +282,7 @@ describe('InspectAdapter', () => { const result = await adapter.execute( { - query: 'modern-typescript.ts', + filePath: 'modern-typescript.ts', format: 'compact', }, mockContext @@ -303,7 +309,7 @@ describe('InspectAdapter', () => { const result = await adapter.execute( { - query: 'modern-typescript.ts', + filePath: 'modern-typescript.ts', format: 'verbose', }, mockContext @@ -322,7 +328,7 @@ describe('InspectAdapter', () => { const result = await adapter.execute( { - query: 'modern-typescript.ts', + filePath: 'modern-typescript.ts', }, mockContext ); @@ -336,7 +342,7 @@ describe('InspectAdapter', () => { const result = await adapter.execute( { - query: 'missing-file.ts', + filePath: 'missing-file.ts', }, mockContext ); @@ -350,7 +356,7 @@ describe('InspectAdapter', () => { const result = await adapter.execute( { - query: 'modern-typescript.ts', + filePath: 'modern-typescript.ts', }, mockContext ); @@ -375,7 +381,7 @@ describe('InspectAdapter', () => { const result = await adapter.execute( { - query: 'modern-typescript.ts', + filePath: 'modern-typescript.ts', }, mockContext ); diff --git a/packages/mcp-server/src/adapters/__tests__/mcp-tools-regression.test.ts b/packages/mcp-server/src/adapters/__tests__/mcp-tools-regression.test.ts index e832d66..b41691f 100644 --- a/packages/mcp-server/src/adapters/__tests__/mcp-tools-regression.test.ts +++ b/packages/mcp-server/src/adapters/__tests__/mcp-tools-regression.test.ts @@ -1,8 +1,8 @@ /** - * MCP Tools Regression — verifies exactly 6 built-in adapters survive Phase 2 cleanup. + * MCP Tools Regression — verifies exactly 5 built-in adapters. * - * This test catches accidental re-introduction of removed adapters (History, GitHub, - * Plan, Explore) and ensures no adapters are silently dropped. + * Health was merged into Status (dev_status section="health"). + * This test catches accidental re-introduction of removed adapters. */ import { describe, expect, it } from 'vitest'; @@ -10,23 +10,19 @@ import * as builtIn from '../built-in/index.js'; const adapterNames = Object.keys(builtIn).filter((k) => k.endsWith('Adapter')); -describe('MCP tools regression (post Phase 2)', () => { - it('barrel exports exactly 6 adapter classes', () => { - expect(adapterNames).toHaveLength(6); +describe('MCP tools regression', () => { + it('barrel exports exactly 5 adapter classes', () => { + expect(adapterNames).toHaveLength(5); }); - it.each([ - 'HealthAdapter', - 'InspectAdapter', - 'MapAdapter', - 'RefsAdapter', - 'SearchAdapter', - 'StatusAdapter', - ])('exports %s', (name) => { - expect(adapterNames).toContain(name); - }); + it.each(['InspectAdapter', 'MapAdapter', 'RefsAdapter', 'SearchAdapter', 'StatusAdapter'])( + 'exports %s', + (name) => { + expect(adapterNames).toContain(name); + } + ); - it.each(['HistoryAdapter', 'GitHubAdapter', 'PlanAdapter', 'ExploreAdapter'])( + it.each(['HealthAdapter', 'HistoryAdapter', 'GitHubAdapter', 'PlanAdapter', 'ExploreAdapter'])( 'does NOT export removed %s', (name) => { expect(adapterNames).not.toContain(name); diff --git a/packages/mcp-server/src/adapters/built-in/health-adapter.ts b/packages/mcp-server/src/adapters/built-in/health-adapter.ts deleted file mode 100644 index bc03bc1..0000000 --- a/packages/mcp-server/src/adapters/built-in/health-adapter.ts +++ /dev/null @@ -1,343 +0,0 @@ -/** - * Health Check Adapter - * - * Provides health and readiness checks for the MCP server and its dependencies. - */ - -import * as fs from 'node:fs/promises'; -import { HealthArgsSchema } from '../../schemas/index.js'; -import { ToolAdapter } from '../tool-adapter'; -import type { AdapterContext, ToolDefinition, ToolExecutionContext, ToolResult } from '../types'; -import { validateArgs } from '../validation.js'; - -export interface HealthCheckConfig { - repositoryPath: string; - vectorStorePath: string; - githubStatePath?: string; -} - -export interface HealthStatus { - status: 'healthy' | 'degraded' | 'unhealthy'; - checks: { - vectorStorage: CheckResult; - repository: CheckResult; - githubIndex?: CheckResult; - }; - timestamp: string; - uptime: number; // milliseconds -} - -interface CheckResult { - status: 'pass' | 'warn' | 'fail'; - message: string; - details?: Record; -} - -export class HealthAdapter extends ToolAdapter { - readonly metadata = { - name: 'health-adapter', - version: '1.0.0', - description: 'Provides health and readiness checks for the MCP server', - }; - - private config: HealthCheckConfig; - private startTime: number; - - constructor(config: HealthCheckConfig) { - super(); - this.config = config; - this.startTime = Date.now(); - } - - async initialize(context: AdapterContext): Promise { - this.initializeBase(context); - } - - async shutdown(): Promise { - // No cleanup needed - } - - getToolDefinition(): ToolDefinition { - return { - name: 'dev_health', - description: - 'Check the health status of the dev-agent MCP server and its dependencies (vector storage, repository, GitHub index)', - inputSchema: { - type: 'object', - properties: { - verbose: { - type: 'boolean', - description: 'Include detailed diagnostic information', - default: false, - }, - }, - }, - }; - } - - async execute(args: Record, context: ToolExecutionContext): Promise { - // Validate args with Zod (no type assertions!) - const validation = validateArgs(HealthArgsSchema, args); - if (!validation.success) { - return validation.error; - } - - const { verbose } = validation.data; - - try { - const health = await this.performHealthChecks(verbose); - - const status = this.getOverallStatus(health); - const emoji = status === 'healthy' ? '✅' : status === 'degraded' ? '⚠️' : '❌'; - - const content = this.formatHealthReport(health, verbose); - - // Return formatted health report (MCP will wrap in content blocks) - return { - success: true, - data: `${emoji} **MCP Server Health: ${status.toUpperCase()}**\n\n${content}`, - }; - } catch (error) { - context.logger.error('Health check failed', { - error: error instanceof Error ? error.message : String(error), - }); - - return { - success: false, - error: { - code: 'HEALTH_CHECK_ERROR', - message: error instanceof Error ? error.message : 'Health check failed', - recoverable: true, - }, - }; - } - } - - private async performHealthChecks(verbose: boolean): Promise { - const [vectorStorage, repository, githubIndex] = await Promise.all([ - this.checkVectorStorage(verbose), - this.checkRepository(verbose), - this.config.githubStatePath ? this.checkGitHubIndex(verbose) : Promise.resolve(undefined), - ]); - - const checks: HealthStatus['checks'] = { - vectorStorage, - repository, - }; - - if (githubIndex) { - checks.githubIndex = githubIndex; - } - - return { - status: this.getOverallStatus({ checks } as HealthStatus), - checks, - timestamp: new Date().toISOString(), - uptime: Date.now() - this.startTime, - }; - } - - private async checkVectorStorage(verbose: boolean): Promise { - try { - const stats = await fs.stat(this.config.vectorStorePath); - - if (!stats.isDirectory()) { - return { - status: 'fail', - message: 'Vector storage path is not a directory', - }; - } - - // Check if vector storage has data - const files = await fs.readdir(this.config.vectorStorePath); - const hasData = files.length > 0; - - if (!hasData) { - return { - status: 'warn', - message: 'Vector storage is empty (repository may not be indexed)', - details: verbose ? { path: this.config.vectorStorePath } : undefined, - }; - } - - return { - status: 'pass', - message: `Vector storage operational (${files.length} files)`, - details: verbose - ? { path: this.config.vectorStorePath, fileCount: files.length } - : undefined, - }; - } catch (error) { - return { - status: 'fail', - message: `Vector storage not accessible: ${error instanceof Error ? error.message : String(error)}`, - details: verbose ? { path: this.config.vectorStorePath } : undefined, - }; - } - } - - private async checkRepository(verbose: boolean): Promise { - try { - const stats = await fs.stat(this.config.repositoryPath); - - if (!stats.isDirectory()) { - return { - status: 'fail', - message: 'Repository path is not a directory', - }; - } - - // Check if it's a git repository - try { - await fs.stat(`${this.config.repositoryPath}/.git`); - return { - status: 'pass', - message: 'Repository accessible and is a Git repository', - details: verbose ? { path: this.config.repositoryPath } : undefined, - }; - } catch { - return { - status: 'warn', - message: 'Repository accessible but not a Git repository', - details: verbose ? { path: this.config.repositoryPath } : undefined, - }; - } - } catch (error) { - return { - status: 'fail', - message: `Repository not accessible: ${error instanceof Error ? error.message : String(error)}`, - details: verbose ? { path: this.config.repositoryPath } : undefined, - }; - } - } - - private async checkGitHubIndex(verbose: boolean): Promise { - if (!this.config.githubStatePath) { - return { - status: 'warn', - message: 'GitHub index not configured', - }; - } - - try { - const content = await fs.readFile(this.config.githubStatePath, 'utf-8'); - const state = JSON.parse(content); - - const lastIndexed = state.lastIndexed ? new Date(state.lastIndexed) : null; - const itemCount = state.items?.length ?? 0; - - if (!lastIndexed) { - return { - status: 'warn', - message: 'GitHub index exists but has no lastIndexed timestamp', - details: verbose ? { path: this.config.githubStatePath } : undefined, - }; - } - - const ageMs = Date.now() - lastIndexed.getTime(); - const ageHours = Math.floor(ageMs / (1000 * 60 * 60)); - - // Warn if index is older than 24 hours - if (ageHours > 24) { - return { - status: 'warn', - message: `GitHub index is ${ageHours}h old (${itemCount} items) - consider re-indexing`, - details: verbose - ? { path: this.config.githubStatePath, lastIndexed: lastIndexed.toISOString() } - : undefined, - }; - } - - return { - status: 'pass', - message: `GitHub index operational (${itemCount} items, indexed ${ageHours}h ago)`, - details: verbose - ? { path: this.config.githubStatePath, lastIndexed: lastIndexed.toISOString() } - : undefined, - }; - } catch (error) { - return { - status: 'warn', - message: `GitHub index not accessible: ${error instanceof Error ? error.message : String(error)}`, - details: verbose ? { path: this.config.githubStatePath } : undefined, - }; - } - } - - private getOverallStatus(health: HealthStatus): 'healthy' | 'degraded' | 'unhealthy' { - const checks = Object.values(health.checks).filter( - (check): check is CheckResult => check !== undefined - ); - - const hasFail = checks.some((check) => check.status === 'fail'); - const hasWarn = checks.some((check) => check.status === 'warn'); - - if (hasFail) { - return 'unhealthy'; - } - if (hasWarn) { - return 'degraded'; - } - return 'healthy'; - } - - private formatHealthReport(health: HealthStatus, verbose: boolean): string { - const lines: string[] = []; - - // Uptime - const uptimeMs = health.uptime; - const uptimeStr = this.formatUptime(uptimeMs); - lines.push(`**Uptime:** ${uptimeStr}`); - lines.push(`**Timestamp:** ${new Date(health.timestamp).toLocaleString()}`); - lines.push(''); - - // Checks - lines.push('**Component Status:**'); - lines.push(''); - - for (const [name, check] of Object.entries(health.checks)) { - if (!check) continue; - - const statusEmoji = check.status === 'pass' ? '✅' : check.status === 'warn' ? '⚠️' : '❌'; - const componentName = this.formatComponentName(name); - - lines.push(`${statusEmoji} **${componentName}:** ${check.message}`); - - if (verbose && check.details) { - lines.push(` *Details:* ${JSON.stringify(check.details)}`); - } - } - - return lines.join('\n'); - } - - private formatComponentName(name: string): string { - return name - .replace(/([A-Z])/g, ' $1') - .replace(/^./, (str) => str.toUpperCase()) - .trim(); - } - - private formatUptime(ms: number): string { - const seconds = Math.floor(ms / 1000); - const minutes = Math.floor(seconds / 60); - const hours = Math.floor(minutes / 60); - const days = Math.floor(hours / 24); - - if (days > 0) { - return `${days}d ${hours % 24}h ${minutes % 60}m`; - } - if (hours > 0) { - return `${hours}h ${minutes % 60}m`; - } - if (minutes > 0) { - return `${minutes}m ${seconds % 60}s`; - } - return `${seconds}s`; - } - - async healthCheck(): Promise { - const health = await this.performHealthChecks(false); - return health.status === 'healthy'; - } -} diff --git a/packages/mcp-server/src/adapters/built-in/index.ts b/packages/mcp-server/src/adapters/built-in/index.ts index 2f8f7a1..d9378e3 100644 --- a/packages/mcp-server/src/adapters/built-in/index.ts +++ b/packages/mcp-server/src/adapters/built-in/index.ts @@ -3,7 +3,6 @@ * Production-ready adapters included with the MCP server */ -export { HealthAdapter, type HealthCheckConfig } from './health-adapter.js'; export { InspectAdapter, type InspectAdapterConfig } from './inspect-adapter.js'; export { MapAdapter, type MapAdapterConfig } from './map-adapter.js'; export { RefsAdapter, type RefsAdapterConfig } from './refs-adapter.js'; diff --git a/packages/mcp-server/src/adapters/built-in/inspect-adapter.ts b/packages/mcp-server/src/adapters/built-in/inspect-adapter.ts index ff30236..f33553d 100644 --- a/packages/mcp-server/src/adapters/built-in/inspect-adapter.ts +++ b/packages/mcp-server/src/adapters/built-in/inspect-adapter.ts @@ -9,6 +9,7 @@ import { PatternAnalysisService, type PatternComparison, type SearchService, + type VectorStorage, } from '@prosdevlab/dev-agent-core'; import { InspectArgsSchema } from '../../schemas/index.js'; import { ToolAdapter } from '../tool-adapter.js'; @@ -18,6 +19,7 @@ import { validateArgs } from '../validation.js'; export interface InspectAdapterConfig { repositoryPath: string; searchService: SearchService; + vectorStorage?: VectorStorage; defaultLimit?: number; defaultThreshold?: number; defaultFormat?: 'compact' | 'verbose'; @@ -49,6 +51,7 @@ export class InspectAdapter extends ToolAdapter { this.searchService = config.searchService; this.patternService = new PatternAnalysisService({ repositoryPath: config.repositoryPath, + vectorStorage: config.vectorStorage, }); this.defaultLimit = config.defaultLimit ?? 10; this.defaultThreshold = config.defaultThreshold ?? 0.7; @@ -72,13 +75,14 @@ export class InspectAdapter extends ToolAdapter { return { name: 'dev_patterns', description: - 'Analyze coding patterns in a file against the codebase. Compares import style, ' + - 'error handling, type coverage, test coverage, and file size against similar files. ' + - 'Use for code reviews and consistency checks.', + 'Analyze coding patterns in a file against similar code in the codebase. ' + + 'Compares import style, error handling, type coverage, test coverage, and file size. ' + + 'Use for code reviews and consistency checks. ' + + 'NOT for finding code (use dev_search) or tracing calls (use dev_refs).', inputSchema: { type: 'object', properties: { - query: { + filePath: { type: 'string', description: 'File path to analyze (e.g., "src/auth/middleware.ts")', }, @@ -91,13 +95,13 @@ export class InspectAdapter extends ToolAdapter { }, format: { type: 'string', - enum: ['compact', 'verbose'], + enum: ['compact', 'verbose', 'json'], description: - 'Output format: "compact" for summaries (default), "verbose" for full details', + 'Output format: "compact" (default), "verbose" for details, "json" for structured data', default: this.defaultFormat, }, }, - required: ['query'], + required: ['filePath'], }, }; } @@ -109,37 +113,51 @@ export class InspectAdapter extends ToolAdapter { return validation.error; } - const { query, limit, format } = validation.data; + const { filePath, limit, format } = validation.data; try { context.logger.debug('Executing pattern analysis', { - query, + filePath, limit, format, }); // Perform comprehensive file inspection - const { content, similarFilesCount, patternsAnalyzed } = await this.inspectFile( - query, - limit, - 0, // No threshold — let the pattern service decide relevance - format - ); + const { content, similarFilesCount, patternsAnalyzed, patternComparison } = + await this.inspectFile(filePath, limit, 0, format); context.logger.info('File inspection completed', { - query, + filePath, similarFilesCount, patternsAnalyzed, contentLength: content.length, }); + // JSON format: return structured data for token-efficient agent workflows + if (format === 'json' && patternComparison) { + const jsonData = JSON.stringify(patternComparison, null, 2); + return { + success: true, + data: jsonData, + metadata: { + tokens: jsonData.length / 4, + duration_ms: 0, + timestamp: new Date().toISOString(), + cached: false, + similar_files_count: similarFilesCount, + patterns_analyzed: patternsAnalyzed, + format: 'json', + }, + }; + } + // Return markdown content (MCP will wrap in content blocks) return { success: true, data: content, metadata: { - tokens: content.length / 4, // Rough estimate - duration_ms: 0, // Calculated by MCP server + tokens: content.length / 4, + duration_ms: 0, timestamp: new Date().toISOString(), cached: false, similar_files_count: similarFilesCount, @@ -156,7 +174,7 @@ export class InspectAdapter extends ToolAdapter { success: false, error: { code: 'FILE_NOT_FOUND', - message: `File not found: ${query}`, + message: `File not found: ${filePath}`, suggestion: 'Check the file path and ensure it exists in the repository.', }, }; @@ -179,6 +197,7 @@ export class InspectAdapter extends ToolAdapter { error: { code: 'INSPECTION_ERROR', message: error instanceof Error ? error.message : 'Unknown inspection error', + suggestion: 'Check the file path is valid and the repository is indexed.', }, }; } @@ -195,7 +214,12 @@ export class InspectAdapter extends ToolAdapter { limit: number, threshold: number, format: string - ): Promise<{ content: string; similarFilesCount: number; patternsAnalyzed: number }> { + ): Promise<{ + content: string; + similarFilesCount: number; + patternsAnalyzed: number; + patternComparison?: PatternComparison; + }> { // Step 1: Find similar files (request slightly more to account for extension filtering) const similarResults = await this.searchService.findSimilar(filePath, { limit: limit + 5, // Small buffer for extension filtering @@ -229,16 +253,19 @@ export class InspectAdapter extends ToolAdapter { const similarFilePaths = filteredResults.map((r) => r.metadata.path as string); const patternComparison = await this.patternService.comparePatterns(filePath, similarFilePaths); - // Step 3: Generate comprehensive inspection report + // Step 3: Generate inspection report (skip formatting for JSON) const content = - format === 'verbose' - ? await this.formatInspectionVerbose(filePath, filteredResults, patternComparison) - : await this.formatInspectionCompact(filePath, filteredResults, patternComparison); + format === 'json' + ? '' // JSON handled by execute() + : format === 'verbose' + ? await this.formatInspectionVerbose(filePath, filteredResults, patternComparison) + : await this.formatInspectionCompact(filePath, filteredResults, patternComparison); return { content, similarFilesCount: filteredResults.length, - patternsAnalyzed: 5, // Currently analyzing 5 pattern categories + patternsAnalyzed: 5, + patternComparison, }; } diff --git a/packages/mcp-server/src/adapters/built-in/map-adapter.ts b/packages/mcp-server/src/adapters/built-in/map-adapter.ts index ff2d035..3e6e4ae 100644 --- a/packages/mcp-server/src/adapters/built-in/map-adapter.ts +++ b/packages/mcp-server/src/adapters/built-in/map-adapter.ts @@ -226,7 +226,7 @@ export class MapAdapter extends ToolAdapter { error: { code: 'MAP_FAILED', message: error instanceof Error ? error.message : 'Unknown error', - details: error, + suggestion: 'Run "dev index" first. Try a lower depth if the map is too large.', }, }; } diff --git a/packages/mcp-server/src/adapters/built-in/refs-adapter.ts b/packages/mcp-server/src/adapters/built-in/refs-adapter.ts index 80f558a..aeb0cfe 100644 --- a/packages/mcp-server/src/adapters/built-in/refs-adapter.ts +++ b/packages/mcp-server/src/adapters/built-in/refs-adapter.ts @@ -130,6 +130,8 @@ export class RefsAdapter extends ToolAdapter { error: { code: 'NOT_FOUND', message: `Could not find function or method named "${name}"`, + suggestion: + 'Verify the function name exists with dev_search first. Names are case-sensitive.', }, }; } @@ -193,7 +195,7 @@ export class RefsAdapter extends ToolAdapter { error: { code: 'REFS_FAILED', message: error instanceof Error ? error.message : 'Unknown error', - details: error, + suggestion: 'Try dev_search to find the correct function name first.', }, }; } diff --git a/packages/mcp-server/src/adapters/built-in/search-adapter.ts b/packages/mcp-server/src/adapters/built-in/search-adapter.ts index 705236e..ae3e883 100644 --- a/packages/mcp-server/src/adapters/built-in/search-adapter.ts +++ b/packages/mcp-server/src/adapters/built-in/search-adapter.ts @@ -216,7 +216,8 @@ export class SearchAdapter extends ToolAdapter { error: { code: 'SEARCH_FAILED', message: error instanceof Error ? error.message : 'Unknown error', - details: error, + suggestion: + 'Run "dev index" to index the repository. Try a different query if no results.', }, }; } diff --git a/packages/mcp-server/src/adapters/built-in/status-adapter.ts b/packages/mcp-server/src/adapters/built-in/status-adapter.ts index 951630e..2095274 100644 --- a/packages/mcp-server/src/adapters/built-in/status-adapter.ts +++ b/packages/mcp-server/src/adapters/built-in/status-adapter.ts @@ -140,7 +140,7 @@ export class StatusAdapter extends ToolAdapter { error: { code: 'STATUS_FAILED', message: error instanceof Error ? error.message : 'Unknown error', - details: error, + suggestion: 'Run "dev setup" to ensure Antfly is running, then "dev index" to index.', }, }; } diff --git a/packages/mcp-server/src/schemas/__tests__/schemas.test.ts b/packages/mcp-server/src/schemas/__tests__/schemas.test.ts index 2b787e5..211564f 100644 --- a/packages/mcp-server/src/schemas/__tests__/schemas.test.ts +++ b/packages/mcp-server/src/schemas/__tests__/schemas.test.ts @@ -7,7 +7,6 @@ import { describe, expect, it } from 'vitest'; import { - HealthArgsSchema, InspectArgsSchema, MapArgsSchema, RefsArgsSchema, @@ -18,7 +17,7 @@ import { describe('InspectArgsSchema', () => { it('should validate valid input', () => { const result = InspectArgsSchema.safeParse({ - query: 'src/auth/token.ts', + filePath: 'src/auth/token.ts', }); expect(result.success).toBe(true); @@ -29,7 +28,7 @@ describe('InspectArgsSchema', () => { it('should apply defaults', () => { const result = InspectArgsSchema.safeParse({ - query: 'test', + filePath: 'test.ts', }); expect(result.success).toBe(true); @@ -41,9 +40,21 @@ describe('InspectArgsSchema', () => { } }); - it('should reject empty query', () => { + it('should accept json format', () => { + const result = InspectArgsSchema.safeParse({ + filePath: 'test.ts', + format: 'json', + }); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.format).toBe('json'); + } + }); + + it('should reject empty filePath', () => { const result = InspectArgsSchema.safeParse({ - query: '', + filePath: '', }); expect(result.success).toBe(false); @@ -51,7 +62,7 @@ describe('InspectArgsSchema', () => { it('should reject out-of-range limit', () => { const result = InspectArgsSchema.safeParse({ - query: 'test', + filePath: 'test.ts', limit: 200, }); @@ -60,7 +71,7 @@ describe('InspectArgsSchema', () => { it('should reject unknown properties', () => { const result = InspectArgsSchema.safeParse({ - query: 'test', + filePath: 'test.ts', unknownProp: 'value', }); @@ -193,32 +204,3 @@ describe('StatusArgsSchema', () => { } }); }); - -describe('HealthArgsSchema', () => { - it('should validate with default', () => { - const result = HealthArgsSchema.safeParse({}); - - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.verbose).toBe(false); - } - }); - - it('should validate verbose flag', () => { - const result = HealthArgsSchema.safeParse({ verbose: true }); - - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.verbose).toBe(true); - } - }); - - it('should reject unknown properties', () => { - const result = HealthArgsSchema.safeParse({ - verbose: true, - unknownProp: 'value', - }); - - expect(result.success).toBe(false); - }); -}); diff --git a/packages/mcp-server/src/schemas/index.ts b/packages/mcp-server/src/schemas/index.ts index 8b8ce24..936bf84 100644 --- a/packages/mcp-server/src/schemas/index.ts +++ b/packages/mcp-server/src/schemas/index.ts @@ -30,9 +30,9 @@ export const BaseQuerySchema = z.object({ export const InspectArgsSchema = z .object({ - query: z.string().min(1, 'Query must be a non-empty string (file path)'), + filePath: z.string().min(1, 'filePath must be a non-empty file path'), limit: z.number().int().min(1).max(50).default(10), - format: FormatSchema.default('compact'), + format: z.enum(['compact', 'verbose', 'json']).default('compact'), }) .strict(); // Reject unknown properties @@ -109,18 +109,6 @@ export const StatusOutputSchema = z.object({ export type StatusOutput = z.infer; -// ============================================================================ -// Health Adapter -// ============================================================================ - -export const HealthArgsSchema = z - .object({ - verbose: z.boolean().default(false), - }) - .strict(); - -export type HealthArgs = z.infer; - // ============================================================================ // Output Schemas (Runtime validation for adapter responses) // ============================================================================ @@ -136,29 +124,6 @@ export const SearchOutputSchema = z.object({ export type SearchOutput = z.infer; -/** - * Health check result schema - */ -export const HealthCheckResultSchema = z.object({ - status: z.enum(['pass', 'warn', 'fail']), - message: z.string(), - details: z.any().optional(), // Allow any type for details -}); - -export const HealthOutputSchema = z.object({ - status: z.enum(['healthy', 'degraded', 'unhealthy']), - uptime: z.number(), - timestamp: z.string(), - checks: z.object({ - vectorStorage: HealthCheckResultSchema, - repository: HealthCheckResultSchema, - githubIndex: HealthCheckResultSchema.optional(), - }), - formattedReport: z.string(), -}); - -export type HealthOutput = z.infer; - /** * Map output schema */ diff --git a/website/content/docs/tools/dev-health.mdx b/website/content/docs/tools/dev-health.mdx index 0910b47..242e502 100644 --- a/website/content/docs/tools/dev-health.mdx +++ b/website/content/docs/tools/dev-health.mdx @@ -1,96 +1,11 @@ -# dev_health +# dev_health (removed) -Monitor MCP server health and component status. Useful for diagnostics and proactive monitoring. +`dev_health` has been merged into [`dev_status`](/docs/tools/dev-status). -## Usage +Use `dev_status` with `section="health"` for health checks: ``` -dev_health(format?) -``` - -## Parameters - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `format` | `"compact"` \| `"verbose"` | `"compact"` | Output format | - -## Example - -> "Use dev_health" - -``` -## MCP Server Health - -Status: ✅ Healthy -Uptime: 2h 34m 12s - -Components: -- Vector Storage: ✅ Pass (24ms) -- Repository Index: ✅ Pass (12ms) -- GitHub Index: ✅ Pass (8ms) -- Rate Limiter: ✅ Pass (89/100 tokens available) -``` - -## Verbose Output - -> "Use dev_health format verbose" - -``` -## MCP Server Health (Verbose) - -Status: ✅ Healthy -Version: 0.1.0 -Started: 2024-01-15T08:00:00Z -Uptime: 2h 34m 12s -Process ID: 12345 - -Components: -┌─────────────────┬────────┬──────────┬─────────────────────┐ -│ Component │ Status │ Latency │ Details │ -├─────────────────┼────────┼──────────┼─────────────────────┤ -│ Vector Storage │ ✅ Pass │ 24ms │ Antfly connected │ -│ Repository Index│ ✅ Pass │ 12ms │ 1,832 components │ -│ GitHub Index │ ✅ Pass │ 8ms │ 172 documents │ -│ Rate Limiter │ ✅ Pass │ <1ms │ 89/100 tokens │ -└─────────────────┴────────┴──────────┴─────────────────────┘ - -Memory Usage: 145 MB -CPU: 2.3% -``` - -## Health States - -| State | Meaning | -|-------|---------| -| ✅ **Healthy** | All components operational | -| ⚠️ **Degraded** | Some components have warnings | -| ❌ **Unhealthy** | Critical components failing | - -## Component Checks - -| Component | What It Checks | -|-----------|----------------| -| **Vector Storage** | Antfly connection and query capability | -| **Repository Index** | Index exists and is readable | -| **GitHub Index** | GitHub metadata is accessible | -| **Rate Limiter** | Token bucket has capacity | - -## Use Cases - -> **Proactive monitoring.** Check health before important operations to catch issues early. - -> **Debugging tool failures.** If a tool returns errors, health check can identify the root cause. - -## Response Metadata - -```json -{ - "metadata": { - "tokens": 156, - "duration_ms": 45, - "status": "healthy", - "components_checked": 4 - } -} +dev_status(section: "health") ``` +This returns the same Antfly connectivity and repository access checks that `dev_health` provided. diff --git a/website/content/docs/tools/dev-patterns.mdx b/website/content/docs/tools/dev-patterns.mdx index b400e1c..efdae70 100644 --- a/website/content/docs/tools/dev-patterns.mdx +++ b/website/content/docs/tools/dev-patterns.mdx @@ -1,36 +1,35 @@ # dev_patterns -Inspect a file for pattern analysis. Finds similar code and compares patterns like error handling, type coverage, imports, and testing. +Analyze coding patterns in a file against similar code in the codebase. Compares import style, error handling, type coverage, test coverage, and file size. + +**Not for finding code** (use `dev_search`) **or tracing calls** (use `dev_refs`). ## Usage ``` -dev_patterns(query, format?, limit?, threshold?) +dev_patterns(filePath, format?, limit?) ``` ## Parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| -| `query` | string | required | File path to inspect | -| `format` | `"compact"` \| `"verbose"` | `"compact"` | Output format | +| `filePath` | string | required | File path to analyze (e.g., `src/auth/middleware.ts`) | +| `format` | `"compact"` \| `"verbose"` \| `"json"` | `"compact"` | Output format | | `limit` | number | 10 | Maximum similar files to analyze | -| `threshold` | number | 0.7 | Similarity threshold (0-1) | ## What It Does `dev_patterns` performs comprehensive file analysis in two steps: -1. **Finds similar files** - Uses semantic search to find code with similar structure/purpose -2. **Analyzes patterns** - Compares error handling, type coverage, imports, testing, and file size - -This helps you understand how your file compares to similar code in the repository. +1. **Finds similar files** — Uses semantic search to find code with similar structure/purpose +2. **Analyzes patterns** — Compares error handling, type coverage, imports, testing, and file size ## Examples ### Basic Inspection -> "Use dev_patterns with query 'src/auth/middleware.ts'" +> "Use dev_patterns with filePath 'src/auth/middleware.ts'" ``` ## File Inspection: src/auth/middleware.ts @@ -51,96 +50,53 @@ This helps you understand how your file compares to similar code in the reposito ### Verbose Output -> "Use dev_patterns with query 'packages/core/src/indexer/index.ts', format 'verbose'" +> "Use dev_patterns with filePath 'packages/core/src/indexer/index.ts', format 'verbose'" Returns detailed pattern distribution and statistics for each pattern category. -### High Similarity Only +### JSON Output (Token-Efficient) -> "Use dev_patterns with query 'src/utils/date.ts', threshold 0.9" +> "Use dev_patterns with filePath 'src/auth/middleware.ts', format 'json'" -Only analyzes very similar files (90%+ match). +Returns structured JSON — compact and machine-parseable for agent workflows: -## Pattern Categories +```json +{ + "fileSize": { "yourFile": 234, "average": 312, "deviation": "smaller" }, + "importStyle": { "yourFile": "esm", "common": "esm", "percentage": 100 }, + "errorHandling": { "yourFile": "throw", "common": "result", "percentage": 60 }, + "typeAnnotations": { "yourFile": "full", "common": "full", "percentage": 80 }, + "testing": { "yourFile": false, "percentage": 40 } +} +``` -`dev_patterns` analyzes 5 key patterns: +## Pattern Categories | Pattern | What It Checks | |---------|----------------| | **Import Style** | `esm`, `cjs`, `mixed`, or `unknown` | -| **Error Handling** | `throw`, `result`, `callback`, or `unknown` | -| **Type Coverage** | `full`, `partial`, or `none` (TypeScript only) | +| **Error Handling** | `throw`, `result`, `error-return`, or `unknown` | +| **Type Coverage** | `full`, `partial`, `minimal`, or `none` (TypeScript only) | | **Testing** | Whether a co-located test file exists | | **File Size** | Line count vs. similar files | -## Output Formats - -### Compact (Default) - -Shows pattern summary with actionable insights: - -``` -**Error Handling:** Your file uses `throw`, but 60% of similar files use `result`. -``` - -Highlights differences from similar files to guide improvements. - -### Verbose - -Shows full pattern distribution: - -``` -#### Error Handling -- **Your File:** `throw` -- **Common Style:** `result` (60% of similar files) -- **Distribution:** - - result: 3 files (60%) - - throw: 2 files (40%) -``` - -## Use Cases - -| Use Case | Description | -|----------|-------------| -| **Code Review** | Check if your code follows project patterns | -| **Refactoring** | Understand pattern usage before making changes | -| **Consistency** | Ensure new code matches similar implementations | -| **Learning** | See how patterns are used across the codebase | - ## Workflow **Typical usage pattern:** 1. **Search** for concepts: `dev_search { query: "authentication middleware" }` -2. **Inspect** what you found: `dev_patterns { query: "src/auth/middleware.ts" }` +2. **Inspect** what you found: `dev_patterns { filePath: "src/auth/middleware.ts" }` 3. **Analyze** dependencies: `dev_refs { name: "authMiddleware" }` -## Tips - -> **Always provide a file path.** Unlike `dev_search`, `dev_patterns` requires a specific file to analyze. - -> **Extension filtering.** Only compares files with the same extension (e.g., `.ts` with `.ts`). - -> **Use after search.** `dev_patterns` is most valuable after finding relevant files with `dev_search`. - -> **Pattern-driven insights.** Focus on patterns where your file differs from similar code. - ## Performance -`dev_patterns` is optimized for speed: -- Batch scanning (5-10x faster than individual scans) -- Semantic similarity matching (not just path-based) -- Extension-based filtering for relevant comparisons - -Typical analysis time: **500-1000ms** for 5 similar files. +Pattern analysis reads from the Antfly index when available (~100ms). Falls back to ts-morph scanning when not (~1-3s). ## Migration from dev_explore If you previously used `dev_explore`: -- `dev_explore { action: "similar", query: "file.ts" }` → `dev_patterns { query: "file.ts" }` -- `dev_explore { action: "validate", query: "file.ts" }` → `dev_patterns { query: "file.ts" }` (now does both!) +- `dev_explore { action: "similar", query: "file.ts" }` → `dev_patterns { filePath: "file.ts" }` +- `dev_explore { action: "validate", query: "file.ts" }` → `dev_patterns { filePath: "file.ts" }` (now does both!) - `dev_explore { action: "pattern" }` → Use `dev_search` instead - `dev_explore { action: "relationships" }` → Use `dev_refs` instead - -**Key change:** `dev_patterns` no longer requires an `action` parameter. It automatically finds similar files AND performs pattern analysis in one call. diff --git a/website/content/docs/tools/index.mdx b/website/content/docs/tools/index.mdx index 2078706..5c2448c 100644 --- a/website/content/docs/tools/index.mdx +++ b/website/content/docs/tools/index.mdx @@ -1,6 +1,6 @@ # MCP Tools Overview -dev-agent provides six tools through the Model Context Protocol (MCP). These tools give AI assistants deep understanding of your codebase. +dev-agent provides five tools through the Model Context Protocol (MCP). These tools give AI assistants deep understanding of your codebase. ## Available Tools @@ -9,9 +9,8 @@ dev-agent provides six tools through the Model Context Protocol (MCP). These too | [`dev_search`](/docs/tools/dev-search) | Semantic code search with snippets | | [`dev_refs`](/docs/tools/dev-refs) | Query code relationships (callers/callees) | | [`dev_map`](/docs/tools/dev-map) | Codebase structure overview with change frequency | -| [`dev_patterns`](/docs/tools/dev-inspect) | File pattern analysis (finds similar code, compares 5 pattern categories) ✨ v0.8.5 | -| [`dev_status`](/docs/tools/dev-status) | Check repository indexing status | -| [`dev_health`](/docs/tools/dev-health) | Monitor MCP server health | +| [`dev_patterns`](/docs/tools/dev-patterns) | File pattern analysis (finds similar code, compares 5 pattern categories) | +| [`dev_status`](/docs/tools/dev-status) | Repository indexing status and health checks | ## New in v0.8.5 @@ -49,8 +48,7 @@ AI Assistant (Cursor/Claude) ├── RefsAdapter → dev_refs ├── MapAdapter → dev_map ├── InspectAdapter → dev_patterns - ├── StatusAdapter → dev_status - └── HealthAdapter → dev_health + └── StatusAdapter → dev_status (includes health checks) ``` ## Tool Response Format diff --git a/website/content/docs/troubleshooting.mdx b/website/content/docs/troubleshooting.mdx index 089b512..f1ae6f6 100644 --- a/website/content/docs/troubleshooting.mdx +++ b/website/content/docs/troubleshooting.mdx @@ -92,7 +92,7 @@ dev index ### Rate limit errors (429) - Wait for `retryAfterMs` period -- Check health: `dev_health` +- Check health: `dev_status section="health"` - Restart AI tool if persistent ## Search Issues @@ -183,7 +183,7 @@ dev mcp install --cursor ### Check health ``` -Use dev_health tool in Cursor/Claude Code +Use dev_status with section="health" in Cursor/Claude Code ``` ### Enable debug logging @@ -195,10 +195,10 @@ Use dev_health tool in Cursor/Claude Code ## Getting Help -1. Run `dev_health` for diagnostics +1. Run `dev_status section="health"` for diagnostics 2. Check [full troubleshooting guide](https://github.com/prosdevlab/dev-agent/blob/main/TROUBLESHOOTING.md) 3. [File an issue](https://github.com/prosdevlab/dev-agent/issues) with: - `dev --version` - - `dev_health` output + - `dev_status section="health"` output - Steps to reproduce diff --git a/website/content/index.mdx b/website/content/index.mdx index a7e034a..ef7d9f9 100644 --- a/website/content/index.mdx +++ b/website/content/index.mdx @@ -21,7 +21,7 @@ Local semantic code search for Cursor and Claude Code via MCP. ## What it does -Your AI tool gets 6 MCP tools that understand your codebase: +Your AI tool gets 5 MCP tools that understand your codebase: | Tool | What it does | |------|--------------| @@ -29,8 +29,7 @@ Your AI tool gets 6 MCP tools that understand your codebase: | [`dev_refs`](/docs/tools/dev-refs) | Find callers/callees of any function | | [`dev_map`](/docs/tools/dev-map) | Codebase structure with hot paths (most referenced files) | | [`dev_patterns`](/docs/tools/dev-patterns) | Compare coding patterns against similar files | -| [`dev_status`](/docs/tools/dev-status) | Repository indexing status and health | -| [`dev_health`](/docs/tools/dev-health) | Server health checks | +| [`dev_status`](/docs/tools/dev-status) | Repository indexing status, health checks, and Antfly stats | ## How it works @@ -42,7 +41,7 @@ flowchart LR subgraph Agent["dev-agent"] B["MCP Server"] - C["6 Tools"] + C["5 Tools"] end subgraph Local["Antfly (local)"] diff --git a/website/content/latest-version.ts b/website/content/latest-version.ts index c6a9871..170ba67 100644 --- a/website/content/latest-version.ts +++ b/website/content/latest-version.ts @@ -4,10 +4,10 @@ */ export const latestVersion = { - version: '0.10.3', - title: 'Fix Setup/Index Model Directory Mismatch', - date: 'March 30, 2026', + version: '0.10.4', + title: 'MCP Tools Improvement (Phase 1)', + date: 'March 31, 2026', summary: - 'Fixed dev setup reporting model ready while dev index fails with "model not found" due to mismatched model directories.', - link: '/updates#v0103--fix-setupindex-model-directory-mismatch', + 'dev_patterns is 10-30x faster, dev_health merged into dev_status, all tools return error recovery suggestions.', + link: '/updates#v0104--mcp-tools-improvement-phase-1', } as const; diff --git a/website/content/updates/index.mdx b/website/content/updates/index.mdx index c8b5c11..a5048a4 100644 --- a/website/content/updates/index.mdx +++ b/website/content/updates/index.mdx @@ -9,6 +9,22 @@ What's new in dev-agent. We ship improvements regularly to help AI assistants un --- +## v0.10.4 — MCP Tools Improvement (Phase 1) + +*March 31, 2026* + +**Faster pattern analysis, merged health into status, better agent usability.** + +- `dev_patterns` is 10-30x faster — reads from Antfly index instead of re-scanning with ts-morph +- `dev_health` merged into `dev_status` (`section="health"`) — reduced from 6 tools to 5 +- `dev_patterns` parameter renamed from `query` to `filePath` to prevent LLM misuse +- New `format: "json"` option on `dev_patterns` for token-efficient agent workflows +- All tools now return a `suggestion` field on errors for agent recovery guidance +- Removed stale GitHub code from health adapter +- Extracted pure pattern analyzers for testability + +--- + ## v0.10.3 — Fix Setup/Index Model Directory Mismatch *March 30, 2026*