@smallironman/mcp-memory-keeper 0.12.2-fork1
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 +542 -0
- package/LICENSE +21 -0
- package/README.md +1281 -0
- package/bin/mcp-memory-keeper +54 -0
- package/dist/__tests__/e2e/issue33-reproduce.test.js +234 -0
- package/dist/__tests__/e2e/server-e2e.test.js +341 -0
- package/dist/__tests__/helpers/database-test-helper.js +160 -0
- package/dist/__tests__/helpers/test-server.js +92 -0
- package/dist/__tests__/integration/advanced-features.test.js +614 -0
- package/dist/__tests__/integration/backward-compatibility.test.js +245 -0
- package/dist/__tests__/integration/batchOperationsE2E.test.js +396 -0
- package/dist/__tests__/integration/batchOperationsHandler.test.js +1230 -0
- package/dist/__tests__/integration/channelManagementHandler.test.js +1291 -0
- package/dist/__tests__/integration/channels.test.js +376 -0
- package/dist/__tests__/integration/checkpoint.test.js +251 -0
- package/dist/__tests__/integration/concurrent-access.test.js +190 -0
- package/dist/__tests__/integration/context-operations.test.js +243 -0
- package/dist/__tests__/integration/contextDiff.test.js +852 -0
- package/dist/__tests__/integration/contextDiffHandler.test.js +976 -0
- package/dist/__tests__/integration/contextExportHandler.test.js +510 -0
- package/dist/__tests__/integration/contextGetPaginationDefaults.test.js +298 -0
- package/dist/__tests__/integration/contextReassignChannelHandler.test.js +908 -0
- package/dist/__tests__/integration/contextRelationshipsHandler.test.js +1151 -0
- package/dist/__tests__/integration/contextSearch.test.js +1054 -0
- package/dist/__tests__/integration/contextSearchHandler.test.js +552 -0
- package/dist/__tests__/integration/contextWatchActual.test.js +165 -0
- package/dist/__tests__/integration/contextWatchHandler.test.js +1500 -0
- package/dist/__tests__/integration/database-initialization.test.js +134 -0
- package/dist/__tests__/integration/enhanced-context-operations.test.js +1082 -0
- package/dist/__tests__/integration/enhancedContextGetHandler.test.js +915 -0
- package/dist/__tests__/integration/enhancedContextTimelineHandler.test.js +716 -0
- package/dist/__tests__/integration/error-cases.test.js +411 -0
- package/dist/__tests__/integration/export-import.test.js +367 -0
- package/dist/__tests__/integration/feature-flags.test.js +542 -0
- package/dist/__tests__/integration/file-operations.test.js +264 -0
- package/dist/__tests__/integration/filterBySessionId.test.js +251 -0
- package/dist/__tests__/integration/git-integration.test.js +241 -0
- package/dist/__tests__/integration/index-tools.test.js +496 -0
- package/dist/__tests__/integration/issue11-actual-bug-demo.test.js +304 -0
- package/dist/__tests__/integration/issue11-search-filters-bug.test.js +561 -0
- package/dist/__tests__/integration/issue12-checkpoint-restore-behavior.test.js +621 -0
- package/dist/__tests__/integration/issue13-key-validation.test.js +433 -0
- package/dist/__tests__/integration/issue24-final-fix.test.js +241 -0
- package/dist/__tests__/integration/issue24-fix-validation.test.js +158 -0
- package/dist/__tests__/integration/issue24-reproduce.test.js +225 -0
- package/dist/__tests__/integration/issue24-token-limit.test.js +199 -0
- package/dist/__tests__/integration/issue33-array-items-schema.test.js +165 -0
- package/dist/__tests__/integration/knowledge-graph.test.js +338 -0
- package/dist/__tests__/integration/migrations.test.js +528 -0
- package/dist/__tests__/integration/multi-agent.test.js +546 -0
- package/dist/__tests__/integration/pagination-critical-fix.test.js +296 -0
- package/dist/__tests__/integration/paginationDefaultsHandler.test.js +600 -0
- package/dist/__tests__/integration/project-directory.test.js +291 -0
- package/dist/__tests__/integration/resource-cleanup.test.js +149 -0
- package/dist/__tests__/integration/retention.test.js +513 -0
- package/dist/__tests__/integration/search.test.js +333 -0
- package/dist/__tests__/integration/semantic-search.test.js +266 -0
- package/dist/__tests__/integration/server-initialization.test.js +305 -0
- package/dist/__tests__/integration/session-management.test.js +219 -0
- package/dist/__tests__/integration/simplified-sharing.test.js +346 -0
- package/dist/__tests__/integration/smart-compaction.test.js +230 -0
- package/dist/__tests__/integration/summarization.test.js +308 -0
- package/dist/__tests__/integration/tokenLimitEnforcement.test.js +134 -0
- package/dist/__tests__/integration/tool-profiles-integration.test.js +150 -0
- package/dist/__tests__/integration/watcher-migration-validation.test.js +544 -0
- package/dist/__tests__/security/input-validation.test.js +115 -0
- package/dist/__tests__/utils/agents.test.js +473 -0
- package/dist/__tests__/utils/database.test.js +177 -0
- package/dist/__tests__/utils/git.test.js +122 -0
- package/dist/__tests__/utils/knowledge-graph.test.js +297 -0
- package/dist/__tests__/utils/migrationHealthCheck.test.js +302 -0
- package/dist/__tests__/utils/project-directory-messages.test.js +192 -0
- package/dist/__tests__/utils/timezone-safe-dates.js +119 -0
- package/dist/__tests__/utils/token-limits.test.js +225 -0
- package/dist/__tests__/utils/tool-profiles.test.js +374 -0
- package/dist/__tests__/utils/validation.test.js +200 -0
- package/dist/__tests__/utils/vector-store.test.js +231 -0
- package/dist/handlers/contextWatchHandlers.js +206 -0
- package/dist/index.js +4425 -0
- package/dist/migrations/003_add_channels.js +174 -0
- package/dist/migrations/004_add_context_watch.js +151 -0
- package/dist/migrations/005_add_context_watch.js +98 -0
- package/dist/migrations/simplify-sharing.js +117 -0
- package/dist/repositories/BaseRepository.js +30 -0
- package/dist/repositories/CheckpointRepository.js +140 -0
- package/dist/repositories/ContextRepository.js +2017 -0
- package/dist/repositories/FileRepository.js +104 -0
- package/dist/repositories/RepositoryManager.js +62 -0
- package/dist/repositories/SessionRepository.js +66 -0
- package/dist/repositories/WatcherRepository.js +252 -0
- package/dist/repositories/index.js +15 -0
- package/dist/test-helpers/database-helper.js +128 -0
- package/dist/types/entities.js +3 -0
- package/dist/utils/agents.js +791 -0
- package/dist/utils/channels.js +150 -0
- package/dist/utils/database.js +780 -0
- package/dist/utils/feature-flags.js +476 -0
- package/dist/utils/git.js +145 -0
- package/dist/utils/knowledge-graph.js +264 -0
- package/dist/utils/migrationHealthCheck.js +373 -0
- package/dist/utils/migrations.js +452 -0
- package/dist/utils/retention.js +460 -0
- package/dist/utils/timestamps.js +112 -0
- package/dist/utils/token-limits.js +350 -0
- package/dist/utils/tool-profiles.js +242 -0
- package/dist/utils/validation.js +296 -0
- package/dist/utils/vector-store.js +247 -0
- package/examples/config.json +31 -0
- package/examples/project-directory-setup.md +114 -0
- package/package.json +85 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MCP Memory Keeper CLI
|
|
5
|
+
* This wrapper ensures the server runs correctly when invoked via npx
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { spawn } = require('child_process');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
|
|
13
|
+
// Determine the data directory
|
|
14
|
+
const DATA_DIR = process.env.DATA_DIR || path.join(os.homedir(), 'mcp-data', 'memory-keeper');
|
|
15
|
+
|
|
16
|
+
// Ensure data directory exists
|
|
17
|
+
if (!fs.existsSync(DATA_DIR)) {
|
|
18
|
+
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Set environment variable for the server
|
|
22
|
+
process.env.DATA_DIR = DATA_DIR;
|
|
23
|
+
|
|
24
|
+
// Get the path to the actual server
|
|
25
|
+
const serverPath = path.join(__dirname, '..', 'dist', 'index.js');
|
|
26
|
+
|
|
27
|
+
// Check if the server is built
|
|
28
|
+
if (!fs.existsSync(serverPath)) {
|
|
29
|
+
console.error('Error: Server not built. This should not happen with the npm package.');
|
|
30
|
+
console.error(
|
|
31
|
+
'Please report this issue at: https://github.com/mkreyman/mcp-memory-keeper/issues'
|
|
32
|
+
);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Change to data directory (where context.db will be created)
|
|
37
|
+
process.chdir(DATA_DIR);
|
|
38
|
+
|
|
39
|
+
// Spawn the server
|
|
40
|
+
const child = spawn(process.execPath, [serverPath, ...process.argv.slice(2)], {
|
|
41
|
+
stdio: 'inherit',
|
|
42
|
+
env: process.env,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Handle exit
|
|
46
|
+
child.on('exit', code => {
|
|
47
|
+
process.exit(code);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Handle errors
|
|
51
|
+
child.on('error', err => {
|
|
52
|
+
console.error('Failed to start memory-keeper server:', err);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
});
|
|
@@ -0,0 +1,234 @@
|
|
|
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
|
+
* Issue #33 reproduction test.
|
|
43
|
+
*
|
|
44
|
+
* Reproduces the exact scenario reported: spawn the MCP server with each
|
|
45
|
+
* tool profile (minimal, standard, full), call tools/list, and validate
|
|
46
|
+
* that every returned tool schema passes strict JSON Schema validation
|
|
47
|
+
* that OpenAI-compatible providers enforce.
|
|
48
|
+
*
|
|
49
|
+
* The reporter found:
|
|
50
|
+
* - minimal: works
|
|
51
|
+
* - standard: works
|
|
52
|
+
* - full: fails (context_delegate.input.insights missing items)
|
|
53
|
+
*
|
|
54
|
+
* After the fix, all three profiles must pass.
|
|
55
|
+
*
|
|
56
|
+
* @see https://github.com/mkreyman/mcp-memory-keeper/issues/33
|
|
57
|
+
*/
|
|
58
|
+
/**
|
|
59
|
+
* Strict schema validation matching what OpenAI-compatible providers enforce.
|
|
60
|
+
* Returns violation strings. Empty array = valid.
|
|
61
|
+
*/
|
|
62
|
+
function strictValidateSchema(prop, path, toolName) {
|
|
63
|
+
const violations = [];
|
|
64
|
+
if (!prop.type) {
|
|
65
|
+
violations.push(`${toolName}: ${path} — missing 'type'`);
|
|
66
|
+
return violations;
|
|
67
|
+
}
|
|
68
|
+
// Arrays MUST have items (the exact bug from issue #33)
|
|
69
|
+
if (prop.type === 'array' && !prop.items) {
|
|
70
|
+
violations.push(`${toolName}: ${path} — array missing 'items' (issue #33)`);
|
|
71
|
+
}
|
|
72
|
+
// Enum values must match declared type
|
|
73
|
+
if (prop.enum && prop.type === 'string') {
|
|
74
|
+
for (const val of prop.enum) {
|
|
75
|
+
if (typeof val !== 'string') {
|
|
76
|
+
violations.push(`${toolName}: ${path} — enum value '${val}' is not a string`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Required fields must exist in properties
|
|
81
|
+
if (prop.type === 'object' && prop.required && prop.properties) {
|
|
82
|
+
for (const req of prop.required) {
|
|
83
|
+
if (!(req in prop.properties)) {
|
|
84
|
+
violations.push(`${toolName}: ${path} — required '${req}' not in properties`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Recurse into object properties
|
|
89
|
+
if (prop.type === 'object' && prop.properties) {
|
|
90
|
+
for (const [key, value] of Object.entries(prop.properties)) {
|
|
91
|
+
violations.push(...strictValidateSchema(value, `${path}.${key}`, toolName));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Recurse into array items
|
|
95
|
+
if (prop.type === 'array' && prop.items && typeof prop.items === 'object') {
|
|
96
|
+
violations.push(...strictValidateSchema(prop.items, `${path}.items`, toolName));
|
|
97
|
+
}
|
|
98
|
+
return violations;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Spawn a server with a given TOOL_PROFILE, call tools/list, return the tools.
|
|
102
|
+
*/
|
|
103
|
+
async function getToolsForProfile(profile) {
|
|
104
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), `mcp-issue33-${profile}-`));
|
|
105
|
+
const proc = (0, child_process_1.spawn)('node', [path.join(__dirname, '../../../dist/index.js')], {
|
|
106
|
+
env: { ...process.env, DATA_DIR: tempDir, TOOL_PROFILE: profile },
|
|
107
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
108
|
+
});
|
|
109
|
+
if (global.testProcesses) {
|
|
110
|
+
global.testProcesses.push(proc);
|
|
111
|
+
}
|
|
112
|
+
let msgId = 0;
|
|
113
|
+
let outputBuffer = '';
|
|
114
|
+
const sendRequest = (method, params = {}) => {
|
|
115
|
+
return new Promise((resolve, reject) => {
|
|
116
|
+
const id = ++msgId;
|
|
117
|
+
const timeout = setTimeout(() => reject(new Error(`Timeout: ${method} on ${profile}`)), 10000);
|
|
118
|
+
const onData = (data) => {
|
|
119
|
+
outputBuffer += data.toString();
|
|
120
|
+
const lines = outputBuffer.split('\n');
|
|
121
|
+
outputBuffer = lines.pop() || '';
|
|
122
|
+
for (const line of lines) {
|
|
123
|
+
if (!line.trim())
|
|
124
|
+
continue;
|
|
125
|
+
try {
|
|
126
|
+
const msg = JSON.parse(line);
|
|
127
|
+
if (msg.id === id) {
|
|
128
|
+
clearTimeout(timeout);
|
|
129
|
+
proc.stdout?.removeListener('data', onData);
|
|
130
|
+
resolve(msg);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// skip
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
proc.stdout?.on('data', onData);
|
|
139
|
+
proc.stdin?.write(JSON.stringify({ jsonrpc: '2.0', method, params, id }) + '\n');
|
|
140
|
+
});
|
|
141
|
+
};
|
|
142
|
+
// Initialize
|
|
143
|
+
await sendRequest('initialize', {
|
|
144
|
+
protocolVersion: '2024-11-05',
|
|
145
|
+
capabilities: {},
|
|
146
|
+
clientInfo: { name: 'issue33-repro', version: '1.0.0' },
|
|
147
|
+
});
|
|
148
|
+
proc.stdin?.write(JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n');
|
|
149
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
150
|
+
// Get tools
|
|
151
|
+
const response = await sendRequest('tools/list');
|
|
152
|
+
return { tools: response.result.tools, process: proc, tempDir };
|
|
153
|
+
}
|
|
154
|
+
const profiles = ['minimal', 'standard', 'full'];
|
|
155
|
+
const servers = [];
|
|
156
|
+
(0, globals_1.describe)('Issue #33 reproduction: all profiles must pass strict schema validation', () => {
|
|
157
|
+
(0, globals_1.afterAll)(async () => {
|
|
158
|
+
for (const server of servers) {
|
|
159
|
+
if (server.process && !server.process.killed) {
|
|
160
|
+
server.process.kill('SIGTERM');
|
|
161
|
+
await new Promise(resolve => {
|
|
162
|
+
const timeout = setTimeout(() => {
|
|
163
|
+
server.process?.kill('SIGKILL');
|
|
164
|
+
resolve();
|
|
165
|
+
}, 3000);
|
|
166
|
+
server.process?.on('exit', () => {
|
|
167
|
+
clearTimeout(timeout);
|
|
168
|
+
resolve();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
server.process?.removeAllListeners();
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
fs.rmSync(server.tempDir, { recursive: true, force: true });
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
// ignore
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
for (const profile of profiles) {
|
|
182
|
+
(0, globals_1.describe)(`TOOL_PROFILE=${profile}`, () => {
|
|
183
|
+
let tools;
|
|
184
|
+
beforeAll(async () => {
|
|
185
|
+
const result = await getToolsForProfile(profile);
|
|
186
|
+
tools = result.tools;
|
|
187
|
+
servers.push({ process: result.process, tempDir: result.tempDir });
|
|
188
|
+
}, 15000);
|
|
189
|
+
(0, globals_1.it)('should return tools', () => {
|
|
190
|
+
(0, globals_1.expect)(tools.length).toBeGreaterThan(0);
|
|
191
|
+
});
|
|
192
|
+
(0, globals_1.it)('all tool schemas should pass strict validation', () => {
|
|
193
|
+
const allViolations = [];
|
|
194
|
+
for (const tool of tools) {
|
|
195
|
+
allViolations.push(...strictValidateSchema(tool.inputSchema, 'inputSchema', tool.name));
|
|
196
|
+
}
|
|
197
|
+
if (allViolations.length > 0) {
|
|
198
|
+
throw new Error(`Profile "${profile}" has ${allViolations.length} schema violation(s):\n` +
|
|
199
|
+
allViolations.map(v => ` ${v}`).join('\n'));
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
(0, globals_1.it)('specifically: no array property should be missing items', () => {
|
|
203
|
+
const arrayViolations = [];
|
|
204
|
+
for (const tool of tools) {
|
|
205
|
+
arrayViolations.push(...strictValidateSchema(tool.inputSchema, 'inputSchema', tool.name).filter(v => v.includes('missing')));
|
|
206
|
+
}
|
|
207
|
+
(0, globals_1.expect)(arrayViolations).toHaveLength(0);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
// The reporter specifically bisected these tools
|
|
212
|
+
(0, globals_1.describe)('reporter-flagged tools', () => {
|
|
213
|
+
let fullTools;
|
|
214
|
+
beforeAll(async () => {
|
|
215
|
+
const result = await getToolsForProfile('full');
|
|
216
|
+
fullTools = result.tools;
|
|
217
|
+
servers.push({ process: result.process, tempDir: result.tempDir });
|
|
218
|
+
}, 15000);
|
|
219
|
+
const flaggedTools = [
|
|
220
|
+
'context_delegate',
|
|
221
|
+
'context_find_related',
|
|
222
|
+
'context_visualize',
|
|
223
|
+
'context_branch_session',
|
|
224
|
+
];
|
|
225
|
+
for (const toolName of flaggedTools) {
|
|
226
|
+
(0, globals_1.it)(`${toolName} should pass strict schema validation`, () => {
|
|
227
|
+
const tool = fullTools.find((t) => t.name === toolName);
|
|
228
|
+
(0, globals_1.expect)(tool).toBeDefined();
|
|
229
|
+
const violations = strictValidateSchema(tool.inputSchema, 'inputSchema', toolName);
|
|
230
|
+
(0, globals_1.expect)(violations).toHaveLength(0);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
});
|
|
@@ -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
|
+
});
|