mcp-memory-keeper 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.12.0] - 2026-02-06
11
+
12
+ ### Added
13
+
14
+ - **Selective Tool Filtering via Profiles** (#29)
15
+ - Control which tools are exposed to reduce context window usage (~10-15K tokens saved with minimal profile)
16
+ - Three built-in profiles: `minimal` (8 tools), `standard` (22 tools), `full` (38 tools, default)
17
+ - `TOOL_PROFILE` environment variable to select active profile at startup
18
+ - `TOOL_PROFILE_CONFIG` environment variable to specify custom config file path
19
+ - Custom profile definitions via `~/.mcp-memory-keeper/config.json`
20
+ - Config file profiles take precedence over built-in defaults
21
+ - Helpful error messages when disabled tools are called, with guidance on enabling them
22
+ - Startup logging shows active profile, tool count, and source
23
+ - Example config file included in `examples/config.json`
24
+
25
+ ### Technical
26
+
27
+ - New `src/utils/tool-profiles.ts` module with `ALL_TOOL_NAMES` source of truth
28
+ - `ToolName` union type for compile-time safety
29
+ - Deep config validation (guards against malformed JSON, null values, non-array profiles, non-string elements)
30
+ - Drift-detection integration test verifies `ALL_TOOL_NAMES` stays in sync with `index.ts` tool definitions
31
+ - Defense-in-depth: both `ListTools` filtering and `CallTool` guard for disabled tools
32
+ - 100% backwards compatible — no env var + no config = all 38 tools (existing behavior unchanged)
33
+ - All 1185 tests passing across Node.js 20, 22, and 24
34
+
10
35
  ## [0.11.0] - 2025-12-10
11
36
 
12
37
  ### Breaking Changes
@@ -466,7 +491,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
466
491
  - **Security**: Security updates
467
492
  - **Technical**: Internal improvements
468
493
 
469
- [Unreleased]: https://github.com/mkreyman/mcp-memory-keeper/compare/v0.10.0...HEAD
494
+ [Unreleased]: https://github.com/mkreyman/mcp-memory-keeper/compare/v0.12.0...HEAD
495
+ [0.12.0]: https://github.com/mkreyman/mcp-memory-keeper/compare/v0.11.0...v0.12.0
496
+ [0.11.0]: https://github.com/mkreyman/mcp-memory-keeper/compare/v0.10.2...v0.11.0
497
+ [0.10.2]: https://github.com/mkreyman/mcp-memory-keeper/compare/v0.10.1...v0.10.2
498
+ [0.10.1]: https://github.com/mkreyman/mcp-memory-keeper/compare/v0.10.0...v0.10.1
470
499
  [0.10.0]: https://github.com/mkreyman/mcp-memory-keeper/compare/v0.9.0...v0.10.0
471
500
  [0.9.0]: https://github.com/mkreyman/mcp-memory-keeper/compare/v0.8.4...v0.9.0
472
501
  [0.8.4]: https://github.com/mkreyman/mcp-memory-keeper/compare/v0.8.3...v0.8.4
package/README.md CHANGED
@@ -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
@@ -58,6 +58,9 @@ describe('Git Integration Tests', () => {
58
58
  await git.init();
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('.');
@@ -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 });