mcp-memory-keeper 0.10.1 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -17
- package/README.md +26 -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__/utils/token-limits.test.js +225 -0
- package/dist/index.js +36 -84
- package/dist/utils/token-limits.js +350 -0
- package/package.json +1 -1
- package/dist/__tests__/integration/cross-session-sharing.test.js +0 -302
- package/dist/index.phase1.backup.js +0 -410
- package/dist/index.phase2.backup.js +0 -704
- package/dist/server.js +0 -384
package/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
-
## [0.10.
|
|
10
|
+
## [0.10.2] - 2025-09-16
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Critical Token Limit Issue (#24)** - Fixed token overflow with includeMetadata
|
|
15
|
+
- Implemented dynamic token limit calculation based on actual content size
|
|
16
|
+
- Automatically adjusts item limits based on average item size in session
|
|
17
|
+
- More accurate token estimation (3.5 chars/token vs 4)
|
|
18
|
+
- Configurable via environment variables (MCP_MAX_TOKENS, MCP_TOKEN_SAFETY_BUFFER)
|
|
19
|
+
- Added tokenInfo to response metadata for transparency
|
|
20
|
+
- Resolves "MCP tool context_get response exceeds maximum allowed tokens" errors
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **Token Limit Management Module** (`utils/token-limits.ts`)
|
|
25
|
+
- Dynamic calculation of safe item limits
|
|
26
|
+
- Response overhead estimation
|
|
27
|
+
- Configurable token limits via environment
|
|
28
|
+
- Better visibility into token usage
|
|
29
|
+
- Proper TypeScript interfaces for context items
|
|
30
|
+
- Environment variable validation with bounds checking
|
|
31
|
+
- Safe JSON parsing with error handling
|
|
32
|
+
- Well-documented constants replacing magic numbers
|
|
33
|
+
|
|
34
|
+
## [0.10.1] - 2025-07-11
|
|
11
35
|
|
|
12
36
|
### Fixed
|
|
13
37
|
|
|
@@ -63,7 +87,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
63
87
|
- Added recipes for common patterns in RECIPES.md
|
|
64
88
|
- Added troubleshooting tips for new features
|
|
65
89
|
|
|
66
|
-
## [0.10.0] - 2025-
|
|
90
|
+
## [0.10.0] - 2025-06-26
|
|
67
91
|
|
|
68
92
|
### Added
|
|
69
93
|
|
|
@@ -102,7 +126,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
102
126
|
- Enhanced validation for channel names
|
|
103
127
|
- Backward compatible - existing items default to 'default' channel
|
|
104
128
|
|
|
105
|
-
## [0.9.0] - 2025-
|
|
129
|
+
## [0.9.0] - 2025-06-21
|
|
106
130
|
|
|
107
131
|
### Changed (BREAKING)
|
|
108
132
|
|
|
@@ -125,7 +149,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
125
149
|
- `context_get_shared` tool (use `context_get` instead)
|
|
126
150
|
- Complex sharing mechanism that was causing inconsistencies
|
|
127
151
|
|
|
128
|
-
## [0.8.4] - 2025-
|
|
152
|
+
## [0.8.4] - 2025-06-19
|
|
129
153
|
|
|
130
154
|
### Fixed
|
|
131
155
|
|
|
@@ -139,7 +163,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
139
163
|
- Feature flags system (planned)
|
|
140
164
|
- Database migration system (planned)
|
|
141
165
|
|
|
142
|
-
## [0.8.3] - 2025-
|
|
166
|
+
## [0.8.3] - 2025-06-19
|
|
143
167
|
|
|
144
168
|
### Added
|
|
145
169
|
|
|
@@ -160,7 +184,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
160
184
|
|
|
161
185
|
- Automatic schema migration for existing databases to add the `working_directory` column
|
|
162
186
|
|
|
163
|
-
## [0.8.0] -
|
|
187
|
+
## [0.8.0] - 2025-06-18
|
|
164
188
|
|
|
165
189
|
### Added
|
|
166
190
|
|
|
@@ -200,7 +224,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
200
224
|
- New tables: `journal_entries`, `compressed_context`, `tool_events`
|
|
201
225
|
- All 255 tests passing
|
|
202
226
|
|
|
203
|
-
## [0.7.0] -
|
|
227
|
+
## [0.7.0] - 2025-06-18
|
|
204
228
|
|
|
205
229
|
### Added
|
|
206
230
|
|
|
@@ -224,7 +248,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
224
248
|
- Added comprehensive test coverage (30 new tests)
|
|
225
249
|
- All 236 tests passing
|
|
226
250
|
|
|
227
|
-
## [0.6.0] -
|
|
251
|
+
## [0.6.0] - 2025-06-17
|
|
228
252
|
|
|
229
253
|
### Added
|
|
230
254
|
|
|
@@ -247,7 +271,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
247
271
|
- Comprehensive test coverage for semantic search
|
|
248
272
|
- All 206 tests passing
|
|
249
273
|
|
|
250
|
-
## [0.5.0] -
|
|
274
|
+
## [0.5.0] - 2025-06-17
|
|
251
275
|
|
|
252
276
|
### Added
|
|
253
277
|
|
|
@@ -270,7 +294,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
270
294
|
- Added `knowledge-graph.ts` utility module
|
|
271
295
|
- Comprehensive test coverage for graph operations
|
|
272
296
|
|
|
273
|
-
## [0.4.2] -
|
|
297
|
+
## [0.4.2] - 2025-06-17
|
|
274
298
|
|
|
275
299
|
### Added
|
|
276
300
|
|
|
@@ -284,7 +308,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
284
308
|
- Git integration error handling
|
|
285
309
|
- Session list date filtering
|
|
286
310
|
|
|
287
|
-
## [0.4.1] -
|
|
311
|
+
## [0.4.1] - 2025-06-17
|
|
288
312
|
|
|
289
313
|
### Fixed
|
|
290
314
|
|
|
@@ -297,7 +321,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
297
321
|
- Improved error messages for better debugging
|
|
298
322
|
- Enhanced validation for file paths
|
|
299
323
|
|
|
300
|
-
## [0.4.0] -
|
|
324
|
+
## [0.4.0] - 2025-06-17
|
|
301
325
|
|
|
302
326
|
### Added
|
|
303
327
|
|
|
@@ -318,7 +342,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
318
342
|
- Created `git.ts` utility module
|
|
319
343
|
- 97% test coverage maintained
|
|
320
344
|
|
|
321
|
-
## [0.3.0] -
|
|
345
|
+
## [0.3.0] - 2025-06-17
|
|
322
346
|
|
|
323
347
|
### Added
|
|
324
348
|
|
|
@@ -347,7 +371,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
347
371
|
- Implemented streaming for large exports
|
|
348
372
|
- Transaction support for atomic operations
|
|
349
373
|
|
|
350
|
-
## [0.2.0] -
|
|
374
|
+
## [0.2.0] - 2025-06-17
|
|
351
375
|
|
|
352
376
|
### Added
|
|
353
377
|
|
|
@@ -377,7 +401,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
377
401
|
- Memory leak in file cache operations
|
|
378
402
|
- Session switching race condition
|
|
379
403
|
|
|
380
|
-
## [0.1.0] -
|
|
404
|
+
## [0.1.0] - 2025-06-17
|
|
381
405
|
|
|
382
406
|
### Added
|
|
383
407
|
|
|
@@ -405,12 +429,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
405
429
|
|
|
406
430
|
## Development Releases
|
|
407
431
|
|
|
408
|
-
### [0.1.0-beta.2] -
|
|
432
|
+
### [0.1.0-beta.2] - 2025-06-17
|
|
409
433
|
|
|
410
434
|
- Fixed Windows path handling
|
|
411
435
|
- Added Node.js 18+ compatibility
|
|
412
436
|
|
|
413
|
-
### [0.1.0-beta.1] -
|
|
437
|
+
### [0.1.0-beta.1] - 2025-06-17
|
|
414
438
|
|
|
415
439
|
- Initial beta release
|
|
416
440
|
- 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,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
|
+
});
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Test to validate the fix for issue #24
|
|
3
|
+
// Tests the actual flow through index.ts handler
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
const globals_1 = require("@jest/globals");
|
|
6
|
+
// Mock the functions used in index.ts for context_get
|
|
7
|
+
function estimateTokens(text) {
|
|
8
|
+
return Math.ceil(text.length / 4);
|
|
9
|
+
}
|
|
10
|
+
function calculateSize(value) {
|
|
11
|
+
return Buffer.byteLength(value, 'utf8');
|
|
12
|
+
}
|
|
13
|
+
function calculateResponseMetrics(items) {
|
|
14
|
+
let totalSize = 0;
|
|
15
|
+
for (const item of items) {
|
|
16
|
+
const itemSize = item.size || calculateSize(item.value);
|
|
17
|
+
totalSize += itemSize;
|
|
18
|
+
}
|
|
19
|
+
// Convert to JSON string to get actual response size
|
|
20
|
+
const jsonString = JSON.stringify(items);
|
|
21
|
+
const estimatedTokens = estimateTokens(jsonString);
|
|
22
|
+
const averageSize = items.length > 0 ? Math.round(totalSize / items.length) : 0;
|
|
23
|
+
return { totalSize, estimatedTokens, averageSize };
|
|
24
|
+
}
|
|
25
|
+
(0, globals_1.describe)('Issue #24 Fix Validation', () => {
|
|
26
|
+
(0, globals_1.it)('should correctly calculate tokens for response with metadata', () => {
|
|
27
|
+
// Create sample items as they would come from the database
|
|
28
|
+
const dbItems = [];
|
|
29
|
+
for (let i = 0; i < 100; i++) {
|
|
30
|
+
dbItems.push({
|
|
31
|
+
id: `id-${i}`,
|
|
32
|
+
session_id: 'test-session',
|
|
33
|
+
key: `test_item_${i}`,
|
|
34
|
+
value: `This is test content that is moderately long to simulate real data. `.repeat(3),
|
|
35
|
+
category: 'task',
|
|
36
|
+
priority: 'high',
|
|
37
|
+
channel: 'test',
|
|
38
|
+
metadata: JSON.stringify({ index: i }),
|
|
39
|
+
size: 200,
|
|
40
|
+
created_at: '2025-01-20T10:00:00Z',
|
|
41
|
+
updated_at: '2025-01-20T10:00:00Z',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
// Test without metadata (original calculation)
|
|
45
|
+
const metricsWithoutMetadata = calculateResponseMetrics(dbItems);
|
|
46
|
+
console.log('Without metadata - Items:', dbItems.length, 'Tokens:', metricsWithoutMetadata.estimatedTokens);
|
|
47
|
+
// Test with metadata (as per the fix)
|
|
48
|
+
const itemsWithMetadata = dbItems.map(item => ({
|
|
49
|
+
key: item.key,
|
|
50
|
+
value: item.value,
|
|
51
|
+
category: item.category,
|
|
52
|
+
priority: item.priority,
|
|
53
|
+
channel: item.channel,
|
|
54
|
+
metadata: item.metadata ? JSON.parse(item.metadata) : null,
|
|
55
|
+
size: item.size || calculateSize(item.value),
|
|
56
|
+
created_at: item.created_at,
|
|
57
|
+
updated_at: item.updated_at,
|
|
58
|
+
}));
|
|
59
|
+
const metricsWithMetadata = calculateResponseMetrics(itemsWithMetadata);
|
|
60
|
+
console.log('With metadata - Items:', itemsWithMetadata.length, 'Tokens:', metricsWithMetadata.estimatedTokens);
|
|
61
|
+
// The metadata version might have fewer tokens due to JSON parsing
|
|
62
|
+
// but the overall response structure adds overhead
|
|
63
|
+
// Build full response structure as the handler does
|
|
64
|
+
const fullResponse = {
|
|
65
|
+
items: itemsWithMetadata,
|
|
66
|
+
pagination: {
|
|
67
|
+
total: 100,
|
|
68
|
+
returned: 100,
|
|
69
|
+
offset: 0,
|
|
70
|
+
hasMore: false,
|
|
71
|
+
nextOffset: null,
|
|
72
|
+
totalCount: 100,
|
|
73
|
+
page: 1,
|
|
74
|
+
pageSize: 100,
|
|
75
|
+
totalPages: 1,
|
|
76
|
+
hasNextPage: false,
|
|
77
|
+
hasPreviousPage: false,
|
|
78
|
+
previousOffset: null,
|
|
79
|
+
totalSize: metricsWithMetadata.totalSize,
|
|
80
|
+
averageSize: metricsWithMetadata.averageSize,
|
|
81
|
+
defaultsApplied: { limit: true, sort: true },
|
|
82
|
+
truncated: false,
|
|
83
|
+
truncatedCount: 0,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
const finalResponseJson = JSON.stringify(fullResponse, null, 2);
|
|
87
|
+
const finalTokens = estimateTokens(finalResponseJson);
|
|
88
|
+
console.log('Final response - Size:', finalResponseJson.length, 'Tokens:', finalTokens);
|
|
89
|
+
// This demonstrates the issue: the final response can be much larger
|
|
90
|
+
// than what calculateResponseMetrics estimates
|
|
91
|
+
console.log('Token difference:', finalTokens - metricsWithMetadata.estimatedTokens);
|
|
92
|
+
// With 100 items and metadata, we should be approaching or exceeding limits
|
|
93
|
+
if (finalTokens > 18000) {
|
|
94
|
+
console.log('WARNING: Response exceeds safe token limit!');
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
(0, globals_1.it)('should demonstrate that 50 items with metadata stays under limit', () => {
|
|
98
|
+
// Create sample items
|
|
99
|
+
const dbItems = [];
|
|
100
|
+
for (let i = 0; i < 50; i++) {
|
|
101
|
+
dbItems.push({
|
|
102
|
+
id: `id-${i}`,
|
|
103
|
+
session_id: 'test-session',
|
|
104
|
+
key: `test_item_${i}`,
|
|
105
|
+
value: `This is test content that is moderately long to simulate real data. `.repeat(3),
|
|
106
|
+
category: 'task',
|
|
107
|
+
priority: 'high',
|
|
108
|
+
channel: 'test',
|
|
109
|
+
metadata: JSON.stringify({ index: i }),
|
|
110
|
+
size: 200,
|
|
111
|
+
created_at: '2025-01-20T10:00:00Z',
|
|
112
|
+
updated_at: '2025-01-20T10:00:00Z',
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
// Transform with metadata
|
|
116
|
+
const itemsWithMetadata = dbItems.map(item => ({
|
|
117
|
+
key: item.key,
|
|
118
|
+
value: item.value,
|
|
119
|
+
category: item.category,
|
|
120
|
+
priority: item.priority,
|
|
121
|
+
channel: item.channel,
|
|
122
|
+
metadata: item.metadata ? JSON.parse(item.metadata) : null,
|
|
123
|
+
size: item.size || calculateSize(item.value),
|
|
124
|
+
created_at: item.created_at,
|
|
125
|
+
updated_at: item.updated_at,
|
|
126
|
+
}));
|
|
127
|
+
const metrics = calculateResponseMetrics(itemsWithMetadata);
|
|
128
|
+
// Build full response
|
|
129
|
+
const fullResponse = {
|
|
130
|
+
items: itemsWithMetadata,
|
|
131
|
+
pagination: {
|
|
132
|
+
total: 50,
|
|
133
|
+
returned: 50,
|
|
134
|
+
offset: 0,
|
|
135
|
+
hasMore: false,
|
|
136
|
+
nextOffset: null,
|
|
137
|
+
totalCount: 50,
|
|
138
|
+
page: 1,
|
|
139
|
+
pageSize: 50,
|
|
140
|
+
totalPages: 1,
|
|
141
|
+
hasNextPage: false,
|
|
142
|
+
hasPreviousPage: false,
|
|
143
|
+
previousOffset: null,
|
|
144
|
+
totalSize: metrics.totalSize,
|
|
145
|
+
averageSize: metrics.averageSize,
|
|
146
|
+
defaultsApplied: { limit: true, sort: true },
|
|
147
|
+
truncated: false,
|
|
148
|
+
truncatedCount: 0,
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
const finalResponseJson = JSON.stringify(fullResponse, null, 2);
|
|
152
|
+
const finalTokens = estimateTokens(finalResponseJson);
|
|
153
|
+
console.log('50 items with metadata - Tokens:', finalTokens);
|
|
154
|
+
// 50 items should be safe
|
|
155
|
+
(0, globals_1.expect)(finalTokens).toBeLessThan(18000);
|
|
156
|
+
(0, globals_1.expect)(finalTokens).toBeLessThan(25000); // Well under MCP limit
|
|
157
|
+
});
|
|
158
|
+
});
|