mcp-memory-keeper 0.12.1 → 0.12.2
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.
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.12.2] - 2026-04-07
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`full` tool profile breaks OpenAI-compatible providers** (#33)
|
|
15
|
+
- `context_delegate.input.insights` array property was missing required `items` declaration
|
|
16
|
+
- Stricter providers rejected the schema with `invalid_function_parameters`
|
|
17
|
+
- Added `items: { type: 'object', properties: {...} }` to the `insights` property with proper `patterns`, `relationships`, `trends`, and `themes` fields
|
|
18
|
+
- Added `properties: {}` to `context_link.metadata` bare object schema for strict validator compatibility
|
|
19
|
+
- Added regression test that validates all array properties across all tool schemas have `items` declared
|
|
20
|
+
- Added comprehensive E2E test suite that spawns the actual MCP server and validates tool schemas, tool calls, and error handling over stdio
|
|
21
|
+
|
|
10
22
|
## [0.12.1] - 2026-03-24
|
|
11
23
|
|
|
12
24
|
### Fixed
|
|
@@ -507,7 +519,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
507
519
|
- **Security**: Security updates
|
|
508
520
|
- **Technical**: Internal improvements
|
|
509
521
|
|
|
510
|
-
[Unreleased]: https://github.com/mkreyman/mcp-memory-keeper/compare/v0.12.
|
|
522
|
+
[Unreleased]: https://github.com/mkreyman/mcp-memory-keeper/compare/v0.12.2...HEAD
|
|
523
|
+
[0.12.2]: https://github.com/mkreyman/mcp-memory-keeper/compare/v0.12.1...v0.12.2
|
|
511
524
|
[0.12.1]: https://github.com/mkreyman/mcp-memory-keeper/compare/v0.12.0...v0.12.1
|
|
512
525
|
[0.12.0]: https://github.com/mkreyman/mcp-memory-keeper/compare/v0.11.0...v0.12.0
|
|
513
526
|
[0.11.0]: https://github.com/mkreyman/mcp-memory-keeper/compare/v0.10.2...v0.11.0
|
|
@@ -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
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -3804,6 +3804,15 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
|
|
3804
3804
|
},
|
|
3805
3805
|
insights: {
|
|
3806
3806
|
type: 'array',
|
|
3807
|
+
items: {
|
|
3808
|
+
type: 'object',
|
|
3809
|
+
properties: {
|
|
3810
|
+
patterns: { type: 'object', properties: {} },
|
|
3811
|
+
relationships: { type: 'object', properties: {} },
|
|
3812
|
+
trends: { type: 'object', properties: {} },
|
|
3813
|
+
themes: { type: 'array', items: { type: 'string' } },
|
|
3814
|
+
},
|
|
3815
|
+
},
|
|
3807
3816
|
description: 'For merge synthesis: array of insights to merge',
|
|
3808
3817
|
},
|
|
3809
3818
|
},
|
|
@@ -4340,6 +4349,7 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
|
|
4340
4349
|
},
|
|
4341
4350
|
metadata: {
|
|
4342
4351
|
type: 'object',
|
|
4352
|
+
properties: {},
|
|
4343
4353
|
description: 'Optional metadata for the relationship',
|
|
4344
4354
|
},
|
|
4345
4355
|
},
|