Files
boocode/apps/server/src/services/tools/codecontext/get_code_impact.ts
indifferentketchup 3c5b2c2bcf 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.
2026-06-08 00:45:46 +00:00

229 lines
6.6 KiB
TypeScript

import { spawn } from 'node:child_process';
import { resolve } from 'node:path';
import { z } from 'zod';
import type { ToolDef } from '../types.js';
import type { CodecontextResponse } from '../../codecontext_client.js';
// ======================= MCP Client =======================
const BOOCONTEXT_PATH = resolve('/opt/forks/boocontext/dist/standalone.js');
const TOOL_CALL_TIMEOUT_MS = 60_000;
interface JsonRpcMessage {
jsonrpc: '2.0';
id?: number | string;
result?: {
content?: Array<{ type: string; text: string }>;
};
error?: { code?: number; message: string };
}
/**
* Single-shot MCP JSON-RPC client for boocontext.
* Spawns the process, sends initialize + tools/call over NDJSON, returns the
* text result from the content array. The boocontext MCP server auto-detects
* newline-delimited JSON transport when the first input lacks Content-Length
* headers, which is exactly what we send.
*/
async function callBoocontext(
toolName: string,
args: Record<string, unknown>,
): Promise<string> {
return new Promise<string>((resolvePromise, reject) => {
const child = spawn(process.execPath, [BOOCONTEXT_PATH], {
stdio: ['pipe', 'pipe', 'pipe'],
timeout: TOOL_CALL_TIMEOUT_MS,
});
let stdout = '';
let stderr = '';
let resolved = false;
function finalize(err?: Error, result?: string): void {
if (resolved) return;
resolved = true;
if (err) reject(err);
else resolvePromise(result!);
child.kill();
}
child.stdout!.on('data', (chunk: Buffer) => {
stdout += chunk.toString();
});
child.stderr!.on('data', (chunk: Buffer) => {
stderr += chunk.toString();
});
child.on('error', (err: Error) => {
finalize(new Error(`boocontext spawn error: ${err.message}`));
});
child.on('close', (code: number | null) => {
if (resolved) return;
// Parse newline-delimited JSON responses from stdout
const lines = stdout.split('\n').filter((l) => l.trim().length > 0);
let toolText: string | undefined;
let toolError: string | undefined;
for (const line of lines) {
try {
const msg = JSON.parse(line) as JsonRpcMessage;
if (msg.id === 2) {
if (msg.error) {
toolError = msg.error.message ?? 'boocontext tool call failed';
} else if (msg.result?.content?.[0]?.text !== undefined) {
toolText = msg.result.content[0].text;
}
}
} catch {
// skip malformed JSON lines
}
}
if (toolError) {
finalize(new Error(toolError));
} else if (toolText !== undefined) {
finalize(undefined, toolText);
} else {
const errSuffix =
stderr.length > 0 ? ` stderr: ${stderr.slice(0, 500)}` : '';
finalize(
new Error(`boocontext MCP call failed (exit ${code})${errSuffix}`),
);
}
});
// Step 1: initialize — establishes MCP protocol version + capabilities
child.stdin!.write(
JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'initialize',
params: {
protocolVersion: '2024-11-05',
capabilities: {},
clientInfo: { name: 'boocode-server', version: '1.0.0' },
},
}) + '\n',
);
// Step 2: tools/call — invoke the named boocontext tool
child.stdin!.write(
JSON.stringify({
jsonrpc: '2.0',
id: 2,
method: 'tools/call',
params: { name: toolName, arguments: args },
}) + '\n',
);
child.stdin!.end();
// Safety timeout — prevent hung processes
setTimeout(() => {
finalize(
new Error(
`boocontext call timed out after ${TOOL_CALL_TIMEOUT_MS}ms`,
),
);
}, TOOL_CALL_TIMEOUT_MS);
});
}
// ======================= Tool Definition =======================
const TRUNCATION_LIMIT = 32_000;
export const GetCodeImpactInput = z.object({
symbol: z.string().min(1).describe('Symbol name for TSA trace_impact'),
file: z.string().optional().describe('File path for codesight blast_radius'),
directory: z
.string()
.optional()
.describe('Directory (defaults to project root)'),
depth: z
.number()
.int()
.min(1)
.max(5)
.optional()
.describe('Max blast-radius traversal depth (default 1)'),
});
export type GetCodeImpactInputT = z.infer<typeof GetCodeImpactInput>;
const DESCRIPTION =
'Impact analysis. Merges symbol-level call trace with file-level blast radius. ' +
'Use before making changes to understand change propagation. ' +
'Single call replaces separate get_symbol_info + get_blast_radius steps.';
/**
* Standalone execute function — calls the boocontext MCP `boocontext_impact`
* tool via a short-lived child process, then wraps the result in the standard
* CodecontextResponse shape with inline truncation at 32 KB.
*/
export async function executeGetCodeImpact(
input: GetCodeImpactInputT,
projectPath: string,
): Promise<CodecontextResponse> {
const args: Record<string, unknown> = {
symbol: input.symbol,
directory: input.directory ?? projectPath,
};
if (input.file) args['file'] = input.file;
const text = await callBoocontext('boocontext_impact', args);
// Inline truncation matching codecontext_client.ts patterns (32 KB ceiling).
if (text.length > TRUNCATION_LIMIT) {
const sliced = text.slice(0, TRUNCATION_LIMIT);
const omitted = text.length - TRUNCATION_LIMIT;
return {
result: `${sliced}\n\n[truncated, ${omitted} chars omitted; narrow with symbol or file parameters]`,
truncated: true,
};
}
return { result: text, truncated: false };
}
export const getCodeImpact: ToolDef<GetCodeImpactInputT> = {
name: 'get_code_impact',
description: DESCRIPTION,
inputSchema: GetCodeImpactInput,
jsonSchema: {
type: 'function',
function: {
name: 'get_code_impact',
description: DESCRIPTION,
parameters: {
type: 'object',
properties: {
symbol: {
type: 'string',
description: 'Symbol name for TSA trace_impact',
},
file: {
type: 'string',
description: 'File path for codesight blast_radius',
},
directory: {
type: 'string',
description: 'Directory (defaults to project root)',
},
depth: {
type: 'number',
description: 'Max blast-radius traversal depth (default 1)',
},
},
required: ['symbol'],
additionalProperties: false,
},
},
},
execute(input, projectRoot) {
return executeGetCodeImpact(input, projectRoot);
},
};