@asd412id/mcp-context-manager 1.0.3 → 1.0.5
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/dist/index.js +2 -0
- package/dist/storage/file-store.d.ts +7 -1
- package/dist/storage/file-store.js +80 -22
- package/dist/tools/checkpoint.js +28 -3
- package/dist/tools/loader.js +45 -14
- package/dist/tools/memory.d.ts +1 -0
- package/dist/tools/memory.js +77 -7
- package/dist/tools/session.d.ts +2 -0
- package/dist/tools/session.js +213 -0
- package/dist/tools/summarizer.js +17 -2
- package/dist/tools/tracker.js +55 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { registerSummarizerTools } from './tools/summarizer.js';
|
|
|
8
8
|
import { registerTrackerTools } from './tools/tracker.js';
|
|
9
9
|
import { registerCheckpointTools } from './tools/checkpoint.js';
|
|
10
10
|
import { registerLoaderTools } from './tools/loader.js';
|
|
11
|
+
import { registerSessionTools } from './tools/session.js';
|
|
11
12
|
import { registerPrompts } from './prompts.js';
|
|
12
13
|
const SERVER_NAME = 'mcp-context-manager';
|
|
13
14
|
const SERVER_VERSION = '1.0.0';
|
|
@@ -23,6 +24,7 @@ async function main() {
|
|
|
23
24
|
registerTrackerTools(server);
|
|
24
25
|
registerCheckpointTools(server);
|
|
25
26
|
registerLoaderTools(server);
|
|
27
|
+
registerSessionTools(server);
|
|
26
28
|
registerPrompts(server);
|
|
27
29
|
// Log before connecting (MCP uses stdio after connect)
|
|
28
30
|
console.error(`${SERVER_NAME} v${SERVER_VERSION} starting...`);
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
export interface StoreOptions {
|
|
2
2
|
basePath: string;
|
|
3
|
+
enableBackup?: boolean;
|
|
4
|
+
maxBackups?: number;
|
|
3
5
|
}
|
|
4
6
|
export declare class FileStore {
|
|
5
7
|
private basePath;
|
|
8
|
+
private enableBackup;
|
|
9
|
+
private maxBackups;
|
|
6
10
|
constructor(options: StoreOptions);
|
|
11
|
+
private ensureDirSync;
|
|
7
12
|
private ensureDir;
|
|
8
13
|
private getFilePath;
|
|
14
|
+
private createBackup;
|
|
9
15
|
read<T>(filename: string, defaultValue: T): Promise<T>;
|
|
10
16
|
write<T>(filename: string, data: T): Promise<void>;
|
|
11
17
|
append<T>(filename: string, item: T): Promise<void>;
|
|
@@ -15,4 +21,4 @@ export declare class FileStore {
|
|
|
15
21
|
getSubStore(subdir: string): FileStore;
|
|
16
22
|
}
|
|
17
23
|
export declare function getStore(basePath?: string): FileStore;
|
|
18
|
-
export declare function initStore(basePath: string): FileStore;
|
|
24
|
+
export declare function initStore(basePath: string, options?: Partial<StoreOptions>): FileStore;
|
|
@@ -1,37 +1,82 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
|
+
import * as fsp from 'fs/promises';
|
|
2
3
|
import * as path from 'path';
|
|
3
4
|
export class FileStore {
|
|
4
5
|
basePath;
|
|
6
|
+
enableBackup;
|
|
7
|
+
maxBackups;
|
|
5
8
|
constructor(options) {
|
|
6
9
|
this.basePath = options.basePath;
|
|
7
|
-
this.
|
|
10
|
+
this.enableBackup = options.enableBackup ?? true;
|
|
11
|
+
this.maxBackups = options.maxBackups ?? 3;
|
|
12
|
+
this.ensureDirSync(this.basePath);
|
|
8
13
|
}
|
|
9
|
-
|
|
14
|
+
ensureDirSync(dirPath) {
|
|
10
15
|
if (!fs.existsSync(dirPath)) {
|
|
11
16
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
12
17
|
}
|
|
13
18
|
}
|
|
19
|
+
async ensureDir(dirPath) {
|
|
20
|
+
try {
|
|
21
|
+
await fsp.access(dirPath);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
await fsp.mkdir(dirPath, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
14
27
|
getFilePath(filename) {
|
|
15
28
|
return path.join(this.basePath, filename);
|
|
16
29
|
}
|
|
30
|
+
async createBackup(filePath) {
|
|
31
|
+
if (!this.enableBackup)
|
|
32
|
+
return;
|
|
33
|
+
try {
|
|
34
|
+
await fsp.access(filePath);
|
|
35
|
+
const timestamp = Date.now();
|
|
36
|
+
const backupPath = `${filePath}.${timestamp}.bak`;
|
|
37
|
+
await fsp.copyFile(filePath, backupPath);
|
|
38
|
+
// Cleanup old backups
|
|
39
|
+
const dir = path.dirname(filePath);
|
|
40
|
+
const basename = path.basename(filePath);
|
|
41
|
+
const files = await fsp.readdir(dir);
|
|
42
|
+
const backups = files
|
|
43
|
+
.filter(f => f.startsWith(basename) && f.endsWith('.bak'))
|
|
44
|
+
.sort()
|
|
45
|
+
.reverse();
|
|
46
|
+
// Remove excess backups
|
|
47
|
+
for (let i = this.maxBackups; i < backups.length; i++) {
|
|
48
|
+
await fsp.unlink(path.join(dir, backups[i])).catch(() => { });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// File doesn't exist, no backup needed
|
|
53
|
+
}
|
|
54
|
+
}
|
|
17
55
|
async read(filename, defaultValue) {
|
|
18
56
|
const filePath = this.getFilePath(filename);
|
|
19
57
|
try {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return JSON.parse(content);
|
|
23
|
-
}
|
|
58
|
+
const content = await fsp.readFile(filePath, 'utf-8');
|
|
59
|
+
return JSON.parse(content);
|
|
24
60
|
}
|
|
25
61
|
catch (error) {
|
|
26
|
-
|
|
62
|
+
const err = error;
|
|
63
|
+
if (err.code !== 'ENOENT') {
|
|
64
|
+
console.error(`Error reading ${filename}:`, error);
|
|
65
|
+
}
|
|
66
|
+
return defaultValue;
|
|
27
67
|
}
|
|
28
|
-
return defaultValue;
|
|
29
68
|
}
|
|
30
69
|
async write(filename, data) {
|
|
31
70
|
const filePath = this.getFilePath(filename);
|
|
32
71
|
const dir = path.dirname(filePath);
|
|
33
|
-
this.ensureDir(dir);
|
|
34
|
-
|
|
72
|
+
await this.ensureDir(dir);
|
|
73
|
+
// Create backup before write
|
|
74
|
+
await this.createBackup(filePath);
|
|
75
|
+
// Write to temp file first, then rename (atomic write)
|
|
76
|
+
const tempPath = `${filePath}.tmp`;
|
|
77
|
+
const content = JSON.stringify(data, null, 2);
|
|
78
|
+
await fsp.writeFile(tempPath, content, 'utf-8');
|
|
79
|
+
await fsp.rename(tempPath, filePath);
|
|
35
80
|
}
|
|
36
81
|
async append(filename, item) {
|
|
37
82
|
const existing = await this.read(filename, []);
|
|
@@ -41,24 +86,32 @@ export class FileStore {
|
|
|
41
86
|
async delete(filename) {
|
|
42
87
|
const filePath = this.getFilePath(filename);
|
|
43
88
|
try {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
89
|
+
await fsp.unlink(filePath);
|
|
90
|
+
return true;
|
|
48
91
|
}
|
|
49
92
|
catch (error) {
|
|
50
|
-
|
|
93
|
+
const err = error;
|
|
94
|
+
if (err.code !== 'ENOENT') {
|
|
95
|
+
console.error(`Error deleting ${filename}:`, error);
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
51
98
|
}
|
|
52
|
-
return false;
|
|
53
99
|
}
|
|
54
100
|
async exists(filename) {
|
|
55
|
-
|
|
101
|
+
try {
|
|
102
|
+
await fsp.access(this.getFilePath(filename));
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
56
108
|
}
|
|
57
109
|
async list(subdir) {
|
|
58
110
|
const dirPath = subdir ? path.join(this.basePath, subdir) : this.basePath;
|
|
59
|
-
this.ensureDir(dirPath);
|
|
111
|
+
await this.ensureDir(dirPath);
|
|
60
112
|
try {
|
|
61
|
-
|
|
113
|
+
const files = await fsp.readdir(dirPath);
|
|
114
|
+
return files.filter(f => f.endsWith('.json'));
|
|
62
115
|
}
|
|
63
116
|
catch {
|
|
64
117
|
return [];
|
|
@@ -66,7 +119,9 @@ export class FileStore {
|
|
|
66
119
|
}
|
|
67
120
|
getSubStore(subdir) {
|
|
68
121
|
return new FileStore({
|
|
69
|
-
basePath: path.join(this.basePath, subdir)
|
|
122
|
+
basePath: path.join(this.basePath, subdir),
|
|
123
|
+
enableBackup: this.enableBackup,
|
|
124
|
+
maxBackups: this.maxBackups
|
|
70
125
|
});
|
|
71
126
|
}
|
|
72
127
|
}
|
|
@@ -78,7 +133,10 @@ export function getStore(basePath) {
|
|
|
78
133
|
}
|
|
79
134
|
return storeInstance;
|
|
80
135
|
}
|
|
81
|
-
export function initStore(basePath) {
|
|
82
|
-
storeInstance = new FileStore({
|
|
136
|
+
export function initStore(basePath, options) {
|
|
137
|
+
storeInstance = new FileStore({
|
|
138
|
+
basePath,
|
|
139
|
+
...options
|
|
140
|
+
});
|
|
83
141
|
return storeInstance;
|
|
84
142
|
}
|
package/dist/tools/checkpoint.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import * as z from 'zod';
|
|
2
2
|
import { getStore } from '../storage/file-store.js';
|
|
3
|
+
const STORAGE_VERSION = 1;
|
|
4
|
+
const MAX_CHECKPOINTS = 50;
|
|
3
5
|
const CHECKPOINTS_DIR = 'checkpoints';
|
|
4
6
|
async function getCheckpointStore() {
|
|
5
7
|
const store = getStore().getSubStore(CHECKPOINTS_DIR);
|
|
6
|
-
|
|
8
|
+
const data = await store.read('index.json', { version: STORAGE_VERSION, checkpoints: [] });
|
|
9
|
+
if (!data.version)
|
|
10
|
+
data.version = STORAGE_VERSION;
|
|
11
|
+
return data;
|
|
7
12
|
}
|
|
8
13
|
async function saveCheckpointStore(data) {
|
|
9
14
|
const store = getStore().getSubStore(CHECKPOINTS_DIR);
|
|
15
|
+
data.version = STORAGE_VERSION;
|
|
10
16
|
await store.write('index.json', data);
|
|
11
17
|
}
|
|
12
18
|
async function saveCheckpointData(id, data) {
|
|
@@ -23,7 +29,13 @@ async function loadCheckpointData(id) {
|
|
|
23
29
|
export function registerCheckpointTools(server) {
|
|
24
30
|
server.registerTool('checkpoint_save', {
|
|
25
31
|
title: 'Save Checkpoint',
|
|
26
|
-
description:
|
|
32
|
+
description: `Save current session state as a checkpoint.
|
|
33
|
+
WHEN TO USE:
|
|
34
|
+
- Every 10-15 messages in long conversations
|
|
35
|
+
- Before major refactoring or risky changes
|
|
36
|
+
- At important milestones (feature complete, bug fixed)
|
|
37
|
+
- Before context gets too long (>60% used)
|
|
38
|
+
- When ending a work session`,
|
|
27
39
|
inputSchema: {
|
|
28
40
|
name: z.string().describe('Checkpoint name (e.g., "before-refactor", "feature-complete")'),
|
|
29
41
|
description: z.string().optional().describe('Description of what was accomplished'),
|
|
@@ -45,6 +57,15 @@ export function registerCheckpointTools(server) {
|
|
|
45
57
|
...checkpoint,
|
|
46
58
|
state: {}
|
|
47
59
|
});
|
|
60
|
+
// Auto-cleanup old checkpoints
|
|
61
|
+
if (checkpointStore.checkpoints.length > MAX_CHECKPOINTS) {
|
|
62
|
+
const store = getStore().getSubStore(CHECKPOINTS_DIR);
|
|
63
|
+
const toRemove = checkpointStore.checkpoints.splice(0, checkpointStore.checkpoints.length - MAX_CHECKPOINTS);
|
|
64
|
+
// Delete old checkpoint data files
|
|
65
|
+
for (const cp of toRemove) {
|
|
66
|
+
await store.delete(`${cp.id}.json`).catch(() => { });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
48
69
|
await saveCheckpointStore(checkpointStore);
|
|
49
70
|
return {
|
|
50
71
|
content: [{
|
|
@@ -55,7 +76,11 @@ export function registerCheckpointTools(server) {
|
|
|
55
76
|
});
|
|
56
77
|
server.registerTool('checkpoint_load', {
|
|
57
78
|
title: 'Load Checkpoint',
|
|
58
|
-
description:
|
|
79
|
+
description: `Load a previously saved checkpoint to restore context.
|
|
80
|
+
WHEN TO USE:
|
|
81
|
+
- At session start (or use session_init instead)
|
|
82
|
+
- To restore to a specific point in time
|
|
83
|
+
- After context reset to recover previous work`,
|
|
59
84
|
inputSchema: {
|
|
60
85
|
id: z.string().optional().describe('Checkpoint ID (loads latest if not specified)'),
|
|
61
86
|
name: z.string().optional().describe('Checkpoint name to search for')
|
package/dist/tools/loader.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import * as z from 'zod';
|
|
2
2
|
import * as fs from 'fs';
|
|
3
|
+
import * as fsp from 'fs/promises';
|
|
3
4
|
import * as path from 'path';
|
|
5
|
+
// Safe glob pattern to regex - escapes special chars except *
|
|
6
|
+
function safeGlobToRegex(pattern) {
|
|
7
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
8
|
+
const regexPattern = '^' + escaped.replace(/\*/g, '.*') + '$';
|
|
9
|
+
return new RegExp(regexPattern, 'i');
|
|
10
|
+
}
|
|
4
11
|
function readFileLines(filePath, startLine, endLine) {
|
|
5
12
|
try {
|
|
6
13
|
if (!fs.existsSync(filePath))
|
|
@@ -94,7 +101,12 @@ function extractCodeStructure(content, extension) {
|
|
|
94
101
|
export function registerLoaderTools(server) {
|
|
95
102
|
server.registerTool('file_smart_read', {
|
|
96
103
|
title: 'Smart File Read',
|
|
97
|
-
description:
|
|
104
|
+
description: `Read a file with smart options: specific lines, keyword search, or structure extraction.
|
|
105
|
+
WHEN TO USE:
|
|
106
|
+
- For large files (>200 lines): use structureOnly:true first to see outline
|
|
107
|
+
- To find specific code: use keywords:["functionName", "className"]
|
|
108
|
+
- For partial reads: use startLine/endLine
|
|
109
|
+
- Saves context vs reading entire file`,
|
|
98
110
|
inputSchema: {
|
|
99
111
|
path: z.string().describe('File path to read'),
|
|
100
112
|
startLine: z.number().optional().describe('Start line (1-indexed)'),
|
|
@@ -164,7 +176,11 @@ export function registerLoaderTools(server) {
|
|
|
164
176
|
});
|
|
165
177
|
server.registerTool('file_info', {
|
|
166
178
|
title: 'File Info',
|
|
167
|
-
description:
|
|
179
|
+
description: `Get file metadata without reading content.
|
|
180
|
+
WHEN TO USE:
|
|
181
|
+
- Before reading to check if file exists
|
|
182
|
+
- To check file size before deciding read strategy
|
|
183
|
+
- To see modification time`,
|
|
168
184
|
inputSchema: {
|
|
169
185
|
paths: z.array(z.string()).describe('File paths to check')
|
|
170
186
|
}
|
|
@@ -183,14 +199,26 @@ export function registerLoaderTools(server) {
|
|
|
183
199
|
contextLines: z.number().optional().describe('Number of context lines before/after match (default: 2)')
|
|
184
200
|
}
|
|
185
201
|
}, async ({ path: filePath, pattern, contextLines = 2 }) => {
|
|
186
|
-
|
|
202
|
+
try {
|
|
203
|
+
await fsp.access(filePath);
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
187
206
|
return {
|
|
188
207
|
content: [{ type: 'text', text: `File not found: ${filePath}` }]
|
|
189
208
|
};
|
|
190
209
|
}
|
|
191
|
-
const content =
|
|
210
|
+
const content = await fsp.readFile(filePath, 'utf-8');
|
|
192
211
|
const lines = content.split('\n');
|
|
193
|
-
|
|
212
|
+
// Validate regex pattern
|
|
213
|
+
let regex;
|
|
214
|
+
try {
|
|
215
|
+
regex = new RegExp(pattern, 'gi');
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return {
|
|
219
|
+
content: [{ type: 'text', text: `Invalid regex pattern: "${pattern}"` }]
|
|
220
|
+
};
|
|
221
|
+
}
|
|
194
222
|
const matches = [];
|
|
195
223
|
for (let i = 0; i < lines.length; i++) {
|
|
196
224
|
if (regex.test(lines[i])) {
|
|
@@ -223,24 +251,27 @@ export function registerLoaderTools(server) {
|
|
|
223
251
|
recursive: z.boolean().optional().describe('Include subdirectories (default: false)')
|
|
224
252
|
}
|
|
225
253
|
}, async ({ path: dirPath, pattern, recursive = false }) => {
|
|
226
|
-
|
|
254
|
+
try {
|
|
255
|
+
await fsp.access(dirPath);
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
227
258
|
return {
|
|
228
259
|
content: [{ type: 'text', text: `Directory not found: ${dirPath}` }]
|
|
229
260
|
};
|
|
230
261
|
}
|
|
231
262
|
const results = [];
|
|
232
|
-
|
|
233
|
-
|
|
263
|
+
const patternRegex = pattern ? safeGlobToRegex(pattern) : null;
|
|
264
|
+
async function walkDir(dir) {
|
|
265
|
+
const items = await fsp.readdir(dir);
|
|
234
266
|
for (const item of items) {
|
|
235
267
|
const fullPath = path.join(dir, item);
|
|
236
|
-
const stat =
|
|
268
|
+
const stat = await fsp.stat(fullPath);
|
|
237
269
|
if (stat.isDirectory() && recursive) {
|
|
238
|
-
walkDir(fullPath);
|
|
270
|
+
await walkDir(fullPath);
|
|
239
271
|
}
|
|
240
272
|
else if (stat.isFile()) {
|
|
241
|
-
if (
|
|
242
|
-
|
|
243
|
-
if (regex.test(item)) {
|
|
273
|
+
if (patternRegex) {
|
|
274
|
+
if (patternRegex.test(item)) {
|
|
244
275
|
results.push(fullPath);
|
|
245
276
|
}
|
|
246
277
|
}
|
|
@@ -250,7 +281,7 @@ export function registerLoaderTools(server) {
|
|
|
250
281
|
}
|
|
251
282
|
}
|
|
252
283
|
}
|
|
253
|
-
walkDir(dirPath);
|
|
284
|
+
await walkDir(dirPath);
|
|
254
285
|
return {
|
|
255
286
|
content: [{
|
|
256
287
|
type: 'text',
|
package/dist/tools/memory.d.ts
CHANGED
package/dist/tools/memory.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import * as z from 'zod';
|
|
2
2
|
import { getStore } from '../storage/file-store.js';
|
|
3
|
+
const STORAGE_VERSION = 1;
|
|
3
4
|
const MEMORY_FILE = 'memory.json';
|
|
4
5
|
async function getMemoryStore() {
|
|
5
6
|
const store = getStore();
|
|
6
|
-
|
|
7
|
+
const data = await store.read(MEMORY_FILE, { version: STORAGE_VERSION, entries: {} });
|
|
8
|
+
// Ensure version field exists for old stores
|
|
9
|
+
if (!data.version)
|
|
10
|
+
data.version = STORAGE_VERSION;
|
|
11
|
+
return data;
|
|
7
12
|
}
|
|
8
13
|
async function saveMemoryStore(data) {
|
|
9
14
|
const store = getStore();
|
|
15
|
+
data.version = STORAGE_VERSION;
|
|
10
16
|
await store.write(MEMORY_FILE, data);
|
|
11
17
|
}
|
|
12
18
|
function isExpired(entry) {
|
|
@@ -15,10 +21,39 @@ function isExpired(entry) {
|
|
|
15
21
|
const expiresAt = new Date(entry.createdAt).getTime() + entry.ttl;
|
|
16
22
|
return Date.now() > expiresAt;
|
|
17
23
|
}
|
|
24
|
+
// Safe pattern conversion - escape special regex chars except *
|
|
25
|
+
function safePatternToRegex(pattern) {
|
|
26
|
+
// Escape all special regex characters except *
|
|
27
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
28
|
+
// Convert * to non-greedy .*
|
|
29
|
+
const regexPattern = '^' + escaped.replace(/\*/g, '.*?') + '$';
|
|
30
|
+
return new RegExp(regexPattern, 'i');
|
|
31
|
+
}
|
|
32
|
+
// Cleanup expired entries and return count
|
|
33
|
+
export async function cleanupExpiredMemories() {
|
|
34
|
+
const memStore = await getMemoryStore();
|
|
35
|
+
const keys = Object.keys(memStore.entries);
|
|
36
|
+
let removed = 0;
|
|
37
|
+
for (const key of keys) {
|
|
38
|
+
if (isExpired(memStore.entries[key])) {
|
|
39
|
+
delete memStore.entries[key];
|
|
40
|
+
removed++;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (removed > 0) {
|
|
44
|
+
await saveMemoryStore(memStore);
|
|
45
|
+
}
|
|
46
|
+
return removed;
|
|
47
|
+
}
|
|
18
48
|
export function registerMemoryTools(server) {
|
|
19
49
|
server.registerTool('memory_set', {
|
|
20
50
|
title: 'Memory Set',
|
|
21
|
-
description:
|
|
51
|
+
description: `Store a key-value pair in persistent memory.
|
|
52
|
+
WHEN TO USE:
|
|
53
|
+
- After discovering important info (API endpoints, configs, credentials refs)
|
|
54
|
+
- When user says "remember this" or "save this"
|
|
55
|
+
- To store frequently referenced data
|
|
56
|
+
- Before ending a session to preserve key context`,
|
|
22
57
|
inputSchema: {
|
|
23
58
|
key: z.string().describe('Unique identifier for this memory'),
|
|
24
59
|
value: z.unknown().describe('Data to store (any JSON-serializable value)'),
|
|
@@ -47,7 +82,11 @@ export function registerMemoryTools(server) {
|
|
|
47
82
|
});
|
|
48
83
|
server.registerTool('memory_get', {
|
|
49
84
|
title: 'Memory Get',
|
|
50
|
-
description:
|
|
85
|
+
description: `Retrieve a value from persistent memory by key.
|
|
86
|
+
WHEN TO USE:
|
|
87
|
+
- Before starting work to recall saved context
|
|
88
|
+
- When you need specific info you saved earlier
|
|
89
|
+
- After session_init if you need detailed value (not just key list)`,
|
|
51
90
|
inputSchema: {
|
|
52
91
|
key: z.string().describe('Key to retrieve')
|
|
53
92
|
}
|
|
@@ -81,7 +120,11 @@ export function registerMemoryTools(server) {
|
|
|
81
120
|
});
|
|
82
121
|
server.registerTool('memory_search', {
|
|
83
122
|
title: 'Memory Search',
|
|
84
|
-
description:
|
|
123
|
+
description: `Search memories by key pattern or tags.
|
|
124
|
+
WHEN TO USE:
|
|
125
|
+
- When you need to find memories but don't know exact key
|
|
126
|
+
- To find all memories related to a topic (via tags)
|
|
127
|
+
- Pattern examples: "api.*" matches "api.users", "api.posts"`,
|
|
85
128
|
inputSchema: {
|
|
86
129
|
pattern: z.string().optional().describe('Key pattern to search (supports * wildcard)'),
|
|
87
130
|
tags: z.array(z.string()).optional().describe('Filter by tags (any match)')
|
|
@@ -89,10 +132,18 @@ export function registerMemoryTools(server) {
|
|
|
89
132
|
}, async ({ pattern, tags }) => {
|
|
90
133
|
const memStore = await getMemoryStore();
|
|
91
134
|
let results = Object.values(memStore.entries);
|
|
135
|
+
// Filter out expired entries
|
|
92
136
|
results = results.filter(entry => !isExpired(entry));
|
|
93
137
|
if (pattern) {
|
|
94
|
-
|
|
95
|
-
|
|
138
|
+
try {
|
|
139
|
+
const regex = safePatternToRegex(pattern);
|
|
140
|
+
results = results.filter(entry => regex.test(entry.key));
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return {
|
|
144
|
+
content: [{ type: 'text', text: `Invalid search pattern: "${pattern}"` }]
|
|
145
|
+
};
|
|
146
|
+
}
|
|
96
147
|
}
|
|
97
148
|
if (tags && tags.length > 0) {
|
|
98
149
|
results = results.filter(entry => tags.some(tag => entry.tags.includes(tag)));
|
|
@@ -133,7 +184,11 @@ export function registerMemoryTools(server) {
|
|
|
133
184
|
});
|
|
134
185
|
server.registerTool('memory_list', {
|
|
135
186
|
title: 'Memory List',
|
|
136
|
-
description:
|
|
187
|
+
description: `List all memory keys with their tags.
|
|
188
|
+
WHEN TO USE:
|
|
189
|
+
- At session start (or use session_init instead)
|
|
190
|
+
- To see what's been saved
|
|
191
|
+
- Before deciding what to store (avoid duplicates)`,
|
|
137
192
|
inputSchema: {}
|
|
138
193
|
}, async () => {
|
|
139
194
|
const memStore = await getMemoryStore();
|
|
@@ -179,4 +234,19 @@ export function registerMemoryTools(server) {
|
|
|
179
234
|
content: [{ type: 'text', text: `Cleared ${count} memories` }]
|
|
180
235
|
};
|
|
181
236
|
});
|
|
237
|
+
server.registerTool('memory_cleanup', {
|
|
238
|
+
title: 'Memory Cleanup',
|
|
239
|
+
description: 'Remove all expired memory entries. Call periodically to free up storage.',
|
|
240
|
+
inputSchema: {}
|
|
241
|
+
}, async () => {
|
|
242
|
+
const removed = await cleanupExpiredMemories();
|
|
243
|
+
return {
|
|
244
|
+
content: [{
|
|
245
|
+
type: 'text',
|
|
246
|
+
text: removed > 0
|
|
247
|
+
? `Cleaned up ${removed} expired memories`
|
|
248
|
+
: 'No expired memories to clean up'
|
|
249
|
+
}]
|
|
250
|
+
};
|
|
251
|
+
});
|
|
182
252
|
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import * as z from 'zod';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { getStore } from '../storage/file-store.js';
|
|
5
|
+
import { cleanupExpiredMemories } from './memory.js';
|
|
6
|
+
function detectProject(cwd) {
|
|
7
|
+
// Try package.json first
|
|
8
|
+
const packageJsonPath = path.join(cwd, 'package.json');
|
|
9
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
10
|
+
try {
|
|
11
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
12
|
+
return {
|
|
13
|
+
name: pkg.name || path.basename(cwd),
|
|
14
|
+
type: pkg.type === 'module' ? 'esm' : 'commonjs',
|
|
15
|
+
path: cwd,
|
|
16
|
+
detectedFrom: 'package.json'
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// ignore parse errors
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// Try .git
|
|
24
|
+
const gitPath = path.join(cwd, '.git');
|
|
25
|
+
if (fs.existsSync(gitPath)) {
|
|
26
|
+
// Try to get repo name from git config
|
|
27
|
+
const gitConfigPath = path.join(gitPath, 'config');
|
|
28
|
+
if (fs.existsSync(gitConfigPath)) {
|
|
29
|
+
try {
|
|
30
|
+
const config = fs.readFileSync(gitConfigPath, 'utf-8');
|
|
31
|
+
const urlMatch = config.match(/url\s*=\s*.*\/([^\/\s]+?)(?:\.git)?$/m);
|
|
32
|
+
if (urlMatch) {
|
|
33
|
+
return {
|
|
34
|
+
name: urlMatch[1],
|
|
35
|
+
type: 'git',
|
|
36
|
+
path: cwd,
|
|
37
|
+
detectedFrom: 'git'
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// ignore
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
name: path.basename(cwd),
|
|
47
|
+
type: 'git',
|
|
48
|
+
path: cwd,
|
|
49
|
+
detectedFrom: 'git-folder'
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// Try pyproject.toml
|
|
53
|
+
const pyprojectPath = path.join(cwd, 'pyproject.toml');
|
|
54
|
+
if (fs.existsSync(pyprojectPath)) {
|
|
55
|
+
try {
|
|
56
|
+
const content = fs.readFileSync(pyprojectPath, 'utf-8');
|
|
57
|
+
const nameMatch = content.match(/name\s*=\s*"([^"]+)"/);
|
|
58
|
+
return {
|
|
59
|
+
name: nameMatch ? nameMatch[1] : path.basename(cwd),
|
|
60
|
+
type: 'python',
|
|
61
|
+
path: cwd,
|
|
62
|
+
detectedFrom: 'pyproject.toml'
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// ignore
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Try Cargo.toml (Rust)
|
|
70
|
+
const cargoPath = path.join(cwd, 'Cargo.toml');
|
|
71
|
+
if (fs.existsSync(cargoPath)) {
|
|
72
|
+
try {
|
|
73
|
+
const content = fs.readFileSync(cargoPath, 'utf-8');
|
|
74
|
+
const nameMatch = content.match(/name\s*=\s*"([^"]+)"/);
|
|
75
|
+
return {
|
|
76
|
+
name: nameMatch ? nameMatch[1] : path.basename(cwd),
|
|
77
|
+
type: 'rust',
|
|
78
|
+
path: cwd,
|
|
79
|
+
detectedFrom: 'Cargo.toml'
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// ignore
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Fallback to folder name
|
|
87
|
+
return {
|
|
88
|
+
name: path.basename(cwd),
|
|
89
|
+
type: 'unknown',
|
|
90
|
+
path: cwd,
|
|
91
|
+
detectedFrom: 'folder-name'
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
async function getCheckpointLatest() {
|
|
95
|
+
const store = getStore().getSubStore('checkpoints');
|
|
96
|
+
const index = await store.read('index.json', { checkpoints: [] });
|
|
97
|
+
if (index.checkpoints.length === 0)
|
|
98
|
+
return null;
|
|
99
|
+
const latest = index.checkpoints[index.checkpoints.length - 1];
|
|
100
|
+
const stateData = await store.read(`${latest.id}.json`, {});
|
|
101
|
+
return {
|
|
102
|
+
id: latest.id,
|
|
103
|
+
name: latest.name,
|
|
104
|
+
description: latest.description,
|
|
105
|
+
createdAt: latest.createdAt,
|
|
106
|
+
files: latest.files,
|
|
107
|
+
state: stateData
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
async function getTrackerStatus() {
|
|
111
|
+
const store = getStore();
|
|
112
|
+
const trackerStore = await store.read('tracker.json', { entries: [] });
|
|
113
|
+
const entries = trackerStore.entries;
|
|
114
|
+
const limit = 5;
|
|
115
|
+
return {
|
|
116
|
+
projectName: trackerStore.projectName,
|
|
117
|
+
totalEntries: entries.length,
|
|
118
|
+
decisions: entries.filter(e => e.type === 'decision').slice(-limit).map(d => ({ id: d.id, content: d.content, date: d.createdAt })),
|
|
119
|
+
pendingTodos: entries.filter(e => e.type === 'todo' && e.status === 'pending').slice(-limit).map(t => ({ id: t.id, content: t.content, tags: t.tags })),
|
|
120
|
+
recentChanges: entries.filter(e => e.type === 'change').slice(-limit).map(c => ({ id: c.id, content: c.content, date: c.createdAt })),
|
|
121
|
+
recentErrors: entries.filter(e => e.type === 'error').slice(-limit).map(e => ({ id: e.id, content: e.content, date: e.createdAt }))
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
async function getMemoryList() {
|
|
125
|
+
const store = getStore();
|
|
126
|
+
const memStore = await store.read('memory.json', { entries: {} });
|
|
127
|
+
const entries = Object.values(memStore.entries).filter(e => {
|
|
128
|
+
if (!e.ttl)
|
|
129
|
+
return true;
|
|
130
|
+
const expiresAt = new Date(e.createdAt).getTime() + e.ttl;
|
|
131
|
+
return Date.now() <= expiresAt;
|
|
132
|
+
});
|
|
133
|
+
if (entries.length === 0)
|
|
134
|
+
return [];
|
|
135
|
+
return entries.map(e => ({
|
|
136
|
+
key: e.key,
|
|
137
|
+
tags: e.tags,
|
|
138
|
+
updatedAt: e.updatedAt
|
|
139
|
+
}));
|
|
140
|
+
}
|
|
141
|
+
export function registerSessionTools(server) {
|
|
142
|
+
server.registerTool('session_init', {
|
|
143
|
+
title: 'Session Init',
|
|
144
|
+
description: `Initialize session by loading all previous context in ONE call.
|
|
145
|
+
WHEN TO USE: Call this ONCE at the START of every session/conversation.
|
|
146
|
+
Returns: latest checkpoint, tracker status (todos/decisions), all memories, and auto-detected project info.
|
|
147
|
+
This replaces calling checkpoint_load(), tracker_status(), and memory_list() separately.`,
|
|
148
|
+
inputSchema: {
|
|
149
|
+
cwd: z.string().optional().describe('Current working directory for project detection (defaults to process.cwd())')
|
|
150
|
+
}
|
|
151
|
+
}, async ({ cwd }) => {
|
|
152
|
+
const workingDir = cwd || process.cwd();
|
|
153
|
+
// Cleanup expired memories first
|
|
154
|
+
const cleanedUp = await cleanupExpiredMemories();
|
|
155
|
+
const [checkpoint, tracker, memories] = await Promise.all([
|
|
156
|
+
getCheckpointLatest(),
|
|
157
|
+
getTrackerStatus(),
|
|
158
|
+
getMemoryList()
|
|
159
|
+
]);
|
|
160
|
+
const project = detectProject(workingDir);
|
|
161
|
+
// Auto-set project name if detected and not already set
|
|
162
|
+
if (project && !tracker?.projectName) {
|
|
163
|
+
const store = getStore();
|
|
164
|
+
const trackerStore = await store.read('tracker.json', { entries: [] });
|
|
165
|
+
trackerStore.projectName = project.name;
|
|
166
|
+
await store.write('tracker.json', trackerStore);
|
|
167
|
+
}
|
|
168
|
+
const state = {
|
|
169
|
+
checkpoint,
|
|
170
|
+
tracker,
|
|
171
|
+
memories,
|
|
172
|
+
project
|
|
173
|
+
};
|
|
174
|
+
const summary = {
|
|
175
|
+
initialized: true,
|
|
176
|
+
project: project ? `${project.name} (${project.type})` : 'unknown',
|
|
177
|
+
hasCheckpoint: !!checkpoint,
|
|
178
|
+
pendingTodos: (tracker?.pendingTodos || []).length,
|
|
179
|
+
totalDecisions: (tracker?.decisions || []).length,
|
|
180
|
+
memoriesCount: Array.isArray(memories) ? memories.length : 0,
|
|
181
|
+
cleanedUpExpiredMemories: cleanedUp
|
|
182
|
+
};
|
|
183
|
+
return {
|
|
184
|
+
content: [{
|
|
185
|
+
type: 'text',
|
|
186
|
+
text: JSON.stringify({
|
|
187
|
+
summary,
|
|
188
|
+
...state
|
|
189
|
+
}, null, 2)
|
|
190
|
+
}]
|
|
191
|
+
};
|
|
192
|
+
});
|
|
193
|
+
server.registerTool('project_detect', {
|
|
194
|
+
title: 'Project Detect',
|
|
195
|
+
description: `Auto-detect project information from current directory.
|
|
196
|
+
WHEN TO USE: When you need to know what project you're working on.
|
|
197
|
+
Detects from: package.json, .git, pyproject.toml, Cargo.toml, or folder name.`,
|
|
198
|
+
inputSchema: {
|
|
199
|
+
cwd: z.string().optional().describe('Directory to detect project from')
|
|
200
|
+
}
|
|
201
|
+
}, async ({ cwd }) => {
|
|
202
|
+
const workingDir = cwd || process.cwd();
|
|
203
|
+
const project = detectProject(workingDir);
|
|
204
|
+
return {
|
|
205
|
+
content: [{
|
|
206
|
+
type: 'text',
|
|
207
|
+
text: project
|
|
208
|
+
? JSON.stringify(project, null, 2)
|
|
209
|
+
: 'Could not detect project'
|
|
210
|
+
}]
|
|
211
|
+
};
|
|
212
|
+
});
|
|
213
|
+
}
|
package/dist/tools/summarizer.js
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import * as z from 'zod';
|
|
2
2
|
import { getStore } from '../storage/file-store.js';
|
|
3
|
+
const STORAGE_VERSION = 1;
|
|
4
|
+
const MAX_SUMMARIES = 100;
|
|
3
5
|
const SUMMARIES_DIR = 'summaries';
|
|
4
6
|
async function getSummaryStore() {
|
|
5
7
|
const store = getStore().getSubStore(SUMMARIES_DIR);
|
|
6
|
-
|
|
8
|
+
const data = await store.read('index.json', { version: STORAGE_VERSION, summaries: [] });
|
|
9
|
+
if (!data.version)
|
|
10
|
+
data.version = STORAGE_VERSION;
|
|
11
|
+
return data;
|
|
7
12
|
}
|
|
8
13
|
async function saveSummaryStore(data) {
|
|
9
14
|
const store = getStore().getSubStore(SUMMARIES_DIR);
|
|
15
|
+
data.version = STORAGE_VERSION;
|
|
16
|
+
// Auto-cleanup old summaries
|
|
17
|
+
if (data.summaries.length > MAX_SUMMARIES) {
|
|
18
|
+
data.summaries = data.summaries.slice(-MAX_SUMMARIES);
|
|
19
|
+
}
|
|
10
20
|
await store.write('index.json', data);
|
|
11
21
|
}
|
|
12
22
|
function extractKeyPoints(text) {
|
|
@@ -90,7 +100,12 @@ function compressText(text, maxLength) {
|
|
|
90
100
|
export function registerSummarizerTools(server) {
|
|
91
101
|
server.registerTool('context_summarize', {
|
|
92
102
|
title: 'Context Summarize',
|
|
93
|
-
description:
|
|
103
|
+
description: `Summarize and compress context/conversation. Extracts key points, decisions, and action items.
|
|
104
|
+
WHEN TO USE:
|
|
105
|
+
- When context is getting long (>60% used)
|
|
106
|
+
- Before checkpoint_save to compress conversation
|
|
107
|
+
- To extract key decisions and action items from long text
|
|
108
|
+
- When you need to free up context space`,
|
|
94
109
|
inputSchema: {
|
|
95
110
|
text: z.string().describe('Text to summarize'),
|
|
96
111
|
maxLength: z.number().optional().describe('Maximum length for compressed summary (default: 2000)'),
|
package/dist/tools/tracker.js
CHANGED
|
@@ -1,18 +1,36 @@
|
|
|
1
1
|
import * as z from 'zod';
|
|
2
2
|
import { getStore } from '../storage/file-store.js';
|
|
3
|
+
const STORAGE_VERSION = 1;
|
|
4
|
+
const MAX_ENTRIES = 1000;
|
|
5
|
+
const ROTATE_KEEP = 800;
|
|
3
6
|
const TRACKER_FILE = 'tracker.json';
|
|
4
7
|
async function getTrackerStore() {
|
|
5
8
|
const store = getStore();
|
|
6
|
-
|
|
9
|
+
const data = await store.read(TRACKER_FILE, { version: STORAGE_VERSION, entries: [] });
|
|
10
|
+
if (!data.version)
|
|
11
|
+
data.version = STORAGE_VERSION;
|
|
12
|
+
return data;
|
|
7
13
|
}
|
|
8
14
|
async function saveTrackerStore(data) {
|
|
9
15
|
const store = getStore();
|
|
16
|
+
data.version = STORAGE_VERSION;
|
|
17
|
+
// Auto-rotate when exceeding limit
|
|
18
|
+
if (data.entries.length > MAX_ENTRIES) {
|
|
19
|
+
data.entries = data.entries.slice(-ROTATE_KEEP);
|
|
20
|
+
}
|
|
10
21
|
await store.write(TRACKER_FILE, data);
|
|
11
22
|
}
|
|
12
23
|
export function registerTrackerTools(server) {
|
|
13
24
|
server.registerTool('tracker_log', {
|
|
14
25
|
title: 'Tracker Log',
|
|
15
|
-
description:
|
|
26
|
+
description: `Log a decision, change, todo, note, or error to the project tracker.
|
|
27
|
+
WHEN TO USE:
|
|
28
|
+
- type:"decision" - After making ANY architectural/implementation choice
|
|
29
|
+
- type:"change" - After modifying ANY file
|
|
30
|
+
- type:"todo" - When user mentions task/TODO/fix needed
|
|
31
|
+
- type:"error" - When encountering errors or bugs
|
|
32
|
+
- type:"note" - For general observations
|
|
33
|
+
ALWAYS log these events - this is MANDATORY, not optional.`,
|
|
16
34
|
inputSchema: {
|
|
17
35
|
type: z.enum(['decision', 'change', 'todo', 'note', 'error']).describe('Type of entry'),
|
|
18
36
|
content: z.string().describe('Description of the entry'),
|
|
@@ -43,7 +61,11 @@ export function registerTrackerTools(server) {
|
|
|
43
61
|
});
|
|
44
62
|
server.registerTool('tracker_status', {
|
|
45
63
|
title: 'Tracker Status',
|
|
46
|
-
description:
|
|
64
|
+
description: `Get current project status including recent decisions, pending todos, and recent changes.
|
|
65
|
+
WHEN TO USE:
|
|
66
|
+
- At session start (or use session_init instead)
|
|
67
|
+
- To review what needs to be done (pending todos)
|
|
68
|
+
- To recall recent decisions and changes`,
|
|
47
69
|
inputSchema: {
|
|
48
70
|
limit: z.number().optional().describe('Maximum entries per type (default: 5)')
|
|
49
71
|
}
|
|
@@ -76,7 +98,11 @@ export function registerTrackerTools(server) {
|
|
|
76
98
|
});
|
|
77
99
|
server.registerTool('tracker_todo_update', {
|
|
78
100
|
title: 'Update Todo',
|
|
79
|
-
description:
|
|
101
|
+
description: `Update the status of a todo item.
|
|
102
|
+
WHEN TO USE:
|
|
103
|
+
- After completing a task -> status:"done"
|
|
104
|
+
- When task is no longer needed -> status:"cancelled"
|
|
105
|
+
- To re-open a task -> status:"pending"`,
|
|
80
106
|
inputSchema: {
|
|
81
107
|
id: z.string().describe('Todo ID to update'),
|
|
82
108
|
status: z.enum(['pending', 'done', 'cancelled']).describe('New status')
|
|
@@ -193,4 +219,29 @@ export function registerTrackerTools(server) {
|
|
|
193
219
|
content: [{ type: 'text', text: md }]
|
|
194
220
|
};
|
|
195
221
|
});
|
|
222
|
+
server.registerTool('tracker_cleanup', {
|
|
223
|
+
title: 'Tracker Cleanup',
|
|
224
|
+
description: `Clean up old tracker entries. Keeps the most recent entries.
|
|
225
|
+
WHEN TO USE:
|
|
226
|
+
- When tracker has too many old entries
|
|
227
|
+
- To free up storage space
|
|
228
|
+
- Periodically for maintenance`,
|
|
229
|
+
inputSchema: {
|
|
230
|
+
keepCount: z.number().optional().describe('Number of entries to keep (default: 500)')
|
|
231
|
+
}
|
|
232
|
+
}, async ({ keepCount = 500 }) => {
|
|
233
|
+
const trackerStore = await getTrackerStore();
|
|
234
|
+
const originalCount = trackerStore.entries.length;
|
|
235
|
+
if (originalCount <= keepCount) {
|
|
236
|
+
return {
|
|
237
|
+
content: [{ type: 'text', text: `No cleanup needed. Current entries: ${originalCount}` }]
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
trackerStore.entries = trackerStore.entries.slice(-keepCount);
|
|
241
|
+
await saveTrackerStore(trackerStore);
|
|
242
|
+
const removed = originalCount - keepCount;
|
|
243
|
+
return {
|
|
244
|
+
content: [{ type: 'text', text: `Cleaned up ${removed} old entries. Remaining: ${keepCount}` }]
|
|
245
|
+
};
|
|
246
|
+
});
|
|
196
247
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@asd412id/mcp-context-manager",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "MCP tools for context management - summarizer, memory store, project tracker, checkpoints, and smart file loader",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|