From 380f4b35db60f92183e731527290bf10aec424c0 Mon Sep 17 00:00:00 2001 From: Nguyen Thanh Dat Date: Mon, 7 Sep 2026 15:35:42 +0700 Subject: [PATCH] fix(memory-mcp): accept the reserved _meta param on ping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tools/list and tools/call on main already admit `_meta` — MCP reserves it for request metadata and a client may attach it to any request. ping still refused every parameter, so a client that sends `_meta` on everything (Codex does) got -32602 on its keepalive. Rebased onto main and narrowed: when this branch was first written the same gap existed on tools/list, which has since been fixed upstream. Only the ping handler is left, so only the ping handler is touched. Refs #2810 --- scripts/memory-mcp.mjs | 14 ++++++++++++-- tests/scripts/memory-mcp.test.js | 20 ++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/scripts/memory-mcp.mjs b/scripts/memory-mcp.mjs index 741f864fb..fad1677dc 100755 --- a/scripts/memory-mcp.mjs +++ b/scripts/memory-mcp.mjs @@ -426,8 +426,18 @@ function createMemoryMcpService(options = {}) { return jsonRpcError(message.id, -32002, 'Server is not initialized.'); } if (message.method === 'ping') { - if (message.params && Object.keys(message.params).length > 0) { - return jsonRpcError(message.id, -32602, 'ping does not accept parameters.'); + const params = message.params ?? {}; + // `_meta` is reserved by MCP for request metadata (e.g. progressToken) and + // may ride on any request, which is why `tools/list` and `tools/call` below + // both admit it. `ping` rejected every parameter, so a client that attaches + // `_meta` to everything — Codex does — got -32602 on its keepalive. Present + // means it must be a metadata object; nothing else is accepted. (#2810) + if ( + !isRecord(params) + || (Object.prototype.hasOwnProperty.call(params, '_meta') && !isRecord(params._meta)) + || Object.keys(params).some(key => key !== '_meta') + ) { + return jsonRpcError(message.id, -32602, 'ping accepts no parameters other than _meta.'); } return jsonRpcResult(message.id, {}); } diff --git a/tests/scripts/memory-mcp.test.js b/tests/scripts/memory-mcp.test.js index 9adf37bf5..5698f93e1 100644 --- a/tests/scripts/memory-mcp.test.js +++ b/tests/scripts/memory-mcp.test.js @@ -260,6 +260,7 @@ async function withClient(fn, options = {}) { { name, arguments: toolArguments } ), callToolRaw: params => request('tools/call', params), + ping: params => request('ping', params), }; phase = 'callback'; await Promise.race([Promise.resolve().then(() => fn(client, fixture)), transportFailure]); @@ -393,6 +394,25 @@ async function main() { }); }); + await test('accepts the reserved _meta param on ping and rejects malformed values (#2810)', async () => { + await withClient(async client => { + assert.deepStrictEqual(await client.ping({ _meta: { progressToken: 'progress-1' } }), {}); + assert.deepStrictEqual(await client.ping(), {}); + assert.deepStrictEqual(await client.ping({}), {}); + + for (const badMeta of [null, ['not', 'an', 'object'], 'string', 42, true]) { + await assert.rejects( + client.ping({ _meta: badMeta }), + /-32602/, + `expected ping _meta=${JSON.stringify(badMeta)} to be rejected` + ); + } + + await assert.rejects(client.ping({ unexpected: true }), /-32602/); + await assert.rejects(client.ping({ _meta: {}, unexpected: true }), /-32602/); + }); + }); + await test('accepts the reserved _meta param on tools/call and rejects malformed values', async () => { await withClient(async client => { // A valid `_meta` object (e.g. progressToken) must not block the tool call.