From ee2e2c0775d5db3489e35bb22b16e1c833b0b714 Mon Sep 17 00:00:00 2001 From: prosdev Date: Wed, 26 Nov 2025 08:21:03 -0800 Subject: [PATCH 1/4] feat(mcp): add MCPMetadata interface for structured tool responses - Define MCPMetadata interface with core metrics (tokens, duration_ms, timestamp, cached) - Add data quality fields (results_total, results_returned, results_truncated) - Include future-ready fields for confidence, warnings, request_id - Update FormattedResult to use 'tokens' instead of 'tokenEstimate' Part of #51 --- packages/mcp-server/src/adapters/types.ts | 35 ++++++++++++++++++--- packages/mcp-server/src/formatters/types.ts | 2 +- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/packages/mcp-server/src/adapters/types.ts b/packages/mcp-server/src/adapters/types.ts index 02fc3de..e14ce17 100644 --- a/packages/mcp-server/src/adapters/types.ts +++ b/packages/mcp-server/src/adapters/types.ts @@ -39,16 +39,41 @@ export interface Config { [key: string]: unknown; } +/** + * Structured metadata for MCP tool responses + * Follows industry best practices (GitHub, Stripe, GraphQL) + */ +export interface MCPMetadata { + // Cost tracking + /** Approximate token count (for context window management) */ + tokens: number; + + // Performance + /** Response time in milliseconds */ + duration_ms: number; + /** ISO 8601 timestamp of response */ + timestamp: string; + + // Data freshness + /** Whether response came from cache */ + cached: boolean; + + // Data quality (optional) + /** Total matches found before limiting */ + results_total?: number; + /** Number of results in this response */ + results_returned?: number; + /** Whether results were truncated due to limits */ + results_truncated?: boolean; +} + // Tool Result export interface ToolResult { success: boolean; data?: unknown; error?: AdapterError; - metadata?: { - tokenEstimate?: number; - executionTime?: number; - cached?: boolean; - }; + /** Structured metadata about the response */ + metadata?: MCPMetadata; } // Adapter Error diff --git a/packages/mcp-server/src/formatters/types.ts b/packages/mcp-server/src/formatters/types.ts index 183d86a..35fc9e4 100644 --- a/packages/mcp-server/src/formatters/types.ts +++ b/packages/mcp-server/src/formatters/types.ts @@ -15,7 +15,7 @@ export type FormatMode = 'compact' | 'verbose'; */ export interface FormattedResult { content: string; - tokenEstimate: number; + tokens: number; } /** From c9acfeff09580ca695334775f0576fe49b35fdc4 Mon Sep 17 00:00:00 2001 From: prosdev Date: Wed, 26 Nov 2025 08:21:15 -0800 Subject: [PATCH 2/4] refactor(mcp): update formatters to return structured {content, tokens} - Remove token footer from content string (moved to metadata) - Return tokens as separate field in FormattedResult - Update formatter tests for new structure Part of #51 --- .../formatters/__tests__/formatters.test.ts | 41 +++++++++---------- .../src/formatters/compact-formatter.ts | 13 +++--- .../src/formatters/verbose-formatter.ts | 13 +++--- 3 files changed, 30 insertions(+), 37 deletions(-) diff --git a/packages/mcp-server/src/formatters/__tests__/formatters.test.ts b/packages/mcp-server/src/formatters/__tests__/formatters.test.ts index 58b15c3..734048b 100644 --- a/packages/mcp-server/src/formatters/__tests__/formatters.test.ts +++ b/packages/mcp-server/src/formatters/__tests__/formatters.test.ts @@ -69,9 +69,9 @@ describe('Formatters', () => { expect(result.content).toContain('1. [89%]'); expect(result.content).toContain('2. [84%]'); expect(result.content).toContain('3. [72%]'); - expect(result.tokenEstimate).toBeGreaterThan(0); - // Should include token footer - expect(result.content).toMatch(/🪙 ~\d+ tokens$/); + expect(result.tokens).toBeGreaterThan(0); + // Token footer moved to metadata, no longer in content + expect(result.content).not.toContain('🪙'); }); it('should respect maxResults option', () => { @@ -143,8 +143,9 @@ describe('Formatters', () => { // Should have double newlines between results expect(result.content).toContain('\n\n'); - // Should include token footer - expect(result.content).toMatch(/🪙 ~\d+ tokens$/); + // Token footer moved to metadata, no longer in content + expect(result.content).not.toContain('🪙'); + expect(result.tokens).toBeGreaterThan(0); }); it('should include signatures by default', () => { @@ -209,7 +210,7 @@ describe('Formatters', () => { const verboseResult = verboseFormatter.formatResults(mockResults); // Verbose should be significantly larger - expect(verboseResult.tokenEstimate).toBeGreaterThan(compactResult.tokenEstimate * 2); + expect(verboseResult.tokens).toBeGreaterThan(compactResult.tokens * 2); }); it('token estimates should scale with result count', () => { @@ -218,37 +219,35 @@ describe('Formatters', () => { const oneResult = formatter.formatResults([mockResults[0]]); const threeResults = formatter.formatResults(mockResults); - expect(threeResults.tokenEstimate).toBeGreaterThan(oneResult.tokenEstimate * 2); + expect(threeResults.tokens).toBeGreaterThan(oneResult.tokens * 2); }); }); - describe('Token Footer', () => { - it('compact formatter should include coin emoji footer', () => { + describe('Structured Metadata', () => { + it('compact formatter should return tokens in result object', () => { const formatter = new CompactFormatter(); const result = formatter.formatResults(mockResults); - expect(result.content).toContain('🪙'); - expect(result.content).toMatch(/~\d+ tokens$/); + // Token info is now in the result object, not the content + expect(result.tokens).toBeGreaterThan(0); + expect(result.content).not.toContain('🪙'); }); - it('verbose formatter should include coin emoji footer', () => { + it('verbose formatter should return tokens in result object', () => { const formatter = new VerboseFormatter(); const result = formatter.formatResults(mockResults); - expect(result.content).toContain('🪙'); - expect(result.content).toMatch(/~\d+ tokens$/); + // Token info is now in the result object, not the content + expect(result.tokens).toBeGreaterThan(0); + expect(result.content).not.toContain('🪙'); }); - it('token footer should match tokenEstimate property', () => { + it('tokens property should be a positive number', () => { const formatter = new CompactFormatter(); const result = formatter.formatResults(mockResults); - // Extract token count from footer - const footerMatch = result.content.match(/🪙 ~(\d+) tokens$/); - expect(footerMatch).toBeTruthy(); - - const footerTokens = Number.parseInt(footerMatch?.[1] ?? '0', 10); - expect(footerTokens).toBe(result.tokenEstimate); + expect(typeof result.tokens).toBe('number'); + expect(result.tokens).toBeGreaterThan(0); }); }); }); diff --git a/packages/mcp-server/src/formatters/compact-formatter.ts b/packages/mcp-server/src/formatters/compact-formatter.ts index 0f00b79..4ee0100 100644 --- a/packages/mcp-server/src/formatters/compact-formatter.ts +++ b/packages/mcp-server/src/formatters/compact-formatter.ts @@ -59,7 +59,7 @@ export class CompactFormatter implements ResultFormatter { const content = 'No results found'; return { content, - tokenEstimate: estimateTokensForText(content), + tokens: estimateTokensForText(content), }; } @@ -71,16 +71,13 @@ export class CompactFormatter implements ResultFormatter { return `${index + 1}. ${this.formatResult(result)}`; }); - // Calculate total tokens - const contentLines = formatted.join('\n'); - const tokenEstimate = estimateTokensForText(contentLines); - - // Add token footer - const content = `${contentLines}\n\n🪙 ~${tokenEstimate} tokens`; + // Calculate total tokens (content only, no footer) + const content = formatted.join('\n'); + const tokens = estimateTokensForText(content); return { content, - tokenEstimate, + tokens, }; } diff --git a/packages/mcp-server/src/formatters/verbose-formatter.ts b/packages/mcp-server/src/formatters/verbose-formatter.ts index 0c29b3c..c8fe8e8 100644 --- a/packages/mcp-server/src/formatters/verbose-formatter.ts +++ b/packages/mcp-server/src/formatters/verbose-formatter.ts @@ -89,7 +89,7 @@ export class VerboseFormatter implements ResultFormatter { const content = 'No results found'; return { content, - tokenEstimate: estimateTokensForText(content), + tokens: estimateTokensForText(content), }; } @@ -101,16 +101,13 @@ export class VerboseFormatter implements ResultFormatter { return `${index + 1}. ${this.formatResult(result)}`; }); - // Calculate total tokens - const contentLines = formatted.join('\n\n'); // Double newline for separation - const tokenEstimate = estimateTokensForText(contentLines); - - // Add token footer - const content = `${contentLines}\n\n🪙 ~${tokenEstimate} tokens`; + // Calculate total tokens (content only, no footer) + const content = formatted.join('\n\n'); // Double newline for separation + const tokens = estimateTokensForText(content); return { content, - tokenEstimate, + tokens, }; } From 4527a55af6212a88546cde20a97117dd25bd8447 Mon Sep 17 00:00:00 2001 From: prosdev Date: Wed, 26 Nov 2025 08:21:35 -0800 Subject: [PATCH 3/4] feat(mcp): add structured metadata to adapter responses - Update search, github, status adapters to populate MCPMetadata - Include tokens, duration_ms, timestamp, cached, results_total - Use duration_ms instead of executionTime in adapter registry - Update all adapter tests for new metadata structure Closes #51 --- .../__tests__/adapter-registry.test.ts | 2 +- .../adapters/__tests__/github-adapter.test.ts | 6 +- .../src/adapters/__tests__/mock-adapter.ts | 5 +- .../adapters/__tests__/search-adapter.test.ts | 23 ++-- .../adapters/__tests__/status-adapter.test.ts | 11 +- .../src/adapters/adapter-registry.ts | 6 +- .../src/adapters/built-in/github-adapter.ts | 105 +++++++++++------- .../src/adapters/built-in/search-adapter.ts | 19 +++- .../src/adapters/built-in/status-adapter.ts | 13 ++- 9 files changed, 121 insertions(+), 69 deletions(-) diff --git a/packages/mcp-server/src/adapters/__tests__/adapter-registry.test.ts b/packages/mcp-server/src/adapters/__tests__/adapter-registry.test.ts index 6ce40ec..7153919 100644 --- a/packages/mcp-server/src/adapters/__tests__/adapter-registry.test.ts +++ b/packages/mcp-server/src/adapters/__tests__/adapter-registry.test.ts @@ -153,7 +153,7 @@ describe('AdapterRegistry', () => { it('should include execution time in metadata', async () => { const result = await registry.executeTool('mock_echo', { message: 'test' }, context); - expect(result.metadata?.executionTime).toBeGreaterThanOrEqual(0); + expect(result.metadata?.duration_ms).toBeGreaterThanOrEqual(0); }); it('should handle tool execution errors', async () => { diff --git a/packages/mcp-server/src/adapters/__tests__/github-adapter.test.ts b/packages/mcp-server/src/adapters/__tests__/github-adapter.test.ts index b006424..8da055c 100644 --- a/packages/mcp-server/src/adapters/__tests__/github-adapter.test.ts +++ b/packages/mcp-server/src/adapters/__tests__/github-adapter.test.ts @@ -254,8 +254,10 @@ describe('GitHubAdapter', () => { expect(result.success).toBe(true); const content = (result.data as { content: string })?.content; - expect(content).toContain('🪙'); - expect(content).toMatch(/~\d+ tokens$/); + expect(content).toBeDefined(); + // Token info is now in metadata, not content + expect(result.metadata).toHaveProperty('tokens'); + expect(result.metadata?.tokens).toBeGreaterThan(0); }); }); diff --git a/packages/mcp-server/src/adapters/__tests__/mock-adapter.ts b/packages/mcp-server/src/adapters/__tests__/mock-adapter.ts index 82357a4..5f0bc72 100644 --- a/packages/mcp-server/src/adapters/__tests__/mock-adapter.ts +++ b/packages/mcp-server/src/adapters/__tests__/mock-adapter.ts @@ -100,7 +100,10 @@ export class MockAdapter extends ToolAdapter { timestamp: new Date().toISOString(), }, metadata: { - tokenEstimate: 10, + tokens: 10, + duration_ms: 1, + timestamp: new Date().toISOString(), + cached: false, }, }; } diff --git a/packages/mcp-server/src/adapters/__tests__/search-adapter.test.ts b/packages/mcp-server/src/adapters/__tests__/search-adapter.test.ts index f01f80d..f658871 100644 --- a/packages/mcp-server/src/adapters/__tests__/search-adapter.test.ts +++ b/packages/mcp-server/src/adapters/__tests__/search-adapter.test.ts @@ -260,9 +260,10 @@ describe('SearchAdapter', () => { expect(result.success).toBe(true); expect(result.data).toHaveProperty('query', 'authentication'); - expect(result.data).toHaveProperty('resultCount', 2); - expect(result.data).toHaveProperty('results'); - expect(result.data).toHaveProperty('tokenEstimate'); + expect(result.data).toHaveProperty('content'); + expect(result.metadata).toHaveProperty('tokens'); + expect(result.metadata).toHaveProperty('duration_ms'); + expect(result.metadata).toHaveProperty('results_total', 2); expect(mockIndexer.search).toHaveBeenCalledWith('authentication', { limit: 10, scoreThreshold: 0, @@ -278,9 +279,9 @@ describe('SearchAdapter', () => { ); expect(result.success).toBe(true); - expect(typeof result.data?.results).toBe('string'); - expect((result.data?.results as string).length).toBeGreaterThan(0); - expect(result.data?.results as string).toContain('authenticate'); + expect(typeof result.data?.content).toBe('string'); + expect((result.data?.content as string).length).toBeGreaterThan(0); + expect(result.data?.content as string).toContain('authenticate'); }); it('should respect limit parameter', async () => { @@ -297,7 +298,7 @@ describe('SearchAdapter', () => { limit: 3, scoreThreshold: 0, }); - expect(result.data?.resultCount).toBe(2); // Mock returns 2 results + expect(result.metadata?.results_total).toBe(2); // Mock returns 2 results }); it('should respect score threshold parameter', async () => { @@ -336,8 +337,8 @@ describe('SearchAdapter', () => { expect(compactResult.success).toBe(true); expect(verboseResult.success).toBe(true); - const compactTokens = compactResult.data?.tokenEstimate as number; - const verboseTokens = verboseResult.data?.tokenEstimate as number; + const compactTokens = compactResult.metadata?.tokens as number; + const verboseTokens = verboseResult.metadata?.tokens as number; expect(verboseTokens).toBeGreaterThan(compactTokens); }); @@ -354,8 +355,8 @@ describe('SearchAdapter', () => { ); expect(result.success).toBe(true); - expect(result.data?.resultCount).toBe(0); - expect(result.data?.results as string).toContain('No results'); + expect(result.metadata?.results_total).toBe(0); + expect(result.data?.content as string).toContain('No results'); }); }); diff --git a/packages/mcp-server/src/adapters/__tests__/status-adapter.test.ts b/packages/mcp-server/src/adapters/__tests__/status-adapter.test.ts index f5dbd10..7a2ca4d 100644 --- a/packages/mcp-server/src/adapters/__tests__/status-adapter.test.ts +++ b/packages/mcp-server/src/adapters/__tests__/status-adapter.test.ts @@ -373,10 +373,13 @@ describe('StatusAdapter', () => { it('should log completion', async () => { await adapter.execute({ section: 'summary' }, mockExecutionContext); - expect(mockExecutionContext.logger.info).toHaveBeenCalledWith('Status check completed', { - section: 'summary', - format: 'compact', - }); + expect(mockExecutionContext.logger.info).toHaveBeenCalledWith( + 'Status check completed', + expect.objectContaining({ + section: 'summary', + format: 'compact', + }) + ); }); }); }); diff --git a/packages/mcp-server/src/adapters/adapter-registry.ts b/packages/mcp-server/src/adapters/adapter-registry.ts index 17bdb33..14d6ccb 100644 --- a/packages/mcp-server/src/adapters/adapter-registry.ts +++ b/packages/mcp-server/src/adapters/adapter-registry.ts @@ -146,9 +146,9 @@ export class AdapterRegistry { const startTime = Date.now(); const result = await adapter.execute(args, context); - // Add execution time if not present - if (result.success && result.metadata) { - result.metadata.executionTime = Date.now() - startTime; + // Ensure duration is tracked (adapters should set this, but fallback here) + if (result.success && result.metadata && !result.metadata.duration_ms) { + result.metadata.duration_ms = Date.now() - startTime; } return result; diff --git a/packages/mcp-server/src/adapters/built-in/github-adapter.ts b/packages/mcp-server/src/adapters/built-in/github-adapter.ts index 660a988..c869945 100644 --- a/packages/mcp-server/src/adapters/built-in/github-adapter.ts +++ b/packages/mcp-server/src/adapters/built-in/github-adapter.ts @@ -291,13 +291,16 @@ export class GitHubAdapter extends ToolAdapter { } try { + const startTime = Date.now(); context.logger.debug('Executing GitHub action', { action, query, number }); let content: string; + let resultsTotal = 0; + let resultsReturned = 0; switch (action) { - case 'search': - content = await this.searchGitHub( + case 'search': { + const result = await this.searchGitHub( query as string, { type: type as 'issue' | 'pull_request' | undefined, @@ -308,15 +311,28 @@ export class GitHubAdapter extends ToolAdapter { }, format ); + content = result.content; + resultsTotal = result.resultsTotal; + resultsReturned = result.resultsReturned; break; + } case 'context': content = await this.getIssueContext(number as number, format); + resultsTotal = 1; + resultsReturned = 1; break; - case 'related': - content = await this.getRelated(number as number, limit, format); + case 'related': { + const result = await this.getRelated(number as number, limit, format); + content = result.content; + resultsTotal = result.resultsTotal; + resultsReturned = result.resultsReturned; break; + } } + const duration_ms = Date.now() - startTime; + const tokens = estimateTokensForText(content); + return { success: true, data: { @@ -325,6 +341,14 @@ export class GitHubAdapter extends ToolAdapter { format, content, }, + metadata: { + tokens, + duration_ms, + timestamp: new Date().toISOString(), + cached: false, + results_total: resultsTotal, + results_returned: resultsReturned, + }, }; } catch (error) { context.logger.error('GitHub action failed', { error }); @@ -370,35 +394,27 @@ export class GitHubAdapter extends ToolAdapter { query: string, options: GitHubSearchOptions, format: string - ): Promise { + ): Promise<{ content: string; resultsTotal: number; resultsReturned: number }> { const indexer = await this.ensureGitHubIndexer(); - // Debug logging to understand what's happening - console.log(`[GitHub Search] Query: "${query}", Options:`, JSON.stringify(options, null, 2)); - const results = await indexer.search(query, options); - console.log(`[GitHub Search] Found ${results.length} results`); - if (results.length > 0) { - console.log(`[GitHub Search] First result:`, { - title: results[0].document.title, - number: results[0].document.number, - score: results[0].score, - }); - } - if (results.length === 0) { - const noResultsMsg = + const content = '## GitHub Search Results\n\nNo matching issues or PRs found. Try:\n- Using different keywords\n- Removing filters (type, state, labels)\n- Re-indexing GitHub data with "dev gh index"'; - const tokens = estimateTokensForText(noResultsMsg); - return `${noResultsMsg}\n\n🪙 ~${tokens} tokens`; + return { content, resultsTotal: 0, resultsReturned: 0 }; } - if (format === 'verbose') { - return this.formatSearchVerbose(query, results, options); - } + const content = + format === 'verbose' + ? this.formatSearchVerbose(query, results, options) + : this.formatSearchCompact(query, results, options); - return this.formatSearchCompact(query, results, options); + return { + content, + resultsTotal: results.length, + resultsReturned: Math.min(results.length, options.limit ?? this.defaultLimit), + }; } /** @@ -439,7 +455,11 @@ export class GitHubAdapter extends ToolAdapter { /** * Find related issues and PRs */ - private async getRelated(number: number, limit: number, format: string): Promise { + private async getRelated( + number: number, + limit: number, + format: string + ): Promise<{ content: string; resultsTotal: number; resultsReturned: number }> { // First get the main issue/PR using the same logic as getIssueContext const indexer = await this.ensureGitHubIndexer(); @@ -468,14 +488,23 @@ export class GitHubAdapter extends ToolAdapter { const related = relatedResults.filter((r) => r.document.number !== number).slice(0, limit); if (related.length === 0) { - return `## Related Issues/PRs\n\n**#${number}: ${mainDoc.title}**\n\nNo related issues or PRs found.`; + return { + content: `## Related Issues/PRs\n\n**#${number}: ${mainDoc.title}**\n\nNo related issues or PRs found.`, + resultsTotal: 0, + resultsReturned: 0, + }; } - if (format === 'verbose') { - return this.formatRelatedVerbose(mainDoc, related); - } + const content = + format === 'verbose' + ? this.formatRelatedVerbose(mainDoc, related) + : this.formatRelatedCompact(mainDoc, related); - return this.formatRelatedCompact(mainDoc, related); + return { + content, + resultsTotal: related.length, + resultsReturned: related.length, + }; } /** @@ -513,9 +542,7 @@ export class GitHubAdapter extends ToolAdapter { lines.push('', `_...and ${results.length - 5} more results_`); } - const content = lines.join('\n'); - const tokens = estimateTokensForText(content); - return `${content}\n\n🪙 ~${tokens} tokens`; + return lines.join('\n'); } /** @@ -559,9 +586,7 @@ export class GitHubAdapter extends ToolAdapter { lines.push(''); } - const content = lines.join('\n'); - const tokens = estimateTokensForText(content); - return `${content}\n\n🪙 ~${tokens} tokens`; + return lines.join('\n'); } /** @@ -588,9 +613,7 @@ export class GitHubAdapter extends ToolAdapter { `**URL:** ${doc.url}`, ].filter(Boolean) as string[]; - const content = lines.join('\n'); - const tokens = estimateTokensForText(content); - return `${content}\n\n🪙 ~${tokens} tokens`; + return lines.join('\n'); } /** @@ -634,9 +657,7 @@ export class GitHubAdapter extends ToolAdapter { `**URL:** ${doc.url}`, ].filter(Boolean) as string[]; - const content = lines.join('\n'); - const tokens = estimateTokensForText(content); - return `${content}\n\n🪙 ~${tokens} tokens`; + return lines.join('\n'); } /** 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 8f9a5d3..68d2dff 100644 --- a/packages/mcp-server/src/adapters/built-in/search-adapter.ts +++ b/packages/mcp-server/src/adapters/built-in/search-adapter.ts @@ -166,6 +166,7 @@ export class SearchAdapter extends ToolAdapter { } try { + const startTime = Date.now(); context.logger.debug('Executing search', { query, format, limit, scoreThreshold }); // Perform search @@ -178,20 +179,30 @@ export class SearchAdapter extends ToolAdapter { const formatter = format === 'verbose' ? this.verboseFormatter : this.compactFormatter; const formatted = formatter.formatResults(results); + const duration_ms = Date.now() - startTime; + context.logger.info('Search completed', { query, resultCount: results.length, - tokenEstimate: formatted.tokenEstimate, + tokens: formatted.tokens, + duration_ms, }); return { success: true, data: { query, - resultCount: results.length, format, - results: formatted.content, - tokenEstimate: formatted.tokenEstimate, + content: formatted.content, + }, + metadata: { + tokens: formatted.tokens, + duration_ms, + timestamp: new Date().toISOString(), + cached: false, + results_total: results.length, + results_returned: Math.min(results.length, limit as number), + results_truncated: results.length > (limit as number), }, }; } catch (error) { 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 96bf9be..fb25b29 100644 --- a/packages/mcp-server/src/adapters/built-in/status-adapter.ts +++ b/packages/mcp-server/src/adapters/built-in/status-adapter.ts @@ -7,6 +7,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import type { RepositoryIndexer } from '@lytics/dev-agent-core'; import { GitHubIndexer } from '@lytics/dev-agent-subagents'; +import { estimateTokensForText } from '../../formatters/utils'; import { ToolAdapter } from '../tool-adapter'; import type { AdapterContext, ToolDefinition, ToolExecutionContext, ToolResult } from '../types'; @@ -228,6 +229,7 @@ export class StatusAdapter extends ToolAdapter { } try { + const startTime = Date.now(); context.logger.debug('Executing status check', { section, format }); // Generate status content based on section @@ -237,7 +239,10 @@ export class StatusAdapter extends ToolAdapter { context ); - context.logger.info('Status check completed', { section, format }); + const duration_ms = Date.now() - startTime; + const tokens = estimateTokensForText(content); + + context.logger.info('Status check completed', { section, format, duration_ms }); return { success: true, @@ -246,6 +251,12 @@ export class StatusAdapter extends ToolAdapter { format, content, }, + metadata: { + tokens, + duration_ms, + timestamp: new Date().toISOString(), + cached: false, + }, }; } catch (error) { context.logger.error('Status check failed', { error }); From 38d60af06b04d081d5e0c74cd9911af3706d7761 Mon Sep 17 00:00:00 2001 From: prosdev Date: Wed, 26 Nov 2025 08:21:48 -0800 Subject: [PATCH 4/4] test(core): fix flaky scanner tests by narrowing scope - Narrow repoRoot scope in slow tests to avoid timeouts - Add explicit timeout of 10s for integration-style tests - Tests now complete in <1s instead of timing out --- packages/core/src/scanner/__tests__/scanner.test.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/core/src/scanner/__tests__/scanner.test.ts b/packages/core/src/scanner/__tests__/scanner.test.ts index a8eacce..8e3625d 100644 --- a/packages/core/src/scanner/__tests__/scanner.test.ts +++ b/packages/core/src/scanner/__tests__/scanner.test.ts @@ -71,14 +71,14 @@ describe('Scanner', () => { it('should handle excluded patterns', async () => { const result = await scanRepository({ repoRoot, - include: ['packages/**/*.ts'], + include: ['packages/core/src/scanner/*.ts'], // Narrow scope for faster test exclude: ['**/node_modules/**', '**/dist/**', '**/*.test.ts'], }); // Should not include test files const testFiles = result.documents.filter((d) => d.metadata.file.includes('.test.ts')); expect(testFiles.length).toBe(0); - }); + }, 10000); it('should provide scanner capabilities', () => { const registry = createDefaultRegistry(); @@ -249,18 +249,19 @@ describe('Scanner', () => { const registry = createDefaultRegistry(); // Scan without include patterns - should auto-detect + // Use a narrow repoRoot for faster test const result = await registry.scanRepository({ - repoRoot, + repoRoot: `${repoRoot}/packages/core/src/scanner`, exclude: ['**/*.test.ts', '**/node_modules/**', '**/dist/**'], }); // Should find files automatically based on registered scanners expect(result.stats.filesScanned).toBeGreaterThan(0); - }); + }, 10000); it('should use default exclusions', async () => { const result = await scanRepository({ - repoRoot, + repoRoot: `${repoRoot}/packages/core/src/scanner`, // Narrow scope for faster test include: ['**/*.ts', '**/*.md'], // Not specifying exclude - should use defaults }); @@ -274,7 +275,7 @@ describe('Scanner', () => { // Should not include dist files const distFiles = result.documents.filter((d) => d.metadata.file.includes('dist/')); expect(distFiles.length).toBe(0); - }); + }, 10000); it('should handle mixed language repositories', async () => { const result = await scanRepository({