Skip to content

Commit b47d7dd

Browse files
committed
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
1 parent 25f15e5 commit b47d7dd

File tree

9 files changed

+121
-69
lines changed

9 files changed

+121
-69
lines changed

packages/mcp-server/src/adapters/__tests__/adapter-registry.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ describe('AdapterRegistry', () => {
153153
it('should include execution time in metadata', async () => {
154154
const result = await registry.executeTool('mock_echo', { message: 'test' }, context);
155155

156-
expect(result.metadata?.executionTime).toBeGreaterThanOrEqual(0);
156+
expect(result.metadata?.duration_ms).toBeGreaterThanOrEqual(0);
157157
});
158158

159159
it('should handle tool execution errors', async () => {

packages/mcp-server/src/adapters/__tests__/github-adapter.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -254,8 +254,10 @@ describe('GitHubAdapter', () => {
254254

255255
expect(result.success).toBe(true);
256256
const content = (result.data as { content: string })?.content;
257-
expect(content).toContain('🪙');
258-
expect(content).toMatch(/~\d+ tokens$/);
257+
expect(content).toBeDefined();
258+
// Token info is now in metadata, not content
259+
expect(result.metadata).toHaveProperty('tokens');
260+
expect(result.metadata?.tokens).toBeGreaterThan(0);
259261
});
260262
});
261263

packages/mcp-server/src/adapters/__tests__/mock-adapter.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,10 @@ export class MockAdapter extends ToolAdapter {
100100
timestamp: new Date().toISOString(),
101101
},
102102
metadata: {
103-
tokenEstimate: 10,
103+
tokens: 10,
104+
duration_ms: 1,
105+
timestamp: new Date().toISOString(),
106+
cached: false,
104107
},
105108
};
106109
}

packages/mcp-server/src/adapters/__tests__/search-adapter.test.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -260,9 +260,10 @@ describe('SearchAdapter', () => {
260260

261261
expect(result.success).toBe(true);
262262
expect(result.data).toHaveProperty('query', 'authentication');
263-
expect(result.data).toHaveProperty('resultCount', 2);
264-
expect(result.data).toHaveProperty('results');
265-
expect(result.data).toHaveProperty('tokenEstimate');
263+
expect(result.data).toHaveProperty('content');
264+
expect(result.metadata).toHaveProperty('tokens');
265+
expect(result.metadata).toHaveProperty('duration_ms');
266+
expect(result.metadata).toHaveProperty('results_total', 2);
266267
expect(mockIndexer.search).toHaveBeenCalledWith('authentication', {
267268
limit: 10,
268269
scoreThreshold: 0,
@@ -278,9 +279,9 @@ describe('SearchAdapter', () => {
278279
);
279280

280281
expect(result.success).toBe(true);
281-
expect(typeof result.data?.results).toBe('string');
282-
expect((result.data?.results as string).length).toBeGreaterThan(0);
283-
expect(result.data?.results as string).toContain('authenticate');
282+
expect(typeof result.data?.content).toBe('string');
283+
expect((result.data?.content as string).length).toBeGreaterThan(0);
284+
expect(result.data?.content as string).toContain('authenticate');
284285
});
285286

286287
it('should respect limit parameter', async () => {
@@ -297,7 +298,7 @@ describe('SearchAdapter', () => {
297298
limit: 3,
298299
scoreThreshold: 0,
299300
});
300-
expect(result.data?.resultCount).toBe(2); // Mock returns 2 results
301+
expect(result.metadata?.results_total).toBe(2); // Mock returns 2 results
301302
});
302303

303304
it('should respect score threshold parameter', async () => {
@@ -336,8 +337,8 @@ describe('SearchAdapter', () => {
336337
expect(compactResult.success).toBe(true);
337338
expect(verboseResult.success).toBe(true);
338339

339-
const compactTokens = compactResult.data?.tokenEstimate as number;
340-
const verboseTokens = verboseResult.data?.tokenEstimate as number;
340+
const compactTokens = compactResult.metadata?.tokens as number;
341+
const verboseTokens = verboseResult.metadata?.tokens as number;
341342

342343
expect(verboseTokens).toBeGreaterThan(compactTokens);
343344
});
@@ -354,8 +355,8 @@ describe('SearchAdapter', () => {
354355
);
355356

356357
expect(result.success).toBe(true);
357-
expect(result.data?.resultCount).toBe(0);
358-
expect(result.data?.results as string).toContain('No results');
358+
expect(result.metadata?.results_total).toBe(0);
359+
expect(result.data?.content as string).toContain('No results');
359360
});
360361
});
361362

packages/mcp-server/src/adapters/__tests__/status-adapter.test.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -373,10 +373,13 @@ describe('StatusAdapter', () => {
373373
it('should log completion', async () => {
374374
await adapter.execute({ section: 'summary' }, mockExecutionContext);
375375

376-
expect(mockExecutionContext.logger.info).toHaveBeenCalledWith('Status check completed', {
377-
section: 'summary',
378-
format: 'compact',
379-
});
376+
expect(mockExecutionContext.logger.info).toHaveBeenCalledWith(
377+
'Status check completed',
378+
expect.objectContaining({
379+
section: 'summary',
380+
format: 'compact',
381+
})
382+
);
380383
});
381384
});
382385
});

packages/mcp-server/src/adapters/adapter-registry.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,9 @@ export class AdapterRegistry {
146146
const startTime = Date.now();
147147
const result = await adapter.execute(args, context);
148148

149-
// Add execution time if not present
150-
if (result.success && result.metadata) {
151-
result.metadata.executionTime = Date.now() - startTime;
149+
// Ensure duration is tracked (adapters should set this, but fallback here)
150+
if (result.success && result.metadata && !result.metadata.duration_ms) {
151+
result.metadata.duration_ms = Date.now() - startTime;
152152
}
153153

154154
return result;

packages/mcp-server/src/adapters/built-in/github-adapter.ts

Lines changed: 63 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -291,13 +291,16 @@ export class GitHubAdapter extends ToolAdapter {
291291
}
292292

293293
try {
294+
const startTime = Date.now();
294295
context.logger.debug('Executing GitHub action', { action, query, number });
295296

296297
let content: string;
298+
let resultsTotal = 0;
299+
let resultsReturned = 0;
297300

298301
switch (action) {
299-
case 'search':
300-
content = await this.searchGitHub(
302+
case 'search': {
303+
const result = await this.searchGitHub(
301304
query as string,
302305
{
303306
type: type as 'issue' | 'pull_request' | undefined,
@@ -308,15 +311,28 @@ export class GitHubAdapter extends ToolAdapter {
308311
},
309312
format
310313
);
314+
content = result.content;
315+
resultsTotal = result.resultsTotal;
316+
resultsReturned = result.resultsReturned;
311317
break;
318+
}
312319
case 'context':
313320
content = await this.getIssueContext(number as number, format);
321+
resultsTotal = 1;
322+
resultsReturned = 1;
314323
break;
315-
case 'related':
316-
content = await this.getRelated(number as number, limit, format);
324+
case 'related': {
325+
const result = await this.getRelated(number as number, limit, format);
326+
content = result.content;
327+
resultsTotal = result.resultsTotal;
328+
resultsReturned = result.resultsReturned;
317329
break;
330+
}
318331
}
319332

333+
const duration_ms = Date.now() - startTime;
334+
const tokens = estimateTokensForText(content);
335+
320336
return {
321337
success: true,
322338
data: {
@@ -325,6 +341,14 @@ export class GitHubAdapter extends ToolAdapter {
325341
format,
326342
content,
327343
},
344+
metadata: {
345+
tokens,
346+
duration_ms,
347+
timestamp: new Date().toISOString(),
348+
cached: false,
349+
results_total: resultsTotal,
350+
results_returned: resultsReturned,
351+
},
328352
};
329353
} catch (error) {
330354
context.logger.error('GitHub action failed', { error });
@@ -370,35 +394,27 @@ export class GitHubAdapter extends ToolAdapter {
370394
query: string,
371395
options: GitHubSearchOptions,
372396
format: string
373-
): Promise<string> {
397+
): Promise<{ content: string; resultsTotal: number; resultsReturned: number }> {
374398
const indexer = await this.ensureGitHubIndexer();
375399

376-
// Debug logging to understand what's happening
377-
console.log(`[GitHub Search] Query: "${query}", Options:`, JSON.stringify(options, null, 2));
378-
379400
const results = await indexer.search(query, options);
380401

381-
console.log(`[GitHub Search] Found ${results.length} results`);
382-
if (results.length > 0) {
383-
console.log(`[GitHub Search] First result:`, {
384-
title: results[0].document.title,
385-
number: results[0].document.number,
386-
score: results[0].score,
387-
});
388-
}
389-
390402
if (results.length === 0) {
391-
const noResultsMsg =
403+
const content =
392404
'## 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"';
393-
const tokens = estimateTokensForText(noResultsMsg);
394-
return `${noResultsMsg}\n\n🪙 ~${tokens} tokens`;
405+
return { content, resultsTotal: 0, resultsReturned: 0 };
395406
}
396407

397-
if (format === 'verbose') {
398-
return this.formatSearchVerbose(query, results, options);
399-
}
408+
const content =
409+
format === 'verbose'
410+
? this.formatSearchVerbose(query, results, options)
411+
: this.formatSearchCompact(query, results, options);
400412

401-
return this.formatSearchCompact(query, results, options);
413+
return {
414+
content,
415+
resultsTotal: results.length,
416+
resultsReturned: Math.min(results.length, options.limit ?? this.defaultLimit),
417+
};
402418
}
403419

404420
/**
@@ -439,7 +455,11 @@ export class GitHubAdapter extends ToolAdapter {
439455
/**
440456
* Find related issues and PRs
441457
*/
442-
private async getRelated(number: number, limit: number, format: string): Promise<string> {
458+
private async getRelated(
459+
number: number,
460+
limit: number,
461+
format: string
462+
): Promise<{ content: string; resultsTotal: number; resultsReturned: number }> {
443463
// First get the main issue/PR using the same logic as getIssueContext
444464
const indexer = await this.ensureGitHubIndexer();
445465

@@ -468,14 +488,23 @@ export class GitHubAdapter extends ToolAdapter {
468488
const related = relatedResults.filter((r) => r.document.number !== number).slice(0, limit);
469489

470490
if (related.length === 0) {
471-
return `## Related Issues/PRs\n\n**#${number}: ${mainDoc.title}**\n\nNo related issues or PRs found.`;
491+
return {
492+
content: `## Related Issues/PRs\n\n**#${number}: ${mainDoc.title}**\n\nNo related issues or PRs found.`,
493+
resultsTotal: 0,
494+
resultsReturned: 0,
495+
};
472496
}
473497

474-
if (format === 'verbose') {
475-
return this.formatRelatedVerbose(mainDoc, related);
476-
}
498+
const content =
499+
format === 'verbose'
500+
? this.formatRelatedVerbose(mainDoc, related)
501+
: this.formatRelatedCompact(mainDoc, related);
477502

478-
return this.formatRelatedCompact(mainDoc, related);
503+
return {
504+
content,
505+
resultsTotal: related.length,
506+
resultsReturned: related.length,
507+
};
479508
}
480509

481510
/**
@@ -513,9 +542,7 @@ export class GitHubAdapter extends ToolAdapter {
513542
lines.push('', `_...and ${results.length - 5} more results_`);
514543
}
515544

516-
const content = lines.join('\n');
517-
const tokens = estimateTokensForText(content);
518-
return `${content}\n\n🪙 ~${tokens} tokens`;
545+
return lines.join('\n');
519546
}
520547

521548
/**
@@ -559,9 +586,7 @@ export class GitHubAdapter extends ToolAdapter {
559586
lines.push('');
560587
}
561588

562-
const content = lines.join('\n');
563-
const tokens = estimateTokensForText(content);
564-
return `${content}\n\n🪙 ~${tokens} tokens`;
589+
return lines.join('\n');
565590
}
566591

567592
/**
@@ -588,9 +613,7 @@ export class GitHubAdapter extends ToolAdapter {
588613
`**URL:** ${doc.url}`,
589614
].filter(Boolean) as string[];
590615

591-
const content = lines.join('\n');
592-
const tokens = estimateTokensForText(content);
593-
return `${content}\n\n🪙 ~${tokens} tokens`;
616+
return lines.join('\n');
594617
}
595618

596619
/**
@@ -634,9 +657,7 @@ export class GitHubAdapter extends ToolAdapter {
634657
`**URL:** ${doc.url}`,
635658
].filter(Boolean) as string[];
636659

637-
const content = lines.join('\n');
638-
const tokens = estimateTokensForText(content);
639-
return `${content}\n\n🪙 ~${tokens} tokens`;
660+
return lines.join('\n');
640661
}
641662

642663
/**

packages/mcp-server/src/adapters/built-in/search-adapter.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ export class SearchAdapter extends ToolAdapter {
166166
}
167167

168168
try {
169+
const startTime = Date.now();
169170
context.logger.debug('Executing search', { query, format, limit, scoreThreshold });
170171

171172
// Perform search
@@ -178,20 +179,30 @@ export class SearchAdapter extends ToolAdapter {
178179
const formatter = format === 'verbose' ? this.verboseFormatter : this.compactFormatter;
179180
const formatted = formatter.formatResults(results);
180181

182+
const duration_ms = Date.now() - startTime;
183+
181184
context.logger.info('Search completed', {
182185
query,
183186
resultCount: results.length,
184-
tokenEstimate: formatted.tokenEstimate,
187+
tokens: formatted.tokens,
188+
duration_ms,
185189
});
186190

187191
return {
188192
success: true,
189193
data: {
190194
query,
191-
resultCount: results.length,
192195
format,
193-
results: formatted.content,
194-
tokenEstimate: formatted.tokenEstimate,
196+
content: formatted.content,
197+
},
198+
metadata: {
199+
tokens: formatted.tokens,
200+
duration_ms,
201+
timestamp: new Date().toISOString(),
202+
cached: false,
203+
results_total: results.length,
204+
results_returned: Math.min(results.length, limit as number),
205+
results_truncated: results.length > (limit as number),
195206
},
196207
};
197208
} catch (error) {

0 commit comments

Comments
 (0)