mcp-memory-keeper 0.11.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.12.1] - 2026-03-24
11
+
12
+ ### Fixed
13
+
14
+ - **Server crashes with SQLITE_CANTOPEN when parent process CWD changes** (#31)
15
+ - Server now resolves database path to an absolute location (`$DATA_DIR` or `~/mcp-data/memory-keeper/`) instead of relying on CWD
16
+ - Added try/catch around data directory creation with actionable error message
17
+ - Startup warning with exact `cp` command when legacy `context.db` detected in CWD
18
+ - README "from source" install command now points to `bin/mcp-memory-keeper` instead of `node dist/index.js`
19
+ - Added Upgrading section documenting database path change and migration steps
20
+
21
+ ### Technical
22
+
23
+ - Fixed integration tests to use `DATA_DIR` instead of dead `MCP_DB_PATH` environment variable
24
+ - Fixed `git.init()` in tests to use `--initial-branch=master` for deterministic behavior
25
+
26
+ ## [0.12.0] - 2026-02-06
27
+
28
+ ### Added
29
+
30
+ - **Selective Tool Filtering via Profiles** (#29)
31
+ - Control which tools are exposed to reduce context window usage (~10-15K tokens saved with minimal profile)
32
+ - Three built-in profiles: `minimal` (8 tools), `standard` (22 tools), `full` (38 tools, default)
33
+ - `TOOL_PROFILE` environment variable to select active profile at startup
34
+ - `TOOL_PROFILE_CONFIG` environment variable to specify custom config file path
35
+ - Custom profile definitions via `~/.mcp-memory-keeper/config.json`
36
+ - Config file profiles take precedence over built-in defaults
37
+ - Helpful error messages when disabled tools are called, with guidance on enabling them
38
+ - Startup logging shows active profile, tool count, and source
39
+ - Example config file included in `examples/config.json`
40
+
41
+ ### Technical
42
+
43
+ - New `src/utils/tool-profiles.ts` module with `ALL_TOOL_NAMES` source of truth
44
+ - `ToolName` union type for compile-time safety
45
+ - Deep config validation (guards against malformed JSON, null values, non-array profiles, non-string elements)
46
+ - Drift-detection integration test verifies `ALL_TOOL_NAMES` stays in sync with `index.ts` tool definitions
47
+ - Defense-in-depth: both `ListTools` filtering and `CallTool` guard for disabled tools
48
+ - 100% backwards compatible — no env var + no config = all 38 tools (existing behavior unchanged)
49
+ - All 1185 tests passing across Node.js 20, 22, and 24
50
+
10
51
  ## [0.11.0] - 2025-12-10
11
52
 
12
53
  ### Breaking Changes
@@ -466,7 +507,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
466
507
  - **Security**: Security updates
467
508
  - **Technical**: Internal improvements
468
509
 
469
- [Unreleased]: https://github.com/mkreyman/mcp-memory-keeper/compare/v0.10.0...HEAD
510
+ [Unreleased]: https://github.com/mkreyman/mcp-memory-keeper/compare/v0.12.1...HEAD
511
+ [0.12.1]: https://github.com/mkreyman/mcp-memory-keeper/compare/v0.12.0...v0.12.1
512
+ [0.12.0]: https://github.com/mkreyman/mcp-memory-keeper/compare/v0.11.0...v0.12.0
513
+ [0.11.0]: https://github.com/mkreyman/mcp-memory-keeper/compare/v0.10.2...v0.11.0
514
+ [0.10.2]: https://github.com/mkreyman/mcp-memory-keeper/compare/v0.10.1...v0.10.2
515
+ [0.10.1]: https://github.com/mkreyman/mcp-memory-keeper/compare/v0.10.0...v0.10.1
470
516
  [0.10.0]: https://github.com/mkreyman/mcp-memory-keeper/compare/v0.9.0...v0.10.0
471
517
  [0.9.0]: https://github.com/mkreyman/mcp-memory-keeper/compare/v0.8.4...v0.9.0
472
518
  [0.8.4]: https://github.com/mkreyman/mcp-memory-keeper/compare/v0.8.3...v0.8.4
package/README.md CHANGED
@@ -174,7 +174,7 @@ npm install
174
174
  npm run build
175
175
 
176
176
  # 4. Add to Claude
177
- claude mcp add memory-keeper node /absolute/path/to/mcp-memory-keeper/dist/index.js
177
+ claude mcp add memory-keeper /absolute/path/to/mcp-memory-keeper/bin/mcp-memory-keeper
178
178
  ```
179
179
 
180
180
  </details>
@@ -213,6 +213,92 @@ export MCP_MAX_ITEMS=50 # Fewer items per response
213
213
  export MCP_CHARS_PER_TOKEN=3.0 # More conservative estimation (optional)
214
214
  ```
215
215
 
216
+ #### Tool Profiles
217
+
218
+ By default, all 38 tools are exposed. To reduce context overhead in your AI assistant, you can activate a tool profile that limits which tools are available.
219
+
220
+ **Quick usage:**
221
+
222
+ ```bash
223
+ # Essential tools only (8 tools)
224
+ TOOL_PROFILE=minimal npx mcp-memory-keeper
225
+
226
+ # Standard workflow set (22 tools)
227
+ TOOL_PROFILE=standard npx mcp-memory-keeper
228
+
229
+ # All tools (default)
230
+ TOOL_PROFILE=full npx mcp-memory-keeper
231
+ ```
232
+
233
+ **Built-in profiles:**
234
+
235
+ | Profile | Tools | Description |
236
+ | ---------- | ----- | -------------------------------------------------------------- |
237
+ | `minimal` | 8 | Core persistence: save, get, search, status, checkpoint |
238
+ | `standard` | 22 | Daily workflow: core + git, batch ops, channels, export/import |
239
+ | `full` | 38 | All tools (default, backwards compatible) |
240
+
241
+ **Custom profiles via config file:**
242
+
243
+ Create `~/.mcp-memory-keeper/config.json` to define or override profiles:
244
+
245
+ ```json
246
+ {
247
+ "profiles": {
248
+ "my_workflow": [
249
+ "context_session_start",
250
+ "context_save",
251
+ "context_get",
252
+ "context_search",
253
+ "context_checkpoint",
254
+ "context_restore_checkpoint",
255
+ "context_diff",
256
+ "context_timeline"
257
+ ]
258
+ }
259
+ }
260
+ ```
261
+
262
+ Then activate it: `TOOL_PROFILE=my_workflow npx mcp-memory-keeper`
263
+
264
+ Config file profiles take precedence over built-in defaults with the same name.
265
+
266
+ **Profile resolution precedence:**
267
+
268
+ | `TOOL_PROFILE` | Config file has profile? | Built-in exists? | Result |
269
+ | -------------- | ------------------------ | ---------------- | -------------------------------- |
270
+ | Set | Yes | — | Uses config file definition |
271
+ | Set | No | Yes | Uses built-in definition |
272
+ | Set | No | No | Warning + falls back to `full` |
273
+ | Not set | — | — | Uses built-in `full` (all tools) |
274
+
275
+ **Environment variables:**
276
+
277
+ | Variable | Description |
278
+ | --------------------- | ------------------------------------------------------------------------- |
279
+ | `TOOL_PROFILE` | Profile name to activate (e.g., `minimal`, `standard`, `full`, or custom) |
280
+ | `TOOL_PROFILE_CONFIG` | Override config file path (default: `~/.mcp-memory-keeper/config.json`) |
281
+
282
+ > Note: Profile resolution happens once at server startup. Changes to the env var or config file take effect on the next server restart.
283
+
284
+ **Claude Code / Claude Desktop configuration:**
285
+
286
+ ```json
287
+ {
288
+ "mcpServers": {
289
+ "memory-keeper": {
290
+ "command": "npx",
291
+ "args": ["mcp-memory-keeper"],
292
+ "env": {
293
+ "TOOL_PROFILE": "minimal"
294
+ }
295
+ }
296
+ }
297
+ }
298
+ ```
299
+
300
+ See `examples/config.json` for a complete example config file.
301
+
216
302
  ### Claude Code (CLI)
217
303
 
218
304
  #### Configuration Scopes
@@ -1126,6 +1212,38 @@ Test categories:
1126
1212
  - [ ] Custom context templates
1127
1213
  - [ ] Automatic retention policies
1128
1214
 
1215
+ ## Upgrading
1216
+
1217
+ ### Database path change (v0.12.x+)
1218
+
1219
+ Prior to this release, the server resolved `context.db` relative to the process's current working directory. The database now lives at an absolute path:
1220
+
1221
+ - **Default:** `~/mcp-data/memory-keeper/context.db`
1222
+ - **Custom:** set `DATA_DIR=/your/path` — the server will use `$DATA_DIR/context.db`
1223
+
1224
+ If you have existing data in a `context.db` in your old working directory, move it to the new location before restarting the server:
1225
+
1226
+ ```bash
1227
+ mkdir -p ~/mcp-data/memory-keeper
1228
+ cp /path/to/old/context.db ~/mcp-data/memory-keeper/context.db
1229
+ ```
1230
+
1231
+ If `DATA_DIR` is set, use that path as the destination instead of `~/mcp-data/memory-keeper/`.
1232
+
1233
+ The server will print a warning to stderr if it detects a `context.db` in the current directory that differs from the configured data directory, including the exact `cp` command to run.
1234
+
1235
+ ### From-source install command change
1236
+
1237
+ If you registered memory-keeper using `node dist/index.js` directly, update your MCP config to use the bin wrapper instead:
1238
+
1239
+ ```bash
1240
+ # remove the old entry
1241
+ claude mcp remove memory-keeper
1242
+
1243
+ # add the updated entry
1244
+ claude mcp add memory-keeper /absolute/path/to/mcp-memory-keeper/bin/mcp-memory-keeper
1245
+ ```
1246
+
1129
1247
  ## Contributing
1130
1248
 
1131
1249
  Contributions are welcome! Please feel free to submit a Pull Request.
@@ -55,9 +55,12 @@ describe('Git Integration Tests', () => {
55
55
  // Create and initialize a real git repo for testing
56
56
  fs.mkdirSync(tempRepoPath, { recursive: true });
57
57
  git = (0, simple_git_1.simpleGit)(tempRepoPath);
58
- await git.init();
58
+ await git.init(['--initial-branch=master']);
59
59
  await git.addConfig('user.name', 'Test User');
60
60
  await git.addConfig('user.email', 'test@example.com');
61
+ // Use repo-local hooks directory to prevent global hooks from interfering
62
+ const localHooksDir = path.join(tempRepoPath, '.git', 'hooks');
63
+ await git.addConfig('core.hooksPath', localHooksDir);
61
64
  // Create initial commit
62
65
  fs.writeFileSync(path.join(tempRepoPath, 'README.md'), '# Test Repo');
63
66
  await git.add('.');
@@ -55,6 +55,9 @@ describe('Project Directory Feature Tests', () => {
55
55
  await git.init();
56
56
  await git.addConfig('user.name', 'Test User');
57
57
  await git.addConfig('user.email', 'test@example.com');
58
+ // Use repo-local hooks directory to prevent global hooks from interfering
59
+ const localHooksDir = path.join(tempProjectPath, '.git', 'hooks');
60
+ await git.addConfig('core.hooksPath', localHooksDir);
58
61
  // Create initial commit
59
62
  fs.writeFileSync(path.join(tempProjectPath, 'README.md'), '# Test Project');
60
63
  await git.add('.');
@@ -242,6 +245,9 @@ describe('Project Directory Feature Tests', () => {
242
245
  // Configure git for this test to avoid CI failures
243
246
  await git.addConfig('user.name', 'Test User');
244
247
  await git.addConfig('user.email', 'test@example.com');
248
+ // Use repo-local hooks directory to prevent global hooks from interfering
249
+ const localHooksDir = path.join(pathWithSpaces, '.git', 'hooks');
250
+ await git.addConfig('core.hooksPath', localHooksDir);
245
251
  try {
246
252
  fs.writeFileSync(path.join(pathWithSpaces, 'test.txt'), 'content');
247
253
  await git.add('.');
@@ -86,12 +86,11 @@ const os = __importStar(require("os"));
86
86
  }
87
87
  });
88
88
  (0, globals_1.it)('should start server and respond to initialize request', done => {
89
- const dbPath = path.join(tempDir, 'test.db');
90
89
  // Start the server
91
90
  serverProcess = (0, child_process_1.spawn)('node', [path.join(__dirname, '../../../dist/index.js')], {
92
91
  env: {
93
92
  ...process.env,
94
- MCP_DB_PATH: dbPath,
93
+ DATA_DIR: tempDir,
95
94
  },
96
95
  stdio: ['pipe', 'pipe', 'pipe'],
97
96
  });
@@ -161,6 +160,7 @@ const os = __importStar(require("os"));
161
160
  // Start the server
162
161
  serverProcess = (0, child_process_1.spawn)('node', [path.join(__dirname, '../../../dist/index.js')], {
163
162
  stdio: ['pipe', 'pipe', 'pipe'],
163
+ env: { ...process.env, DATA_DIR: tempDir },
164
164
  });
165
165
  // Track for global cleanup
166
166
  if (!global.testProcesses) {
@@ -190,11 +190,10 @@ const os = __importStar(require("os"));
190
190
  }
191
191
  });
192
192
  (0, globals_1.it)('should handle invalid requests gracefully', done => {
193
- const dbPath = path.join(tempDir, 'test.db');
194
193
  serverProcess = (0, child_process_1.spawn)('node', [path.join(__dirname, '../../../dist/index.js')], {
195
194
  env: {
196
195
  ...process.env,
197
- MCP_DB_PATH: dbPath,
196
+ DATA_DIR: tempDir,
198
197
  },
199
198
  stdio: ['pipe', 'pipe', 'pipe'],
200
199
  });
@@ -256,11 +255,10 @@ const os = __importStar(require("os"));
256
255
  }, 5000);
257
256
  });
258
257
  (0, globals_1.it)('should handle server shutdown gracefully', done => {
259
- const dbPath = path.join(tempDir, 'test.db');
260
258
  serverProcess = (0, child_process_1.spawn)('node', [path.join(__dirname, '../../../dist/index.js')], {
261
259
  env: {
262
260
  ...process.env,
263
- MCP_DB_PATH: dbPath,
261
+ DATA_DIR: tempDir,
264
262
  },
265
263
  stdio: ['pipe', 'pipe', 'pipe'],
266
264
  });
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const globals_1 = require("@jest/globals");
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const tool_profiles_1 = require("../../utils/tool-profiles");
40
+ /**
41
+ * Drift-detection: extract tool names from the ListToolsRequestSchema handler
42
+ * in src/index.ts to verify ALL_TOOL_NAMES stays in sync with actual tool definitions.
43
+ */
44
+ function extractToolNamesFromIndexTs() {
45
+ const indexPath = path.join(__dirname, '..', '..', 'index.ts');
46
+ const src = fs.readFileSync(indexPath, 'utf-8');
47
+ // Find the allTools array: starts after "const allTools" and ends at the matching "];"
48
+ // We look for tool name strings inside the ListToolsRequestSchema handler
49
+ const toolNameRegex = /^\s+name:\s+'(context_[a-z_]+)'/gm;
50
+ const names = [];
51
+ let match;
52
+ // Only capture tool names outside of block comments (skip commented-out tools)
53
+ // Split by block comment boundaries and only scan non-comment sections
54
+ const sections = src.split(/\/\*[\s\S]*?\*\//);
55
+ for (const section of sections) {
56
+ toolNameRegex.lastIndex = 0;
57
+ while ((match = toolNameRegex.exec(section)) !== null) {
58
+ names.push(match[1]);
59
+ }
60
+ }
61
+ return names;
62
+ }
63
+ (0, globals_1.describe)('Tool Profile Integration Tests', () => {
64
+ const originalEnv = process.env.TOOL_PROFILE;
65
+ afterEach(() => {
66
+ if (originalEnv !== undefined) {
67
+ process.env.TOOL_PROFILE = originalEnv;
68
+ }
69
+ else {
70
+ delete process.env.TOOL_PROFILE;
71
+ }
72
+ });
73
+ (0, globals_1.describe)('Drift detection: ALL_TOOL_NAMES vs index.ts', () => {
74
+ (0, globals_1.it)('ALL_TOOL_NAMES should match active tools defined in index.ts', () => {
75
+ const indexToolNames = extractToolNamesFromIndexTs();
76
+ const allToolNamesArray = [...tool_profiles_1.ALL_TOOL_NAMES];
77
+ // Same count
78
+ (0, globals_1.expect)(allToolNamesArray.length).toBe(indexToolNames.length);
79
+ // Same set of names
80
+ (0, globals_1.expect)(new Set(allToolNamesArray)).toEqual(new Set(indexToolNames));
81
+ });
82
+ (0, globals_1.it)('should not include commented-out tools', () => {
83
+ // context_share and context_get_shared are commented out in index.ts
84
+ (0, globals_1.expect)(tool_profiles_1.ALL_TOOL_NAMES_SET.has('context_share')).toBe(false);
85
+ (0, globals_1.expect)(tool_profiles_1.ALL_TOOL_NAMES_SET.has('context_get_shared')).toBe(false);
86
+ });
87
+ });
88
+ (0, globals_1.describe)('Profile filtering behavior', () => {
89
+ (0, globals_1.it)('minimal profile should include core tools and exclude advanced tools', () => {
90
+ process.env.TOOL_PROFILE = 'minimal';
91
+ const profile = (0, tool_profiles_1.resolveActiveProfile)('/nonexistent/config.json');
92
+ // Core tools present
93
+ (0, globals_1.expect)(profile.tools.has('context_save')).toBe(true);
94
+ (0, globals_1.expect)(profile.tools.has('context_get')).toBe(true);
95
+ (0, globals_1.expect)(profile.tools.has('context_search')).toBe(true);
96
+ (0, globals_1.expect)(profile.tools.has('context_checkpoint')).toBe(true);
97
+ // Advanced tools absent
98
+ (0, globals_1.expect)(profile.tools.has('context_analyze')).toBe(false);
99
+ (0, globals_1.expect)(profile.tools.has('context_visualize')).toBe(false);
100
+ (0, globals_1.expect)(profile.tools.has('context_delegate')).toBe(false);
101
+ (0, globals_1.expect)(profile.tools.has('context_semantic_search')).toBe(false);
102
+ });
103
+ (0, globals_1.it)('default (no env var) should expose all tools with backwards-compatible behavior', () => {
104
+ delete process.env.TOOL_PROFILE;
105
+ const profile = (0, tool_profiles_1.resolveActiveProfile)('/nonexistent/config.json');
106
+ (0, globals_1.expect)(profile.tools.size).toBe(tool_profiles_1.ALL_TOOL_NAMES.length);
107
+ (0, globals_1.expect)(profile.profileName).toBe('full');
108
+ (0, globals_1.expect)(profile.source).toBe('default');
109
+ (0, globals_1.expect)(profile.warnings).toHaveLength(0);
110
+ });
111
+ });
112
+ (0, globals_1.describe)('CallTool guard behavior', () => {
113
+ (0, globals_1.it)('disabled tool should be in ALL_TOOL_NAMES_SET but not in profile tools', () => {
114
+ process.env.TOOL_PROFILE = 'minimal';
115
+ const profile = (0, tool_profiles_1.resolveActiveProfile)('/nonexistent/config.json');
116
+ const disabledTool = 'context_analyze';
117
+ // The guard logic: known tool that is not enabled
118
+ const isKnown = tool_profiles_1.ALL_TOOL_NAMES_SET.has(disabledTool);
119
+ const isEnabled = profile.tools.has(disabledTool);
120
+ (0, globals_1.expect)(isKnown).toBe(true);
121
+ (0, globals_1.expect)(isEnabled).toBe(false);
122
+ // In index.ts: isKnown && !isEnabled → return isError: true
123
+ });
124
+ (0, globals_1.it)('unknown tool should not be in ALL_TOOL_NAMES_SET (falls through to default switch)', () => {
125
+ const unknownTool = 'non_existent_tool';
126
+ (0, globals_1.expect)(tool_profiles_1.ALL_TOOL_NAMES_SET.has(unknownTool)).toBe(false);
127
+ // In index.ts: !isKnown → falls through to default: throw new Error()
128
+ });
129
+ (0, globals_1.it)('enabled tool should pass both checks', () => {
130
+ process.env.TOOL_PROFILE = 'minimal';
131
+ const profile = (0, tool_profiles_1.resolveActiveProfile)('/nonexistent/config.json');
132
+ const enabledTool = 'context_save';
133
+ const isKnown = tool_profiles_1.ALL_TOOL_NAMES_SET.has(enabledTool);
134
+ const isEnabled = profile.tools.has(enabledTool);
135
+ (0, globals_1.expect)(isKnown).toBe(true);
136
+ (0, globals_1.expect)(isEnabled).toBe(true);
137
+ // In index.ts: isKnown && isEnabled → guard does not fire, proceeds to switch
138
+ });
139
+ });
140
+ (0, globals_1.describe)('TOOL_PROFILE_CONFIG support', () => {
141
+ (0, globals_1.it)('resolveActiveProfile accepts custom config path (used by TOOL_PROFILE_CONFIG)', () => {
142
+ // This tests the mechanism that index.ts uses:
143
+ // resolveActiveProfile(process.env.TOOL_PROFILE_CONFIG)
144
+ const result = (0, tool_profiles_1.resolveActiveProfile)('/nonexistent/custom/path/config.json');
145
+ // Missing file → no config → falls back to built-in 'full'
146
+ (0, globals_1.expect)(result.profileName).toBe('full');
147
+ (0, globals_1.expect)(result.tools.size).toBe(38);
148
+ });
149
+ });
150
+ });
@@ -123,6 +123,9 @@ Tip: Initialize git with 'git init' to enable git tracking features.`;
123
123
  await git.init();
124
124
  await git.addConfig('user.name', 'Test User');
125
125
  await git.addConfig('user.email', 'test@example.com');
126
+ // Use repo-local hooks directory to prevent global hooks from interfering
127
+ const localHooksDir = path.join(tempRepoPath, '.git', 'hooks');
128
+ await git.addConfig('core.hooksPath', localHooksDir);
126
129
  });
127
130
  afterEach(() => {
128
131
  fs.rmSync(tempRepoPath, { recursive: true, force: true });