diff --git a/scripts/memory-mcp.mjs b/scripts/memory-mcp.mjs old mode 100755 new mode 100644 index 932d085d4..ea2fd57c6 --- a/scripts/memory-mcp.mjs +++ b/scripts/memory-mcp.mjs @@ -383,10 +383,12 @@ function createMemoryMcpService(options = {}) { const isNotification = !hasId; if (isNotification) { + const params = message.params ?? {}; if ( message.method === 'notifications/initialized' && initializationRequested - && Object.keys(message.params || {}).length === 0 + && (!Object.prototype.hasOwnProperty.call(params, '_meta') || isRecord(params._meta)) + && Object.keys(params).every(key => key === '_meta') ) { initialized = true; } diff --git a/tests/scripts/memory-mcp.test.js b/tests/scripts/memory-mcp.test.js index 9ba90dd55..bbb43f9bf 100644 --- a/tests/scripts/memory-mcp.test.js +++ b/tests/scripts/memory-mcp.test.js @@ -806,6 +806,76 @@ async function main() { assert.strictEqual(invalidParams.error.code, -32600); }); + for (const [label, params] of [ + ['omitted params', undefined], + ['empty params', {}], + ['empty metadata', { _meta: {} }], + ['extension metadata', { _meta: { 'example.com/trace': 'sample' } }], + ]) { + await test(`accepts initialized notifications with ${label}`, async () => { + const { createMemoryMcpService } = await import(pathToFileURL(SERVER).href); + const service = createMemoryMcpService({ harness: 'claude' }); + const initialized = await service.handle({ + jsonrpc: '2.0', id: 1, method: 'initialize', + params: { + protocolVersion: '2025-11-25', capabilities: {}, + clientInfo: { name: 'metadata-test', version: '1.0.0' }, + }, + }); + assert.strictEqual(initialized.error, undefined); + const notification = await service.handle({ + jsonrpc: '2.0', method: 'notifications/initialized', + ...(params === undefined ? {} : { params }), + }); + assert.strictEqual(notification, null); + const listed = await service.handle({ jsonrpc: '2.0', id: 2, method: 'tools/list' }); + assert.strictEqual(listed.error, undefined); + assert.ok(listed.result.tools.some(tool => tool.name === 'memory_search')); + }); + } + + await test('ignores initialized notifications with malformed metadata or unknown params', async () => { + const { createMemoryMcpService } = await import(pathToFileURL(SERVER).href); + for (const params of [ + ...[null, [], 'invalid', 1, false].map(_meta => ({ _meta })), + { _meta: {}, unexpected: true }, + ]) { + const service = createMemoryMcpService({ harness: 'claude' }); + await service.handle({ + jsonrpc: '2.0', id: 1, method: 'initialize', + params: { + protocolVersion: '2025-11-25', capabilities: {}, + clientInfo: { name: 'metadata-test', version: '1.0.0' }, + }, + }); + assert.strictEqual(await service.handle({ + jsonrpc: '2.0', method: 'notifications/initialized', params, + }), null); + const listed = await service.handle({ jsonrpc: '2.0', id: 2, method: 'tools/list' }); + assert.strictEqual(listed.error?.code, -32002, JSON.stringify(params)); + } + }); + + await test('does not initialize from a metadata notification sent before initialize', async () => { + const { createMemoryMcpService } = await import(pathToFileURL(SERVER).href); + const service = createMemoryMcpService({ harness: 'claude' }); + const notification = { + jsonrpc: '2.0', method: 'notifications/initialized', params: { _meta: {} }, + }; + assert.strictEqual(await service.handle(notification), null); + const before = await service.handle({ jsonrpc: '2.0', id: 1, method: 'tools/list' }); + assert.strictEqual(before.error?.code, -32002); + await service.handle({ + jsonrpc: '2.0', id: 2, method: 'initialize', + params: { + protocolVersion: '2025-11-25', capabilities: {}, + clientInfo: { name: 'metadata-test', version: '1.0.0' }, + }, + }); + const after = await service.handle({ jsonrpc: '2.0', id: 3, method: 'tools/list' }); + assert.strictEqual(after.error?.code, -32002); + }); + await test('bounds queued transport work under a single-chunk request flood', async () => { const { MAX_PENDING_MESSAGES,