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; 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): Promise { 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 = { 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 { return callBoocontextMap({ directory: input.directory ?? projectRoot, compress: input.compress ?? true, }); }, }; export async function executeGetCodeMap( input: GetCodeMapInputT, projectRoot: string, ): Promise { return callBoocontextMap({ directory: input.directory ?? projectRoot, compress: input.compress ?? true, }); }