From 5775eb905358ec644cb1943e153503bcd830fa37 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Thu, 1 Oct 2026 23:02:01 -0700 Subject: [PATCH] feat: log local MCP requests to stderr by method The stdio server writes each request's method, and the tool or resource it names, to stderr, which clients such as Claude Desktop copy into their logs. Arguments and results are never logged. --- server/mcp/stdio.js | 25 ++++++++++++++++++++++--- src/tools/localServer.test.mjs | 17 +++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/server/mcp/stdio.js b/server/mcp/stdio.js index c372e1f6c..563ef30ce 100644 --- a/server/mcp/stdio.js +++ b/server/mcp/stdio.js @@ -14,13 +14,29 @@ import { parseErrorResponse } from '../../src/tools/mcp/index.js'; import { createLocalMcpServer } from './server.js'; import { DEFAULT_API_BASE } from './services.js'; +/** + * A one-line description of a request for the diagnostic log: its method, + * and the tool or resource it names. Arguments and data are never logged. + */ +export function describeRequest(message) { + const method = typeof message?.method === 'string' ? message.method : null; + if (!method) return null; + const subject = + method === 'tools/call' + ? message.params?.name + : method === 'resources/read' + ? message.params?.uri + : null; + return typeof subject === 'string' ? `${method} ${subject}` : method; +} + /** Serve newline-delimited JSON-RPC until `input` ends. */ -export async function serveStdio(server, { input, output }) { +export async function serveStdio(server, { input, output, log = () => {} }) { const lines = createInterface({ input, crlfDelay: Infinity }); const pending = new Set(); for await (const line of lines) { if (!line.trim()) continue; - const work = respond(server, line).then((response) => { + const work = respond(server, line, log).then((response) => { if (response) output.write(`${JSON.stringify(response)}\n`); }); pending.add(work); @@ -29,13 +45,15 @@ export async function serveStdio(server, { input, output }) { await Promise.all(pending); } -async function respond(server, line) { +async function respond(server, line, log) { let message; try { message = JSON.parse(line); } catch { return parseErrorResponse(); } + const described = describeRequest(message); + if (described) log(described); return server.handle(message); } @@ -59,6 +77,7 @@ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { await serveStdio(createLocalMcpServer(options), { input: process.stdin, output: process.stdout, + log: (line) => console.error(`<- ${line}`), }); } catch (error) { console.error(error.message); diff --git a/src/tools/localServer.test.mjs b/src/tools/localServer.test.mjs index 3cb61ebbb..61aa5c024 100644 --- a/src/tools/localServer.test.mjs +++ b/src/tools/localServer.test.mjs @@ -87,3 +87,20 @@ test('command-line arguments are strict', () => { }); assert.throws(() => parseArgs(['--port', '1']), /Unknown argument/); }); + +test('the stdio log names methods, tools and resources but never arguments', async () => { + const { describeRequest } = await import('../../server/mcp/stdio.js'); + assert.equal(describeRequest({ method: 'tools/list' }), 'tools/list'); + assert.equal( + describeRequest({ + method: 'tools/call', + params: { name: 'get_weather', arguments: { location: { place: 'x' } } }, + }), + 'tools/call get_weather', + ); + assert.equal( + describeRequest({ method: 'resources/read', params: { uri: 'ui://a/b' } }), + 'resources/read ui://a/b', + ); + assert.equal(describeRequest({ result: {} }), null); +});