@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 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.ensureDir(this.basePath);
10
+ this.enableBackup = options.enableBackup ?? true;
11
+ this.maxBackups = options.maxBackups ?? 3;
12
+ this.ensureDirSync(this.basePath);
8
13
  }
9
- ensureDir(dirPath) {
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
- if (fs.existsSync(filePath)) {
21
- const content = fs.readFileSync(filePath, 'utf-8');
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
- console.error(`Error reading ${filename}:`, error);
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
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
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
- if (fs.existsSync(filePath)) {
45
- fs.unlinkSync(filePath);
46
- return true;
47
- }
89
+ await fsp.unlink(filePath);
90
+ return true;
48
91
  }
49
92
  catch (error) {
50
- console.error(`Error deleting ${filename}:`, error);
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
- return fs.existsSync(this.getFilePath(filename));
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
- return fs.readdirSync(dirPath).filter(f => f.endsWith('.json'));
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({ basePath });
136
+ export function initStore(basePath, options) {
137
+ storeInstance = new FileStore({
138
+ basePath,
139
+ ...options
140
+ });
83
141
  return storeInstance;
84
142
  }
@@ -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
- return store.read('index.json', { checkpoints: [] });
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: 'Save current session state as a checkpoint. Use before context gets too long or at important milestones.',
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: 'Load a previously saved checkpoint to restore context.',
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')
@@ -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: 'Read a file with smart options: specific lines, keyword search, or structure extraction. Optimized for context efficiency.',
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: 'Get file metadata without reading content. Use to check file existence, size, and modification time.',
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
- if (!fs.existsSync(filePath)) {
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 = fs.readFileSync(filePath, 'utf-8');
210
+ const content = await fsp.readFile(filePath, 'utf-8');
192
211
  const lines = content.split('\n');
193
- const regex = new RegExp(pattern, 'gi');
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
- if (!fs.existsSync(dirPath)) {
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
- function walkDir(dir) {
233
- const items = fs.readdirSync(dir);
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 = fs.statSync(fullPath);
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 (pattern) {
242
- const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
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',
@@ -1,2 +1,3 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function cleanupExpiredMemories(): Promise<number>;
2
3
  export declare function registerMemoryTools(server: McpServer): void;
@@ -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
- return store.read(MEMORY_FILE, { entries: {} });
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: 'Store a key-value pair in persistent memory. Use for saving important context, decisions, or data across sessions.',
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: 'Retrieve a value from persistent memory by key.',
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: 'Search memories by key pattern or tags.',
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
- const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$', 'i');
95
- results = results.filter(entry => regex.test(entry.key));
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: 'List all memory keys with their tags.',
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,2 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerSessionTools(server: McpServer): void;
@@ -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
+ }
@@ -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
- return store.read('index.json', { summaries: [] });
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: 'Summarize and compress context/conversation. Extracts key points, decisions, and action items. Use this when context is getting too long.',
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)'),
@@ -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
- return store.read(TRACKER_FILE, { entries: [] });
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: 'Log a decision, change, todo, note, or error to the project tracker.',
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: 'Get current project status including recent decisions, pending todos, and recent changes.',
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: 'Update the status of a todo item.',
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",
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",