From 7b76082b13278d8b449a39fef25aafed6d4fee28 Mon Sep 17 00:00:00 2001 From: Yowon Jeong Date: Tue, 4 Aug 2026 13:28:09 +0900 Subject: [PATCH] fix(mcp): accept reserved _meta field in tools/call params (#2670) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(mcp): accept reserved _meta field in tools/call params The memory MCP server rejected any tools/call whose params contained a key other than name/arguments, returning -32602 "Unknown or missing memory tool." MCP clients (e.g. Claude Code) attach the spec-reserved `_meta` field (such as progressToken) to request params, so every tool call from a compliant client failed and the entire memory MCP surface was unreachable — even though initialize/tools-list and the `ecc memory` CLI kept working. Per the MCP base protocol, `_meta` is reserved for request metadata and must be accepted. Add it to the params key allowlist. Co-Authored-By: Claude Opus 4.8 * test(mcp): validate _meta shape and cover tools/call param allowlist Address CodeRabbit review on #2670: - Validate params._meta when present: accept metadata objects, reject null, arrays, and scalar values (reuses isRecord). Keeps _meta optional and preserves existing name/arguments/unexpected-key rejection. - Add regression tests: accept _meta with progressToken, reject malformed _meta values, and continue rejecting unrelated top-level params. Co-Authored-By: Claude Opus 4.8 --------- Co-authored-by: Claude Opus 4.8 --- scripts/memory-mcp.mjs | 5 ++++- tests/scripts/memory-mcp.test.js | 35 ++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/scripts/memory-mcp.mjs b/scripts/memory-mcp.mjs index cb1ea8e16..5cde5e80e 100755 --- a/scripts/memory-mcp.mjs +++ b/scripts/memory-mcp.mjs @@ -437,7 +437,10 @@ function createMemoryMcpService(options = {}) { !isRecord(params) || typeof name !== 'string' || !TOOL_BY_NAME.has(name) - || Object.keys(params).some(key => !['name', 'arguments'].includes(key)) + // `_meta` is reserved by MCP for request metadata (e.g. progressToken); accept it, + // but when present it must be a metadata object — reject null, arrays, and scalars. + || (Object.prototype.hasOwnProperty.call(params, '_meta') && !isRecord(params._meta)) + || Object.keys(params).some(key => !['name', 'arguments', '_meta'].includes(key)) ) { return jsonRpcError(message.id, -32602, 'Unknown or missing memory tool.'); } diff --git a/tests/scripts/memory-mcp.test.js b/tests/scripts/memory-mcp.test.js index 2f540bf84..0a3a7a7a0 100644 --- a/tests/scripts/memory-mcp.test.js +++ b/tests/scripts/memory-mcp.test.js @@ -136,6 +136,7 @@ async function withClient(fn, options = {}) { 'tools/call', { name, arguments: toolArguments } ), + callToolRaw: params => request('tools/call', params), }; try { @@ -180,6 +181,40 @@ async function main() { }); }); + 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. + const withMeta = await client.callToolRaw({ + name: 'memory_doctor', + arguments: {}, + _meta: { progressToken: 'progress-123' }, + }); + assert.ok(Array.isArray(withMeta.content)); + + // Baseline: no `_meta` still works. + const withoutMeta = await client.callToolRaw({ + name: 'memory_doctor', + arguments: {}, + }); + assert.ok(Array.isArray(withoutMeta.content)); + + // A malformed `_meta` (null, array, or scalar) must be rejected. + for (const badMeta of [null, ['not', 'an', 'object'], 'string', 42, true]) { + await assert.rejects( + client.callToolRaw({ name: 'memory_doctor', arguments: {}, _meta: badMeta }), + /-32602/, + `expected _meta=${JSON.stringify(badMeta)} to be rejected` + ); + } + + // Unrelated top-level params must still be rejected. + await assert.rejects( + client.callToolRaw({ name: 'memory_doctor', arguments: {}, unexpected: true }), + /-32602/ + ); + }); + }); + await test('starts when the npm bin invokes the server through a symlink', async () => { const binRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-memory-bin-')); const binPath = path.join(binRoot, 'ecc-memory-mcp');