extract stream parser module with tests, add mcp tool call logging
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,8 +13,8 @@
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import { z } from 'zod';
|
||||
import { readdirSync, existsSync, readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { readdirSync, existsSync, readFileSync, appendFileSync, mkdirSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
|
||||
// ── Types ──
|
||||
|
||||
@@ -186,6 +186,31 @@ function buildZodSchema(inputs: Record<string, ToolParam>): z.ZodRawShape {
|
||||
return shape;
|
||||
}
|
||||
|
||||
// ── Logging ──
|
||||
|
||||
const LOG_FILE = process.env.MCP_TOOLS_LOG ?? '';
|
||||
|
||||
function logToolCall(toolName: string, durationMs: number, success: boolean, error?: string): void {
|
||||
const ts = new Date().toISOString();
|
||||
const status = success ? 'ok' : 'error';
|
||||
const line = error
|
||||
? `${ts}\t${toolName}\t${status}\t${durationMs}ms\t${error}\n`
|
||||
: `${ts}\t${toolName}\t${status}\t${durationMs}ms\n`;
|
||||
|
||||
// Always log to stderr for process-level visibility
|
||||
console.error(`[mcp-tools] ${toolName} ${status} (${durationMs}ms)${error ? ': ' + error : ''}`);
|
||||
|
||||
// Write to log file if configured
|
||||
if (LOG_FILE) {
|
||||
try {
|
||||
mkdirSync(dirname(LOG_FILE), { recursive: true });
|
||||
appendFileSync(LOG_FILE, line);
|
||||
} catch {
|
||||
// Don't fail tool calls over logging
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Main ──
|
||||
|
||||
const rawDirs = process.env.PI_TOOLS_DIRS ?? '';
|
||||
@@ -218,11 +243,14 @@ for (const { entryFile, meta } of tools) {
|
||||
inputSchema: z.object(schema),
|
||||
},
|
||||
async (params) => {
|
||||
const start = performance.now();
|
||||
let executeFn: Function | undefined;
|
||||
try {
|
||||
const mod = await import(capturedEntry);
|
||||
executeFn = mod.execute ?? mod.default?.execute;
|
||||
} catch (err) {
|
||||
const ms = Math.round(performance.now() - start);
|
||||
logToolCall(toolName, ms, false, `load failed: ${String(err)}`);
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Failed to load ${toolName}: ${String(err)}` }],
|
||||
isError: true,
|
||||
@@ -230,14 +258,27 @@ for (const { entryFile, meta } of tools) {
|
||||
}
|
||||
|
||||
if (typeof executeFn !== 'function') {
|
||||
const ms = Math.round(performance.now() - start);
|
||||
logToolCall(toolName, ms, false, 'no execute function');
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `${toolName}/index.ts must export an "execute" function` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await executeFn('mcp', params, undefined, undefined);
|
||||
return result;
|
||||
try {
|
||||
const result = await executeFn('mcp', params, undefined, undefined);
|
||||
const ms = Math.round(performance.now() - start);
|
||||
logToolCall(toolName, ms, !result.isError, result.isError ? 'tool returned error' : undefined);
|
||||
return result;
|
||||
} catch (err) {
|
||||
const ms = Math.round(performance.now() - start);
|
||||
logToolCall(toolName, ms, false, String(err));
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `${toolName} threw: ${String(err)}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user