mcp-memory-keeper 0.12.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,341 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const globals_1 = require("@jest/globals");
37
+ const child_process_1 = require("child_process");
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const os = __importStar(require("os"));
41
+ /**
42
+ * End-to-end tests for the MCP Memory Keeper server.
43
+ *
44
+ * These tests spawn the actual server process over stdio, send real MCP
45
+ * protocol messages, and validate the responses. Unlike integration tests
46
+ * that instantiate internal classes directly, these exercise the full stack:
47
+ * transport → protocol → handler → database → response.
48
+ *
49
+ * @see https://github.com/mkreyman/mcp-memory-keeper/issues/33
50
+ */
51
+ // Valid JSON Schema types per the spec
52
+ const VALID_JSON_SCHEMA_TYPES = new Set([
53
+ 'string',
54
+ 'number',
55
+ 'integer',
56
+ 'boolean',
57
+ 'array',
58
+ 'object',
59
+ 'null',
60
+ ]);
61
+ let serverProcess = null;
62
+ let tempDir;
63
+ let msgId = 0;
64
+ let outputBuffer = '';
65
+ /** Send a JSON-RPC message to the server and wait for a response with the matching id. */
66
+ function sendRequest(method, params = {}, timeoutMs = 5000) {
67
+ return new Promise((resolve, reject) => {
68
+ const id = ++msgId;
69
+ const timeout = setTimeout(() => {
70
+ reject(new Error(`Timeout waiting for response to ${method} (id=${id})`));
71
+ }, timeoutMs);
72
+ const onData = (data) => {
73
+ outputBuffer += data.toString();
74
+ const lines = outputBuffer.split('\n');
75
+ // Keep the last incomplete line in the buffer
76
+ outputBuffer = lines.pop() || '';
77
+ for (const line of lines) {
78
+ if (!line.trim())
79
+ continue;
80
+ try {
81
+ const msg = JSON.parse(line);
82
+ if (msg.id === id) {
83
+ clearTimeout(timeout);
84
+ serverProcess?.stdout?.removeListener('data', onData);
85
+ resolve(msg);
86
+ }
87
+ }
88
+ catch {
89
+ // Not JSON, skip
90
+ }
91
+ }
92
+ };
93
+ serverProcess?.stdout?.on('data', onData);
94
+ serverProcess?.stdin?.write(JSON.stringify({ jsonrpc: '2.0', method, params, id }) + '\n');
95
+ });
96
+ }
97
+ /**
98
+ * Recursively validate a JSON Schema property definition.
99
+ * Returns an array of human-readable violation strings.
100
+ */
101
+ function validateSchemaProperty(prop, path, toolName) {
102
+ const violations = [];
103
+ // Every property should have a type
104
+ if (!prop.type) {
105
+ violations.push(`${toolName}: ${path} — missing 'type'`);
106
+ return violations; // Can't validate further without type
107
+ }
108
+ // Type must be a valid JSON Schema type
109
+ if (!VALID_JSON_SCHEMA_TYPES.has(prop.type)) {
110
+ violations.push(`${toolName}: ${path} — invalid type '${prop.type}'`);
111
+ }
112
+ // Arrays must have items
113
+ if (prop.type === 'array' && !prop.items) {
114
+ violations.push(`${toolName}: ${path} — type 'array' missing 'items'`);
115
+ }
116
+ // If enum is present, values should match the declared type
117
+ if (prop.enum && prop.type) {
118
+ for (const val of prop.enum) {
119
+ if (prop.type === 'string' && typeof val !== 'string') {
120
+ violations.push(`${toolName}: ${path} — enum value '${val}' is not a string`);
121
+ }
122
+ if (prop.type === 'number' && typeof val !== 'number') {
123
+ violations.push(`${toolName}: ${path} — enum value '${val}' is not a number`);
124
+ }
125
+ }
126
+ }
127
+ // Required must reference existing properties
128
+ if (prop.type === 'object' && prop.required && prop.properties) {
129
+ for (const req of prop.required) {
130
+ if (!(req in prop.properties)) {
131
+ violations.push(`${toolName}: ${path} — required field '${req}' not found in properties`);
132
+ }
133
+ }
134
+ }
135
+ // Recurse into object properties
136
+ if (prop.type === 'object' && prop.properties) {
137
+ for (const [key, value] of Object.entries(prop.properties)) {
138
+ violations.push(...validateSchemaProperty(value, `${path}.${key}`, toolName));
139
+ }
140
+ }
141
+ // Recurse into array items
142
+ if (prop.type === 'array' && prop.items && typeof prop.items === 'object') {
143
+ violations.push(...validateSchemaProperty(prop.items, `${path}.items`, toolName));
144
+ }
145
+ return violations;
146
+ }
147
+ (0, globals_1.describe)('MCP Server E2E Tests', () => {
148
+ (0, globals_1.beforeAll)(async () => {
149
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mcp-e2e-'));
150
+ serverProcess = (0, child_process_1.spawn)('node', [path.join(__dirname, '../../../dist/index.js')], {
151
+ env: { ...process.env, DATA_DIR: tempDir },
152
+ stdio: ['pipe', 'pipe', 'pipe'],
153
+ });
154
+ if (global.testProcesses) {
155
+ global.testProcesses.push(serverProcess);
156
+ }
157
+ // Initialize the MCP session
158
+ const initResponse = await sendRequest('initialize', {
159
+ protocolVersion: '2024-11-05',
160
+ capabilities: {},
161
+ clientInfo: { name: 'e2e-test', version: '1.0.0' },
162
+ });
163
+ (0, globals_1.expect)(initResponse.result).toHaveProperty('protocolVersion');
164
+ // Send initialized notification (no response expected)
165
+ serverProcess?.stdin?.write(JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n');
166
+ // Brief pause for server to process the notification
167
+ await new Promise(resolve => setTimeout(resolve, 200));
168
+ }, 10000);
169
+ (0, globals_1.afterAll)(async () => {
170
+ if (serverProcess && !serverProcess.killed) {
171
+ serverProcess.kill('SIGTERM');
172
+ await new Promise(resolve => {
173
+ const timeout = setTimeout(() => {
174
+ serverProcess?.kill('SIGKILL');
175
+ resolve();
176
+ }, 3000);
177
+ serverProcess?.on('exit', () => {
178
+ clearTimeout(timeout);
179
+ resolve();
180
+ });
181
+ });
182
+ serverProcess?.removeAllListeners();
183
+ }
184
+ serverProcess = null;
185
+ try {
186
+ fs.rmSync(tempDir, { recursive: true, force: true });
187
+ }
188
+ catch {
189
+ // ignore cleanup errors
190
+ }
191
+ });
192
+ // ─── ListTools Schema Validation ────────────────────────────────────
193
+ (0, globals_1.describe)('tools/list — schema validation', () => {
194
+ let tools;
195
+ (0, globals_1.beforeAll)(async () => {
196
+ const response = await sendRequest('tools/list');
197
+ (0, globals_1.expect)(response.result).toHaveProperty('tools');
198
+ tools = response.result.tools;
199
+ });
200
+ (0, globals_1.it)('should return a non-empty list of tools', () => {
201
+ (0, globals_1.expect)(tools.length).toBeGreaterThan(0);
202
+ });
203
+ (0, globals_1.it)('every tool should have name, description, and inputSchema', () => {
204
+ for (const tool of tools) {
205
+ (0, globals_1.expect)(tool).toHaveProperty('name');
206
+ (0, globals_1.expect)(tool).toHaveProperty('description');
207
+ (0, globals_1.expect)(tool).toHaveProperty('inputSchema');
208
+ (0, globals_1.expect)(typeof tool.name).toBe('string');
209
+ (0, globals_1.expect)(typeof tool.description).toBe('string');
210
+ (0, globals_1.expect)(tool.name.length).toBeGreaterThan(0);
211
+ (0, globals_1.expect)(tool.description.length).toBeGreaterThan(0);
212
+ }
213
+ });
214
+ (0, globals_1.it)('every tool name should follow the context_ convention', () => {
215
+ for (const tool of tools) {
216
+ (0, globals_1.expect)(tool.name).toMatch(/^context_[a-z_]+$/);
217
+ }
218
+ });
219
+ (0, globals_1.it)('every inputSchema should be type object with properties', () => {
220
+ for (const tool of tools) {
221
+ (0, globals_1.expect)(tool.inputSchema.type).toBe('object');
222
+ (0, globals_1.expect)(tool.inputSchema).toHaveProperty('properties');
223
+ (0, globals_1.expect)(typeof tool.inputSchema.properties).toBe('object');
224
+ }
225
+ });
226
+ (0, globals_1.it)('no tool names should be duplicated', () => {
227
+ const names = tools.map((t) => t.name);
228
+ (0, globals_1.expect)(new Set(names).size).toBe(names.length);
229
+ });
230
+ (0, globals_1.it)('every array property should have items declared (issue #33)', () => {
231
+ const violations = [];
232
+ for (const tool of tools) {
233
+ violations.push(...validateSchemaProperty(tool.inputSchema, 'inputSchema', tool.name).filter(v => v.includes("missing 'items'")));
234
+ }
235
+ (0, globals_1.expect)(violations).toHaveLength(0);
236
+ });
237
+ (0, globals_1.it)('every property type should be a valid JSON Schema type', () => {
238
+ const violations = [];
239
+ for (const tool of tools) {
240
+ violations.push(...validateSchemaProperty(tool.inputSchema, 'inputSchema', tool.name).filter(v => v.includes('invalid type')));
241
+ }
242
+ (0, globals_1.expect)(violations).toHaveLength(0);
243
+ });
244
+ (0, globals_1.it)('every required field should reference an existing property', () => {
245
+ const violations = [];
246
+ for (const tool of tools) {
247
+ violations.push(...validateSchemaProperty(tool.inputSchema, 'inputSchema', tool.name).filter(v => v.includes('not found in properties')));
248
+ }
249
+ (0, globals_1.expect)(violations).toHaveLength(0);
250
+ });
251
+ (0, globals_1.it)('enum values should match their declared type', () => {
252
+ const violations = [];
253
+ for (const tool of tools) {
254
+ violations.push(...validateSchemaProperty(tool.inputSchema, 'inputSchema', tool.name).filter(v => v.includes('enum value')));
255
+ }
256
+ (0, globals_1.expect)(violations).toHaveLength(0);
257
+ });
258
+ (0, globals_1.it)('full schema validation should find zero violations across all tools', () => {
259
+ const allViolations = [];
260
+ for (const tool of tools) {
261
+ allViolations.push(...validateSchemaProperty(tool.inputSchema, 'inputSchema', tool.name));
262
+ }
263
+ if (allViolations.length > 0) {
264
+ throw new Error(`Found ${allViolations.length} schema violation(s):\n${allViolations.map(v => ` ${v}`).join('\n')}`);
265
+ }
266
+ });
267
+ });
268
+ // ─── Tool Call Smoke Tests ──────────────────────────────────────────
269
+ (0, globals_1.describe)('tools/call — smoke tests', () => {
270
+ (0, globals_1.it)('should save and retrieve a context item', async () => {
271
+ // Save
272
+ const saveResponse = await sendRequest('tools/call', {
273
+ name: 'context_save',
274
+ arguments: {
275
+ key: 'e2e_test_key',
276
+ value: 'e2e_test_value',
277
+ category: 'note',
278
+ },
279
+ });
280
+ (0, globals_1.expect)(saveResponse.result).toHaveProperty('content');
281
+ const saveText = saveResponse.result.content[0].text;
282
+ (0, globals_1.expect)(saveText).toContain('e2e_test_key');
283
+ // Retrieve
284
+ const getResponse = await sendRequest('tools/call', {
285
+ name: 'context_get',
286
+ arguments: { key: 'e2e_test_key' },
287
+ });
288
+ (0, globals_1.expect)(getResponse.result).toHaveProperty('content');
289
+ const getResult = JSON.parse(getResponse.result.content[0].text);
290
+ (0, globals_1.expect)(getResult.items).toBeDefined();
291
+ (0, globals_1.expect)(getResult.items.length).toBeGreaterThan(0);
292
+ (0, globals_1.expect)(getResult.items[0].value).toBe('e2e_test_value');
293
+ });
294
+ (0, globals_1.it)('should search for saved items', async () => {
295
+ const response = await sendRequest('tools/call', {
296
+ name: 'context_search',
297
+ arguments: { query: 'e2e_test' },
298
+ });
299
+ (0, globals_1.expect)(response.result).toHaveProperty('content');
300
+ const text = response.result.content[0].text;
301
+ (0, globals_1.expect)(text).toContain('e2e_test');
302
+ });
303
+ (0, globals_1.it)('should return status', async () => {
304
+ const response = await sendRequest('tools/call', {
305
+ name: 'context_status',
306
+ arguments: {},
307
+ });
308
+ (0, globals_1.expect)(response.result).toHaveProperty('content');
309
+ const text = response.result.content[0].text;
310
+ // Status response contains session info regardless of format
311
+ (0, globals_1.expect)(text.length).toBeGreaterThan(0);
312
+ (0, globals_1.expect)(text).toMatch(/session|item/i);
313
+ });
314
+ (0, globals_1.it)('should reject unknown tools with an error', async () => {
315
+ const response = await sendRequest('tools/call', {
316
+ name: 'nonexistent_tool',
317
+ arguments: {},
318
+ });
319
+ // MCP SDK wraps handler errors
320
+ (0, globals_1.expect)(response.error || response.result?.isError).toBeTruthy();
321
+ });
322
+ (0, globals_1.it)('should handle missing required arguments gracefully', async () => {
323
+ const response = await sendRequest('tools/call', {
324
+ name: 'context_save',
325
+ arguments: {},
326
+ });
327
+ (0, globals_1.expect)(response.result).toHaveProperty('content');
328
+ const text = response.result.content[0].text;
329
+ // Should return an error message, not crash
330
+ (0, globals_1.expect)(text.toLowerCase()).toMatch(/error|required|key/i);
331
+ });
332
+ });
333
+ // ─── Tool Profile Filtering ────────────────────────────────────────
334
+ (0, globals_1.describe)('tool profile filtering', () => {
335
+ (0, globals_1.it)('default profile should expose all tools', async () => {
336
+ const response = await sendRequest('tools/list');
337
+ // Default is "full" profile with 38 tools
338
+ (0, globals_1.expect)(response.result.tools.length).toBe(38);
339
+ });
340
+ });
341
+ });
@@ -0,0 +1,165 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const globals_1 = require("@jest/globals");
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ /**
40
+ * Issue #33: full tool profile exposes a schema that breaks some OpenAI-compatible providers
41
+ *
42
+ * Some providers reject tool schemas where an array property lacks an `items` declaration.
43
+ * Per JSON Schema spec, `items` is required for `type: 'array'` to fully describe the schema.
44
+ *
45
+ * This test scans src/index.ts and verifies that every property with `type: 'array'`
46
+ * has an `items` declaration within the same schema block.
47
+ *
48
+ * @see https://github.com/mkreyman/mcp-memory-keeper/issues/33
49
+ */
50
+ /**
51
+ * Scan the source for tool definitions and find array properties missing `items`.
52
+ * Skips block comments to avoid false positives from commented-out schemas.
53
+ * Line numbers refer to the original source file for accurate debugging.
54
+ */
55
+ function findArrayPropertiesMissingItems(src) {
56
+ const violations = [];
57
+ const lines = src.split('\n');
58
+ let currentTool = '';
59
+ let inBlockComment = false;
60
+ for (let i = 0; i < lines.length; i++) {
61
+ const line = lines[i];
62
+ // Track block comment state
63
+ if (inBlockComment) {
64
+ if (line.includes('*/')) {
65
+ inBlockComment = false;
66
+ }
67
+ continue;
68
+ }
69
+ if (line.includes('/*')) {
70
+ inBlockComment = true;
71
+ if (line.includes('*/')) {
72
+ inBlockComment = false;
73
+ }
74
+ continue;
75
+ }
76
+ // Track which tool we're inside
77
+ const toolMatch = line.match(/name:\s*'(context_[a-z_]+)'/);
78
+ if (toolMatch) {
79
+ currentTool = toolMatch[1];
80
+ }
81
+ // Find array type declarations
82
+ if (!line.match(/type:\s*'array'/))
83
+ continue;
84
+ if (!currentTool)
85
+ continue;
86
+ // Find the property name (check current line first for single-line declarations)
87
+ let propertyName = '(unknown)';
88
+ const currentLinePropMatch = line.match(/(\w+)\s*:\s*\{/);
89
+ if (currentLinePropMatch) {
90
+ propertyName = currentLinePropMatch[1];
91
+ }
92
+ else {
93
+ for (let j = i - 1; j >= Math.max(0, i - 5); j--) {
94
+ const propMatch = lines[j].match(/(\w+)\s*:\s*\{/);
95
+ if (propMatch) {
96
+ propertyName = propMatch[1];
97
+ break;
98
+ }
99
+ }
100
+ }
101
+ // Check current line first (handles single-line array declarations)
102
+ let foundItems = line.includes('items');
103
+ let depth = 0;
104
+ for (let j = i + 1; !foundItems && j < Math.min(lines.length, i + 50); j++) {
105
+ const fwdLine = lines[j];
106
+ for (const ch of fwdLine) {
107
+ if (ch === '{')
108
+ depth++;
109
+ if (ch === '}')
110
+ depth--;
111
+ }
112
+ if (fwdLine.match(/^\s*items\s*:/)) {
113
+ foundItems = true;
114
+ break;
115
+ }
116
+ if (depth < 0)
117
+ break;
118
+ }
119
+ if (!foundItems) {
120
+ violations.push({
121
+ tool: currentTool,
122
+ property: propertyName,
123
+ line: i + 1,
124
+ });
125
+ }
126
+ }
127
+ return violations;
128
+ }
129
+ (0, globals_1.describe)('Issue #33: Array properties must declare items', () => {
130
+ const indexPath = path.join(__dirname, '..', '..', 'index.ts');
131
+ const src = fs.readFileSync(indexPath, 'utf-8');
132
+ (0, globals_1.it)('should find tool definitions in source', () => {
133
+ const toolNames = src.match(/name:\s*'context_[a-z_]+'/g);
134
+ (0, globals_1.expect)(toolNames).not.toBeNull();
135
+ (0, globals_1.expect)(toolNames.length).toBeGreaterThan(0);
136
+ });
137
+ (0, globals_1.it)('every array property in every tool schema must have an items declaration', () => {
138
+ const violations = findArrayPropertiesMissingItems(src);
139
+ (0, globals_1.expect)(violations).toHaveLength(0);
140
+ });
141
+ (0, globals_1.it)('context_delegate.input.insights specifically must have items', () => {
142
+ const lines = src.split('\n');
143
+ let inDelegate = false;
144
+ let insightsLine = -1;
145
+ for (let i = 0; i < lines.length; i++) {
146
+ if (lines[i].match(/name:\s*'context_delegate'/)) {
147
+ inDelegate = true;
148
+ }
149
+ if (inDelegate &&
150
+ i > 0 &&
151
+ lines[i].match(/name:\s*'context_/) &&
152
+ !lines[i].match(/context_delegate/)) {
153
+ break;
154
+ }
155
+ if (inDelegate && lines[i].match(/insights\s*:\s*\{/)) {
156
+ insightsLine = i;
157
+ break;
158
+ }
159
+ }
160
+ (0, globals_1.expect)(insightsLine).toBeGreaterThan(-1);
161
+ const insightsBlock = lines.slice(insightsLine, insightsLine + 5).join('\n');
162
+ (0, globals_1.expect)(insightsBlock).toContain("type: 'array'");
163
+ (0, globals_1.expect)(insightsBlock).toMatch(/items\s*:/);
164
+ });
165
+ });