mcp-memory-keeper 0.10.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,7 +7,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- ## [0.10.1] - 2025-01-11
10
+ ## [0.11.0] - 2025-12-10
11
+
12
+ ### Breaking Changes
13
+
14
+ - **Node.js 18 support dropped** - Minimum required Node.js version is now 20.0.0
15
+ - Node.js 18 reached End-of-Life on April 30, 2025
16
+ - Users on Node.js 18 must upgrade to Node.js 20 or later
17
+ - Existing installations will continue working until updated
18
+
19
+ ### Fixed
20
+
21
+ - **Installation fails on Node.js 24 (#28)** - Updated `better-sqlite3` dependency
22
+ - Upgraded from `^11.10.0` to `^12.1.0` to support Node.js 24 (LTS "Krypton")
23
+ - Prebuilt binaries now available for Node.js 20, 22, and 24
24
+ - Resolves `gyp ERR!` build failures on Node.js 24
25
+
26
+ ## [0.10.2] - 2025-09-16
27
+
28
+ ### Fixed
29
+
30
+ - **Critical Token Limit Issue (#24)** - Fixed token overflow with includeMetadata
31
+ - Implemented dynamic token limit calculation based on actual content size
32
+ - Automatically adjusts item limits based on average item size in session
33
+ - More accurate token estimation (3.5 chars/token vs 4)
34
+ - Configurable via environment variables (MCP_MAX_TOKENS, MCP_TOKEN_SAFETY_BUFFER)
35
+ - Added tokenInfo to response metadata for transparency
36
+ - Resolves "MCP tool context_get response exceeds maximum allowed tokens" errors
37
+
38
+ ### Added
39
+
40
+ - **Token Limit Management Module** (`utils/token-limits.ts`)
41
+ - Dynamic calculation of safe item limits
42
+ - Response overhead estimation
43
+ - Configurable token limits via environment
44
+ - Better visibility into token usage
45
+ - Proper TypeScript interfaces for context items
46
+ - Environment variable validation with bounds checking
47
+ - Safe JSON parsing with error handling
48
+ - Well-documented constants replacing magic numbers
49
+
50
+ ## [0.10.1] - 2025-07-11
11
51
 
12
52
  ### Fixed
13
53
 
@@ -63,7 +103,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
63
103
  - Added recipes for common patterns in RECIPES.md
64
104
  - Added troubleshooting tips for new features
65
105
 
66
- ## [0.10.0] - 2025-01-26
106
+ ## [0.10.0] - 2025-06-26
67
107
 
68
108
  ### Added
69
109
 
@@ -102,7 +142,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
102
142
  - Enhanced validation for channel names
103
143
  - Backward compatible - existing items default to 'default' channel
104
144
 
105
- ## [0.9.0] - 2025-01-20
145
+ ## [0.9.0] - 2025-06-21
106
146
 
107
147
  ### Changed (BREAKING)
108
148
 
@@ -125,7 +165,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
125
165
  - `context_get_shared` tool (use `context_get` instead)
126
166
  - Complex sharing mechanism that was causing inconsistencies
127
167
 
128
- ## [0.8.4] - 2025-01-19
168
+ ## [0.8.4] - 2025-06-19
129
169
 
130
170
  ### Fixed
131
171
 
@@ -139,7 +179,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
139
179
  - Feature flags system (planned)
140
180
  - Database migration system (planned)
141
181
 
142
- ## [0.8.3] - 2025-01-19
182
+ ## [0.8.3] - 2025-06-19
143
183
 
144
184
  ### Added
145
185
 
@@ -160,7 +200,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
160
200
 
161
201
  - Automatic schema migration for existing databases to add the `working_directory` column
162
202
 
163
- ## [0.8.0] - 2024-01-18
203
+ ## [0.8.0] - 2025-06-18
164
204
 
165
205
  ### Added
166
206
 
@@ -200,7 +240,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
200
240
  - New tables: `journal_entries`, `compressed_context`, `tool_events`
201
241
  - All 255 tests passing
202
242
 
203
- ## [0.7.0] - 2024-01-18
243
+ ## [0.7.0] - 2025-06-18
204
244
 
205
245
  ### Added
206
246
 
@@ -224,7 +264,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
224
264
  - Added comprehensive test coverage (30 new tests)
225
265
  - All 236 tests passing
226
266
 
227
- ## [0.6.0] - 2024-01-17
267
+ ## [0.6.0] - 2025-06-17
228
268
 
229
269
  ### Added
230
270
 
@@ -247,7 +287,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
247
287
  - Comprehensive test coverage for semantic search
248
288
  - All 206 tests passing
249
289
 
250
- ## [0.5.0] - 2024-01-17
290
+ ## [0.5.0] - 2025-06-17
251
291
 
252
292
  ### Added
253
293
 
@@ -270,7 +310,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
270
310
  - Added `knowledge-graph.ts` utility module
271
311
  - Comprehensive test coverage for graph operations
272
312
 
273
- ## [0.4.2] - 2024-01-16
313
+ ## [0.4.2] - 2025-06-17
274
314
 
275
315
  ### Added
276
316
 
@@ -284,7 +324,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
284
324
  - Git integration error handling
285
325
  - Session list date filtering
286
326
 
287
- ## [0.4.1] - 2024-01-16
327
+ ## [0.4.1] - 2025-06-17
288
328
 
289
329
  ### Fixed
290
330
 
@@ -297,7 +337,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
297
337
  - Improved error messages for better debugging
298
338
  - Enhanced validation for file paths
299
339
 
300
- ## [0.4.0] - 2024-01-15
340
+ ## [0.4.0] - 2025-06-17
301
341
 
302
342
  ### Added
303
343
 
@@ -318,7 +358,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
318
358
  - Created `git.ts` utility module
319
359
  - 97% test coverage maintained
320
360
 
321
- ## [0.3.0] - 2024-01-14
361
+ ## [0.3.0] - 2025-06-17
322
362
 
323
363
  ### Added
324
364
 
@@ -347,7 +387,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
347
387
  - Implemented streaming for large exports
348
388
  - Transaction support for atomic operations
349
389
 
350
- ## [0.2.0] - 2024-01-13
390
+ ## [0.2.0] - 2025-06-17
351
391
 
352
392
  ### Added
353
393
 
@@ -377,7 +417,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
377
417
  - Memory leak in file cache operations
378
418
  - Session switching race condition
379
419
 
380
- ## [0.1.0] - 2024-01-12
420
+ ## [0.1.0] - 2025-06-17
381
421
 
382
422
  ### Added
383
423
 
@@ -405,12 +445,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
405
445
 
406
446
  ## Development Releases
407
447
 
408
- ### [0.1.0-beta.2] - 2024-01-11
448
+ ### [0.1.0-beta.2] - 2025-06-17
409
449
 
410
450
  - Fixed Windows path handling
411
451
  - Added Node.js 18+ compatibility
412
452
 
413
- ### [0.1.0-beta.1] - 2024-01-10
453
+ ### [0.1.0-beta.1] - 2025-06-17
414
454
 
415
455
  - Initial beta release
416
456
  - Basic functionality testing
package/README.md CHANGED
@@ -183,10 +183,36 @@ claude mcp add memory-keeper node /absolute/path/to/mcp-memory-keeper/dist/index
183
183
 
184
184
  ### Environment Variables
185
185
 
186
+ #### Storage and Installation
187
+
186
188
  - `DATA_DIR` - Directory for database storage (default: `~/mcp-data/memory-keeper/`)
187
189
  - `MEMORY_KEEPER_INSTALL_DIR` - Installation directory (default: `~/.local/mcp-servers/memory-keeper/`)
188
190
  - `MEMORY_KEEPER_AUTO_UPDATE` - Set to `1` to enable auto-updates
189
191
 
192
+ #### Token Limit Configuration
193
+
194
+ - `MCP_MAX_TOKENS` - Maximum tokens allowed in responses (default: `25000`, range: `1000-100000`)
195
+ - Adjust this if your MCP client has different limits
196
+ - `MCP_TOKEN_SAFETY_BUFFER` - Safety buffer percentage (default: `0.8`, range: `0.1-1.0`)
197
+ - Uses only this fraction of the max tokens to prevent overflows
198
+ - `MCP_MIN_ITEMS` - Minimum items to return even if exceeding limits (default: `1`, range: `1-100`)
199
+ - Ensures at least some results are returned
200
+ - `MCP_MAX_ITEMS` - Maximum items allowed per response (default: `100`, range: `10-1000`)
201
+ - Upper bound for result sets regardless of token limits
202
+ - `MCP_CHARS_PER_TOKEN` - Characters per token ratio (default: `3.5`, range: `2.5-5.0`) **[Advanced]**
203
+ - Adjusts token estimation accuracy for different content types
204
+ - Lower values = more conservative (safer but returns fewer items)
205
+ - Higher values = more aggressive (returns more items but risks overflow)
206
+
207
+ Example configuration for stricter token limits:
208
+
209
+ ```bash
210
+ export MCP_MAX_TOKENS=20000 # Lower max tokens
211
+ export MCP_TOKEN_SAFETY_BUFFER=0.7 # More conservative buffer
212
+ export MCP_MAX_ITEMS=50 # Fewer items per response
213
+ export MCP_CHARS_PER_TOKEN=3.0 # More conservative estimation (optional)
214
+ ```
215
+
190
216
  ### Claude Code (CLI)
191
217
 
192
218
  #### Configuration Scopes
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ /**
3
+ * Test for token limit issue with channel queries
4
+ *
5
+ * Reproduces the bug where context_get with limit: 50 still exceeds token limits
6
+ * when querying a channel with large items.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ const globals_1 = require("@jest/globals");
10
+ const database_js_1 = require("../../utils/database.js");
11
+ const RepositoryManager_js_1 = require("../../repositories/RepositoryManager.js");
12
+ const token_limits_js_1 = require("../../utils/token-limits.js");
13
+ (0, globals_1.describe)('Token Limit with Channel Query Bug', () => {
14
+ let dbManager;
15
+ let repositories;
16
+ let sessionId;
17
+ (0, globals_1.beforeEach)(() => {
18
+ dbManager = new database_js_1.DatabaseManager({ filename: ':memory:' });
19
+ repositories = new RepositoryManager_js_1.RepositoryManager(dbManager);
20
+ const session = repositories.sessions.create({
21
+ name: 'Test Session',
22
+ defaultChannel: 'test-channel',
23
+ });
24
+ sessionId = session.id;
25
+ });
26
+ (0, globals_1.afterEach)(() => {
27
+ dbManager.close();
28
+ });
29
+ (0, globals_1.it)('should enforce token limits even when limit parameter is provided', () => {
30
+ // Create 50 large context items (each ~1000 chars)
31
+ const largeValue = 'x'.repeat(1000);
32
+ for (let i = 0; i < 50; i++) {
33
+ repositories.contexts.save(sessionId, {
34
+ key: `large-item-${i}`,
35
+ value: largeValue,
36
+ channel: 'outbound-call-center',
37
+ priority: 'normal',
38
+ });
39
+ }
40
+ // Query with limit: 50
41
+ const result = repositories.contexts.queryEnhanced({
42
+ sessionId,
43
+ channel: 'outbound-call-center',
44
+ limit: 50,
45
+ sort: 'updated_desc',
46
+ includeMetadata: false,
47
+ });
48
+ (0, globals_1.expect)(result.items.length).toBe(50);
49
+ // Check if response would exceed token limit
50
+ const tokenConfig = (0, token_limits_js_1.getTokenConfig)();
51
+ const { exceedsLimit, safeItemCount } = (0, token_limits_js_1.checkTokenLimit)(result.items, false, tokenConfig);
52
+ // Build the actual response structure
53
+ const response = {
54
+ items: result.items,
55
+ pagination: {
56
+ total: result.totalCount,
57
+ returned: result.items.length,
58
+ offset: 0,
59
+ hasMore: false,
60
+ nextOffset: null,
61
+ truncated: false,
62
+ truncatedCount: 0,
63
+ },
64
+ };
65
+ const responseJson = JSON.stringify(response, null, 2);
66
+ const actualTokens = Math.ceil(responseJson.length / tokenConfig.charsPerToken);
67
+ // The bug: even with limit: 50, the response can exceed token limits
68
+ if (actualTokens > tokenConfig.mcpMaxTokens) {
69
+ // BUG REPRODUCED: The response exceeds token limits
70
+ // Verify that checkTokenLimit correctly detected the issue
71
+ (0, globals_1.expect)(exceedsLimit).toBe(true);
72
+ (0, globals_1.expect)(safeItemCount).toBeLessThan(50);
73
+ (0, globals_1.expect)(actualTokens).toBeGreaterThan(tokenConfig.mcpMaxTokens);
74
+ }
75
+ });
76
+ (0, globals_1.it)('should respect token limits over user-provided limit parameter', () => {
77
+ // Create 100 large context items (each ~800 chars)
78
+ const largeValue = 'y'.repeat(800);
79
+ for (let i = 0; i < 100; i++) {
80
+ repositories.contexts.save(sessionId, {
81
+ key: `item-${i}`,
82
+ value: largeValue,
83
+ channel: 'test-channel',
84
+ priority: 'normal',
85
+ });
86
+ }
87
+ // Query with limit: 50 (user expectation)
88
+ const result = repositories.contexts.queryEnhanced({
89
+ sessionId,
90
+ channel: 'test-channel',
91
+ limit: 50,
92
+ sort: 'created_desc',
93
+ includeMetadata: false,
94
+ });
95
+ // Simulate the context_get handler logic
96
+ const tokenConfig = (0, token_limits_js_1.getTokenConfig)();
97
+ const { exceedsLimit, safeItemCount } = (0, token_limits_js_1.checkTokenLimit)(result.items, false, tokenConfig);
98
+ let actualItems = result.items;
99
+ let wasTruncated = false;
100
+ if (exceedsLimit && safeItemCount < result.items.length) {
101
+ actualItems = result.items.slice(0, safeItemCount);
102
+ wasTruncated = true;
103
+ }
104
+ // Build response
105
+ const response = {
106
+ items: actualItems,
107
+ pagination: {
108
+ total: result.totalCount,
109
+ returned: actualItems.length,
110
+ offset: 0,
111
+ hasMore: wasTruncated || actualItems.length < result.totalCount,
112
+ nextOffset: wasTruncated ? actualItems.length : null,
113
+ truncated: wasTruncated,
114
+ truncatedCount: wasTruncated ? result.items.length - actualItems.length : 0,
115
+ },
116
+ };
117
+ const responseJson = JSON.stringify(response, null, 2);
118
+ const actualTokens = Math.ceil(responseJson.length / tokenConfig.charsPerToken);
119
+ // Verify token limit is not exceeded
120
+ (0, globals_1.expect)(actualTokens).toBeLessThanOrEqual(tokenConfig.mcpMaxTokens);
121
+ // Verify truncation occurred if needed
122
+ if (exceedsLimit) {
123
+ (0, globals_1.expect)(wasTruncated).toBe(true);
124
+ (0, globals_1.expect)(actualItems.length).toBeLessThan(50);
125
+ (0, globals_1.expect)(actualItems.length).toBe(safeItemCount);
126
+ }
127
+ });
128
+ });
@@ -0,0 +1,241 @@
1
+ "use strict";
2
+ // Final test to verify the fix for issue #24
3
+ // Simulates the exact scenario: context_get with sessionId: "current" and includeMetadata: true
4
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
5
+ if (k2 === undefined) k2 = k;
6
+ var desc = Object.getOwnPropertyDescriptor(m, k);
7
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
8
+ desc = { enumerable: true, get: function() { return m[k]; } };
9
+ }
10
+ Object.defineProperty(o, k2, desc);
11
+ }) : (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ o[k2] = m[k];
14
+ }));
15
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
16
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
17
+ }) : function(o, v) {
18
+ o["default"] = v;
19
+ });
20
+ var __importStar = (this && this.__importStar) || (function () {
21
+ var ownKeys = function(o) {
22
+ ownKeys = Object.getOwnPropertyNames || function (o) {
23
+ var ar = [];
24
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
25
+ return ar;
26
+ };
27
+ return ownKeys(o);
28
+ };
29
+ return function (mod) {
30
+ if (mod && mod.__esModule) return mod;
31
+ var result = {};
32
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
33
+ __setModuleDefault(result, mod);
34
+ return result;
35
+ };
36
+ })();
37
+ Object.defineProperty(exports, "__esModule", { value: true });
38
+ const database_1 = require("../../utils/database");
39
+ const RepositoryManager_1 = require("../../repositories/RepositoryManager");
40
+ const os = __importStar(require("os"));
41
+ const path = __importStar(require("path"));
42
+ const fs = __importStar(require("fs"));
43
+ // Mock functions from index.ts
44
+ function estimateTokens(text) {
45
+ return Math.ceil(text.length / 4);
46
+ }
47
+ function calculateSize(value) {
48
+ return Buffer.byteLength(value, 'utf8');
49
+ }
50
+ function validatePaginationParams(params) {
51
+ const errors = [];
52
+ let limit = 25; // default
53
+ let offset = 0; // default
54
+ // Validate limit
55
+ if (params.limit !== undefined && params.limit !== null) {
56
+ const rawLimit = params.limit;
57
+ if (!Number.isInteger(rawLimit) || rawLimit <= 0) {
58
+ errors.push(`Invalid limit: expected positive integer, got ${typeof rawLimit} '${rawLimit}'`);
59
+ }
60
+ else {
61
+ limit = Math.min(Math.max(1, rawLimit), 100); // clamp between 1-100
62
+ }
63
+ }
64
+ // Validate offset
65
+ if (params.offset !== undefined && params.offset !== null) {
66
+ const rawOffset = params.offset;
67
+ if (!Number.isInteger(rawOffset) || rawOffset < 0) {
68
+ errors.push(`Invalid offset: expected non-negative integer, got ${typeof rawOffset} '${rawOffset}'`);
69
+ }
70
+ else {
71
+ offset = rawOffset;
72
+ }
73
+ }
74
+ return { limit, offset, errors };
75
+ }
76
+ describe('Issue #24 - Final Fix Verification', () => {
77
+ let dbManager;
78
+ let repositories;
79
+ let tempDbPath;
80
+ let testSessionId;
81
+ beforeEach(() => {
82
+ tempDbPath = path.join(os.tmpdir(), `test-issue24-final-${Date.now()}.db`);
83
+ dbManager = new database_1.DatabaseManager({
84
+ filename: tempDbPath,
85
+ maxSize: 10 * 1024 * 1024,
86
+ walMode: true,
87
+ });
88
+ repositories = new RepositoryManager_1.RepositoryManager(dbManager);
89
+ // Create test session (simulating "current" session)
90
+ const session = repositories.sessions.create({
91
+ name: 'Current Session',
92
+ description: 'Simulating the current active session',
93
+ });
94
+ testSessionId = session.id;
95
+ // Create many realistic items that would cause overflow
96
+ const largeContent = `
97
+ This is a realistic context item saved during development.
98
+ It contains implementation details, code snippets, notes, and documentation.
99
+ The content is substantial to simulate real-world usage patterns.
100
+ Developers often save detailed context about complex features, debugging sessions,
101
+ architectural decisions, API documentation, and troubleshooting information.
102
+ `.trim();
103
+ for (let i = 0; i < 200; i++) {
104
+ repositories.contexts.save(testSessionId, {
105
+ key: `context_${String(i).padStart(3, '0')}`,
106
+ value: `${largeContent}\n\nSpecific item ${i} notes and details.`,
107
+ category: ['task', 'decision', 'progress', 'note'][i % 4],
108
+ priority: ['high', 'normal', 'low'][i % 3],
109
+ channel: `channel-${i % 10}`,
110
+ metadata: JSON.stringify({
111
+ index: i,
112
+ timestamp: new Date().toISOString(),
113
+ tags: ['dev', 'test', 'review'][i % 3],
114
+ }),
115
+ });
116
+ }
117
+ });
118
+ afterEach(() => {
119
+ dbManager.close();
120
+ try {
121
+ fs.unlinkSync(tempDbPath);
122
+ fs.unlinkSync(`${tempDbPath}-wal`);
123
+ fs.unlinkSync(`${tempDbPath}-shm`);
124
+ }
125
+ catch (_e) {
126
+ // Ignore
127
+ }
128
+ });
129
+ it('should handle context_get with includeMetadata: true without exceeding token limit', () => {
130
+ // Simulate the exact call from the issue:
131
+ // context_get(sessionId: "current", includeMetadata: true)
132
+ const args = {
133
+ sessionId: testSessionId, // Would be resolved from "current"
134
+ includeMetadata: true,
135
+ // No limit specified - should use new default of 30
136
+ };
137
+ // Simulate what the handler does
138
+ const includeMetadata = args.includeMetadata;
139
+ const rawLimit = undefined; // Not specified in the call
140
+ const rawOffset = undefined;
141
+ // Apply our fix: default limit is 30 when includeMetadata is true
142
+ const defaultLimit = includeMetadata ? 30 : 100;
143
+ const paginationValidation = validatePaginationParams({
144
+ limit: rawLimit !== undefined ? rawLimit : defaultLimit,
145
+ offset: rawOffset,
146
+ });
147
+ const { limit, offset } = paginationValidation;
148
+ console.log(`Using limit: ${limit} (includeMetadata: ${includeMetadata})`);
149
+ // Query with the calculated limit
150
+ const result = repositories.contexts.queryEnhanced({
151
+ sessionId: testSessionId,
152
+ includeMetadata: true,
153
+ limit: limit,
154
+ offset: offset,
155
+ });
156
+ console.log(`Retrieved ${result.items.length} of ${result.totalCount} items`);
157
+ // Transform items with metadata
158
+ const itemsWithMetadata = result.items.map(item => ({
159
+ key: item.key,
160
+ value: item.value,
161
+ category: item.category,
162
+ priority: item.priority,
163
+ channel: item.channel,
164
+ metadata: item.metadata ? JSON.parse(item.metadata) : null,
165
+ size: item.size || calculateSize(item.value),
166
+ created_at: item.created_at,
167
+ updated_at: item.updated_at,
168
+ }));
169
+ // Calculate metrics for token checking
170
+ const itemsForMetrics = itemsWithMetadata;
171
+ const jsonString = JSON.stringify(itemsForMetrics);
172
+ const estimatedTokens = estimateTokens(jsonString);
173
+ console.log(`Items token estimate: ${estimatedTokens}`);
174
+ // Build full response
175
+ const response = {
176
+ items: itemsWithMetadata,
177
+ pagination: {
178
+ total: result.totalCount,
179
+ returned: result.items.length,
180
+ offset: 0,
181
+ hasMore: result.totalCount > result.items.length,
182
+ nextOffset: result.items.length < result.totalCount ? result.items.length : null,
183
+ totalCount: result.totalCount,
184
+ page: 1,
185
+ pageSize: limit,
186
+ totalPages: Math.ceil(result.totalCount / limit),
187
+ hasNextPage: result.totalCount > result.items.length,
188
+ hasPreviousPage: false,
189
+ previousOffset: null,
190
+ totalSize: itemsWithMetadata.reduce((sum, item) => sum + (item.size || 0), 0),
191
+ averageSize: Math.round(itemsWithMetadata.reduce((sum, item) => sum + (item.size || 0), 0) / result.items.length),
192
+ defaultsApplied: { limit: true, sort: true },
193
+ truncated: false,
194
+ truncatedCount: 0,
195
+ },
196
+ };
197
+ const fullResponseJson = JSON.stringify(response, null, 2);
198
+ const finalTokens = estimateTokens(fullResponseJson);
199
+ console.log(`Final response tokens: ${finalTokens}`);
200
+ // Verify the fix works
201
+ expect(limit).toBe(30); // Our new default when includeMetadata is true
202
+ expect(result.items.length).toBeLessThanOrEqual(30);
203
+ expect(finalTokens).toBeLessThan(15000); // Our new conservative TOKEN_LIMIT
204
+ expect(finalTokens).toBeLessThan(25000); // MCP's actual limit
205
+ console.log('✅ Fix verified: Response stays well under token limit');
206
+ });
207
+ it('should still allow explicit higher limits but truncate if needed', () => {
208
+ // User explicitly requests more items
209
+ const args = {
210
+ sessionId: testSessionId,
211
+ includeMetadata: true,
212
+ limit: 100, // Explicitly requesting many items
213
+ };
214
+ const result = repositories.contexts.queryEnhanced({
215
+ sessionId: testSessionId,
216
+ includeMetadata: true,
217
+ limit: args.limit,
218
+ });
219
+ // With explicit limit, we get what was requested (up to 100)
220
+ expect(result.items.length).toBeLessThanOrEqual(100);
221
+ // But in the handler, if this exceeds tokens, it would be truncated
222
+ const itemsWithMetadata = result.items.map(item => ({
223
+ key: item.key,
224
+ value: item.value,
225
+ category: item.category,
226
+ priority: item.priority,
227
+ channel: item.channel,
228
+ metadata: item.metadata ? JSON.parse(item.metadata) : null,
229
+ size: item.size || calculateSize(item.value),
230
+ created_at: item.created_at,
231
+ updated_at: item.updated_at,
232
+ }));
233
+ const responseJson = JSON.stringify({ items: itemsWithMetadata }, null, 2);
234
+ const tokens = estimateTokens(responseJson);
235
+ console.log(`With explicit limit=100: ${result.items.length} items, ${tokens} tokens`);
236
+ // This would trigger truncation in the handler
237
+ if (tokens > 15000) {
238
+ console.log('⚠️ Would trigger truncation in handler');
239
+ }
240
+ });
241
+ });