@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.
Files changed (110) hide show
  1. package/CHANGELOG.md +542 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1281 -0
  4. package/bin/mcp-memory-keeper +54 -0
  5. package/dist/__tests__/e2e/issue33-reproduce.test.js +234 -0
  6. package/dist/__tests__/e2e/server-e2e.test.js +341 -0
  7. package/dist/__tests__/helpers/database-test-helper.js +160 -0
  8. package/dist/__tests__/helpers/test-server.js +92 -0
  9. package/dist/__tests__/integration/advanced-features.test.js +614 -0
  10. package/dist/__tests__/integration/backward-compatibility.test.js +245 -0
  11. package/dist/__tests__/integration/batchOperationsE2E.test.js +396 -0
  12. package/dist/__tests__/integration/batchOperationsHandler.test.js +1230 -0
  13. package/dist/__tests__/integration/channelManagementHandler.test.js +1291 -0
  14. package/dist/__tests__/integration/channels.test.js +376 -0
  15. package/dist/__tests__/integration/checkpoint.test.js +251 -0
  16. package/dist/__tests__/integration/concurrent-access.test.js +190 -0
  17. package/dist/__tests__/integration/context-operations.test.js +243 -0
  18. package/dist/__tests__/integration/contextDiff.test.js +852 -0
  19. package/dist/__tests__/integration/contextDiffHandler.test.js +976 -0
  20. package/dist/__tests__/integration/contextExportHandler.test.js +510 -0
  21. package/dist/__tests__/integration/contextGetPaginationDefaults.test.js +298 -0
  22. package/dist/__tests__/integration/contextReassignChannelHandler.test.js +908 -0
  23. package/dist/__tests__/integration/contextRelationshipsHandler.test.js +1151 -0
  24. package/dist/__tests__/integration/contextSearch.test.js +1054 -0
  25. package/dist/__tests__/integration/contextSearchHandler.test.js +552 -0
  26. package/dist/__tests__/integration/contextWatchActual.test.js +165 -0
  27. package/dist/__tests__/integration/contextWatchHandler.test.js +1500 -0
  28. package/dist/__tests__/integration/database-initialization.test.js +134 -0
  29. package/dist/__tests__/integration/enhanced-context-operations.test.js +1082 -0
  30. package/dist/__tests__/integration/enhancedContextGetHandler.test.js +915 -0
  31. package/dist/__tests__/integration/enhancedContextTimelineHandler.test.js +716 -0
  32. package/dist/__tests__/integration/error-cases.test.js +411 -0
  33. package/dist/__tests__/integration/export-import.test.js +367 -0
  34. package/dist/__tests__/integration/feature-flags.test.js +542 -0
  35. package/dist/__tests__/integration/file-operations.test.js +264 -0
  36. package/dist/__tests__/integration/filterBySessionId.test.js +251 -0
  37. package/dist/__tests__/integration/git-integration.test.js +241 -0
  38. package/dist/__tests__/integration/index-tools.test.js +496 -0
  39. package/dist/__tests__/integration/issue11-actual-bug-demo.test.js +304 -0
  40. package/dist/__tests__/integration/issue11-search-filters-bug.test.js +561 -0
  41. package/dist/__tests__/integration/issue12-checkpoint-restore-behavior.test.js +621 -0
  42. package/dist/__tests__/integration/issue13-key-validation.test.js +433 -0
  43. package/dist/__tests__/integration/issue24-final-fix.test.js +241 -0
  44. package/dist/__tests__/integration/issue24-fix-validation.test.js +158 -0
  45. package/dist/__tests__/integration/issue24-reproduce.test.js +225 -0
  46. package/dist/__tests__/integration/issue24-token-limit.test.js +199 -0
  47. package/dist/__tests__/integration/issue33-array-items-schema.test.js +165 -0
  48. package/dist/__tests__/integration/knowledge-graph.test.js +338 -0
  49. package/dist/__tests__/integration/migrations.test.js +528 -0
  50. package/dist/__tests__/integration/multi-agent.test.js +546 -0
  51. package/dist/__tests__/integration/pagination-critical-fix.test.js +296 -0
  52. package/dist/__tests__/integration/paginationDefaultsHandler.test.js +600 -0
  53. package/dist/__tests__/integration/project-directory.test.js +291 -0
  54. package/dist/__tests__/integration/resource-cleanup.test.js +149 -0
  55. package/dist/__tests__/integration/retention.test.js +513 -0
  56. package/dist/__tests__/integration/search.test.js +333 -0
  57. package/dist/__tests__/integration/semantic-search.test.js +266 -0
  58. package/dist/__tests__/integration/server-initialization.test.js +305 -0
  59. package/dist/__tests__/integration/session-management.test.js +219 -0
  60. package/dist/__tests__/integration/simplified-sharing.test.js +346 -0
  61. package/dist/__tests__/integration/smart-compaction.test.js +230 -0
  62. package/dist/__tests__/integration/summarization.test.js +308 -0
  63. package/dist/__tests__/integration/tokenLimitEnforcement.test.js +134 -0
  64. package/dist/__tests__/integration/tool-profiles-integration.test.js +150 -0
  65. package/dist/__tests__/integration/watcher-migration-validation.test.js +544 -0
  66. package/dist/__tests__/security/input-validation.test.js +115 -0
  67. package/dist/__tests__/utils/agents.test.js +473 -0
  68. package/dist/__tests__/utils/database.test.js +177 -0
  69. package/dist/__tests__/utils/git.test.js +122 -0
  70. package/dist/__tests__/utils/knowledge-graph.test.js +297 -0
  71. package/dist/__tests__/utils/migrationHealthCheck.test.js +302 -0
  72. package/dist/__tests__/utils/project-directory-messages.test.js +192 -0
  73. package/dist/__tests__/utils/timezone-safe-dates.js +119 -0
  74. package/dist/__tests__/utils/token-limits.test.js +225 -0
  75. package/dist/__tests__/utils/tool-profiles.test.js +374 -0
  76. package/dist/__tests__/utils/validation.test.js +200 -0
  77. package/dist/__tests__/utils/vector-store.test.js +231 -0
  78. package/dist/handlers/contextWatchHandlers.js +206 -0
  79. package/dist/index.js +4425 -0
  80. package/dist/migrations/003_add_channels.js +174 -0
  81. package/dist/migrations/004_add_context_watch.js +151 -0
  82. package/dist/migrations/005_add_context_watch.js +98 -0
  83. package/dist/migrations/simplify-sharing.js +117 -0
  84. package/dist/repositories/BaseRepository.js +30 -0
  85. package/dist/repositories/CheckpointRepository.js +140 -0
  86. package/dist/repositories/ContextRepository.js +2017 -0
  87. package/dist/repositories/FileRepository.js +104 -0
  88. package/dist/repositories/RepositoryManager.js +62 -0
  89. package/dist/repositories/SessionRepository.js +66 -0
  90. package/dist/repositories/WatcherRepository.js +252 -0
  91. package/dist/repositories/index.js +15 -0
  92. package/dist/test-helpers/database-helper.js +128 -0
  93. package/dist/types/entities.js +3 -0
  94. package/dist/utils/agents.js +791 -0
  95. package/dist/utils/channels.js +150 -0
  96. package/dist/utils/database.js +780 -0
  97. package/dist/utils/feature-flags.js +476 -0
  98. package/dist/utils/git.js +145 -0
  99. package/dist/utils/knowledge-graph.js +264 -0
  100. package/dist/utils/migrationHealthCheck.js +373 -0
  101. package/dist/utils/migrations.js +452 -0
  102. package/dist/utils/retention.js +460 -0
  103. package/dist/utils/timestamps.js +112 -0
  104. package/dist/utils/token-limits.js +350 -0
  105. package/dist/utils/tool-profiles.js +242 -0
  106. package/dist/utils/validation.js +296 -0
  107. package/dist/utils/vector-store.js +247 -0
  108. package/examples/config.json +31 -0
  109. package/examples/project-directory-setup.md +114 -0
  110. 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
+ });