fix(mcp): accept reserved _meta field in tools/call params (#2670)
* 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ab373716e7
commit
7b76082b13
|
|
@ -437,7 +437,10 @@ function createMemoryMcpService(options = {}) {
|
||||||
!isRecord(params)
|
!isRecord(params)
|
||||||
|| typeof name !== 'string'
|
|| typeof name !== 'string'
|
||||||
|| !TOOL_BY_NAME.has(name)
|
|| !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.');
|
return jsonRpcError(message.id, -32602, 'Unknown or missing memory tool.');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -136,6 +136,7 @@ async function withClient(fn, options = {}) {
|
||||||
'tools/call',
|
'tools/call',
|
||||||
{ name, arguments: toolArguments }
|
{ name, arguments: toolArguments }
|
||||||
),
|
),
|
||||||
|
callToolRaw: params => request('tools/call', params),
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
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 () => {
|
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 binRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-memory-bin-'));
|
||||||
const binPath = path.join(binRoot, 'ecc-memory-mcp');
|
const binPath = path.join(binRoot, 'ecc-memory-mcp');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue