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 +30 -1
- package/README.md +86 -0
- package/dist/__tests__/integration/git-integration.test.js +3 -0
- package/dist/__tests__/integration/project-directory.test.js +6 -0
- package/dist/__tests__/integration/tool-profiles-integration.test.js +150 -0
- package/dist/__tests__/utils/project-directory-messages.test.js +3 -0
- package/dist/__tests__/utils/tool-profiles.test.js +374 -0
- package/dist/index.js +1075 -1052
- package/dist/utils/tool-profiles.js +242 -0
- package/examples/config.json +31 -0
- package/examples/project-directory-setup.md +114 -0
- package/package.json +2 -1
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.
|
|
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 });
|