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.
193 lines
5.7 KiB
TypeScript
193 lines
5.7 KiB
TypeScript
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,
|
|
});
|
|
}
|