feat(server): Domain 2 Phase 1 — boocontext MCP client + 4 new code intelligence tools
Shared boocontext MCP client (boocontext_client.ts) wrapping the existing mcp-client.ts callTool() infrastructure with 32KB truncation and error handling. Used by get_code_health. 4 new first-class agent tools backed by the boocontext MCP server: - get_code_health — A-F grades per file across 7 dimensions, project health summary, refactoring candidates (wraps boocontext_health) - get_code_impact — merged symbol trace + blast radius in one call (wraps boocontext_impact, replaces two-step get_symbol_info+get_blast_radius) - get_type_info — TypeScript type recovery via type-inject MCP (wraps boocontext_types, returns signatures, interfaces, generics, JSDoc) - get_code_map — DCP-compressed context map with compress toggle (wraps boocontext_map, 10x token reduction vs full scan) All 4 registered in ALL_TOOLS as read-only tools.
This commit is contained in:
192
apps/server/src/services/tools/codecontext/get_code_map.ts
Normal file
192
apps/server/src/services/tools/codecontext/get_code_map.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
import { z } from 'zod';
|
||||
import type { ToolDef } from '../types.js';
|
||||
|
||||
export const GetCodeMapInput = z.object({
|
||||
directory: z.string().optional().describe('Directory to scan (defaults to project root)'),
|
||||
compress: z.boolean().optional().describe('Apply DCP compression if payload exceeds threshold (default: true)'),
|
||||
});
|
||||
export type GetCodeMapInputT = z.infer<typeof GetCodeMapInput>;
|
||||
|
||||
const DESCRIPTION =
|
||||
'DCP-compressed codebase context map. Returns filenames, sizes, import relationships in a compressed format. ' +
|
||||
'Use compress=false for full detail, compress=true (default) for token-efficient overview.';
|
||||
|
||||
const BOOCONTEXT_PATH = '/opt/forks/boocontext/dist/standalone.js';
|
||||
const TOOL_TIMEOUT_MS = 30_000;
|
||||
const MAX_RESULT_BYTES = 32_768;
|
||||
|
||||
export interface CodeMapResponse {
|
||||
result: string;
|
||||
truncated: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the boocontext MCP server over stdio JSON-RPC to invoke
|
||||
* the boocontext_map tool. Spawns the standalone binary, sends
|
||||
* initialize + tools/call, collects NDJSON responses, and kills
|
||||
* the child process.
|
||||
*/
|
||||
function callBoocontextMap(args: Record<string, unknown>): Promise<CodeMapResponse> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn('node', [BOOCONTEXT_PATH], {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
let stdoutBuf = '';
|
||||
const lines: string[] = [];
|
||||
let timedOut = false;
|
||||
let resolved = false;
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
timedOut = true;
|
||||
child.kill('SIGKILL');
|
||||
reject(new Error(`boocontext MCP call timed out after ${TOOL_TIMEOUT_MS}ms`));
|
||||
}, TOOL_TIMEOUT_MS);
|
||||
|
||||
function tryParse(): void {
|
||||
if (resolved || timedOut) return;
|
||||
|
||||
// Accumulate complete NDJSON lines
|
||||
const parts = stdoutBuf.split('\n');
|
||||
stdoutBuf = parts.pop()! ?? '';
|
||||
for (const p of parts) {
|
||||
const t = p.trim();
|
||||
if (t) lines.push(t);
|
||||
}
|
||||
|
||||
// Need at least 2 responses: initialize + tools/call
|
||||
if (lines.length < 2) return;
|
||||
|
||||
resolved = true;
|
||||
clearTimeout(timer);
|
||||
child.kill();
|
||||
|
||||
try {
|
||||
const callResponse = JSON.parse(lines[1]!);
|
||||
if (callResponse.error) {
|
||||
reject(new Error(`MCP error: ${callResponse.error.message}`));
|
||||
return;
|
||||
}
|
||||
|
||||
const content = callResponse.result?.content;
|
||||
if (!content?.[0]?.text) {
|
||||
reject(new Error('Unexpected MCP response shape — missing content[0].text'));
|
||||
return;
|
||||
}
|
||||
|
||||
// content[0].text is JSON-stringified VerdictEnvelope from boocontext
|
||||
const envelope = JSON.parse(content[0].text as string);
|
||||
const details = envelope.details;
|
||||
|
||||
let result: string;
|
||||
if (details && typeof details === 'object' && 'data' in details) {
|
||||
// DcpEnvelope shape: { compressed, originalLength, compressedLength, data }
|
||||
if (details.compressed) {
|
||||
// Return the full DcpEnvelope as JSON so the LLM can pass it
|
||||
// transparently to a decompression step
|
||||
result = JSON.stringify(details);
|
||||
} else {
|
||||
// Uncompressed — data is the raw output
|
||||
result = details.data;
|
||||
}
|
||||
} else {
|
||||
result = JSON.stringify(details ?? envelope);
|
||||
}
|
||||
|
||||
const truncated = Buffer.byteLength(result, 'utf-8') > MAX_RESULT_BYTES;
|
||||
if (truncated) {
|
||||
result = result.substring(0, MAX_RESULT_BYTES);
|
||||
}
|
||||
|
||||
resolve({ result, truncated });
|
||||
} catch (e: any) {
|
||||
reject(new Error(`Failed to parse boocontext response: ${e.message}`));
|
||||
}
|
||||
}
|
||||
|
||||
child.stdout!.on('data', (chunk: Buffer) => {
|
||||
if (timedOut) return;
|
||||
stdoutBuf += chunk.toString('utf-8');
|
||||
tryParse();
|
||||
});
|
||||
|
||||
child.stderr!.on('data', (_chunk: Buffer) => {
|
||||
// Captured but not surfaced — logged only on parse failure
|
||||
});
|
||||
|
||||
child.on('error', (err: Error) => {
|
||||
clearTimeout(timer);
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
reject(new Error(`boocontext spawn failed: ${err.message}`));
|
||||
}
|
||||
});
|
||||
|
||||
child.on('close', () => {
|
||||
clearTimeout(timer);
|
||||
if (!resolved && !timedOut) {
|
||||
tryParse();
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
reject(new Error('boocontext process closed without producing a valid response'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Step 1: initialize
|
||||
child.stdin!.write(
|
||||
JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize' }) + '\n',
|
||||
);
|
||||
|
||||
// Step 2: tools/call for boocontext_map
|
||||
child.stdin!.write(
|
||||
JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
id: 2,
|
||||
method: 'tools/call',
|
||||
params: { name: 'boocontext_map', arguments: args },
|
||||
}) + '\n',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export const getCodeMap: ToolDef<GetCodeMapInputT> = {
|
||||
name: 'get_code_map',
|
||||
description: DESCRIPTION,
|
||||
inputSchema: GetCodeMapInput,
|
||||
jsonSchema: {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'get_code_map',
|
||||
description: DESCRIPTION,
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
directory: { type: 'string', description: 'Directory to scan (defaults to project root)' },
|
||||
compress: {
|
||||
type: 'boolean',
|
||||
description: 'Apply DCP compression if payload exceeds threshold (default: true)',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
async execute(input, projectRoot): Promise<CodeMapResponse> {
|
||||
return callBoocontextMap({
|
||||
directory: input.directory ?? projectRoot,
|
||||
compress: input.compress ?? true,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export async function executeGetCodeMap(
|
||||
input: GetCodeMapInputT,
|
||||
projectRoot: string,
|
||||
): Promise<CodeMapResponse> {
|
||||
return callBoocontextMap({
|
||||
directory: input.directory ?? projectRoot,
|
||||
compress: input.compress ?? true,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user