mcp-memory-bucket 0.8.2 → 0.8.4
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/src/memory/repository.js +12 -1
- package/dist/src/server.js +13 -4
- package/dist/src/shared/bucket-folder-tool.js +82 -6
- package/dist/src/shared/folderfoo-tenant.js +7 -0
- package/dist/src/shared/relocate-tool.js +4 -1
- package/dist/src/skills/repository.js +12 -1
- package/package.json +1 -1
- package/dist/src/shared/bucket-root-tool.js +0 -66
|
@@ -129,7 +129,13 @@ export class MemoryRepository {
|
|
|
129
129
|
scanSingleFolder(this.db, this.syncSpec, folder.path);
|
|
130
130
|
this.watcher?.add(folder.path);
|
|
131
131
|
}
|
|
132
|
-
/**
|
|
132
|
+
/**
|
|
133
|
+
* Unregisters a folder: stops watching it and drops its cached rows. Never touches the user's
|
|
134
|
+
* own files on disk. If `name` was a remote (folderfoo) source, also drops its RemoteFolder
|
|
135
|
+
* entry (so a same-named folder added afterwards, local or remote, isn't mistaken for the old
|
|
136
|
+
* connection by remoteFor()) and deletes its local mirror cache directory — that mirror is
|
|
137
|
+
* bucket-owned derived state, not user content, and gets recreated fresh on reconnect.
|
|
138
|
+
*/
|
|
133
139
|
removeFolder(name) {
|
|
134
140
|
const idx = this.folders.findIndex((f) => f.name === name);
|
|
135
141
|
if (idx === -1)
|
|
@@ -137,6 +143,11 @@ export class MemoryRepository {
|
|
|
137
143
|
const [removed] = this.folders.splice(idx, 1);
|
|
138
144
|
this.watcher?.unwatch(removed.path);
|
|
139
145
|
unregisterFolder(this.db, 'memory_docs', name);
|
|
146
|
+
const remoteIdx = this.remoteFolders.findIndex((f) => f.name === name);
|
|
147
|
+
if (remoteIdx !== -1) {
|
|
148
|
+
const [removedRemote] = this.remoteFolders.splice(remoteIdx, 1);
|
|
149
|
+
fs.rmSync(removedRemote.mirrorDir, { recursive: true, force: true });
|
|
150
|
+
}
|
|
140
151
|
}
|
|
141
152
|
/**
|
|
142
153
|
* Exact-match lookup by normalized key, per V0 (no fuzzy matching).
|
package/dist/src/server.js
CHANGED
|
@@ -38,13 +38,22 @@ const PORT = process.env.PORT ? Number(process.env.PORT) : 8767;
|
|
|
38
38
|
const config = loadConfig();
|
|
39
39
|
const db = openCache(config.cacheDbPath);
|
|
40
40
|
const identity = new IdentityTracker(config.folderfooMode);
|
|
41
|
-
|
|
41
|
+
// Built ONCE and shared by reference with SkillRepository below — registerRemoteFolder/addFolder
|
|
42
|
+
// push into this same array in place, so skillSpec.sources (used by initialScan/watchSources/
|
|
43
|
+
// startRemotePolling/pollOne's upsertFile) sees a folder added live, without a restart. Evaluating
|
|
44
|
+
// this array literal separately for skillSpec and for `new SkillRepository(...)` (as before) creates
|
|
45
|
+
// two distinct arrays that fall out of sync the moment a folder is added live: upsertFile's
|
|
46
|
+
// folderForFile lookup against the stale skillSpec.sources then finds no match and stamps folder=""
|
|
47
|
+
// on every doc from that folder — permanently, since upsertFile's mtime-based skip check means the
|
|
48
|
+
// row is never reprocessed to pick up the correct value once the array is finally in sync.
|
|
49
|
+
const skillFolders = [{ name: 'builtin', path: builtinSkillsDir }, ...config.skillFolders];
|
|
50
|
+
const skillSpec = skillSyncSpec(skillFolders);
|
|
42
51
|
const memorySpec = memorySyncSpec(config.memoryFolders);
|
|
43
52
|
initialScan(db, skillSpec);
|
|
44
53
|
initialScan(db, memorySpec);
|
|
45
54
|
const skillWatcher = watchSources(db, skillSpec);
|
|
46
55
|
const memoryWatcher = watchSources(db, memorySpec);
|
|
47
|
-
const skillRepo = new SkillRepository(db,
|
|
56
|
+
const skillRepo = new SkillRepository(db, skillFolders, config.remoteSkillFolders, config.baseDir, identity);
|
|
48
57
|
const memoryRepo = new MemoryRepository(db, config.memoryFolders, config.remoteMemoryFolders, config.baseDir, identity);
|
|
49
58
|
skillRepo.setWatcher(skillWatcher);
|
|
50
59
|
memoryRepo.setWatcher(memoryWatcher);
|
|
@@ -63,14 +72,14 @@ if (config.skillFolders.length === 0 && config.memoryFolders.length === 0) {
|
|
|
63
72
|
// this server — surfaced both in serverInfo.description and instructions so
|
|
64
73
|
// clients that expose either to the model can make that association.
|
|
65
74
|
const SERVER_DESCRIPTION = 'Also known as "memory bucket", "mem bucket", or "skill bucket" — if the user refers to this server by any of those names, they mean this one.';
|
|
66
|
-
const SERVER_INSTRUCTIONS = `${SERVER_DESCRIPTION} Exposes skill_* (reusable coding patterns, stored as agentskills.io-standard SKILL.md folders) and memory_* (point-in-time working context — plans, specs, SQL, session summaries — looked up by key) tools, plus shared relocate/bucket_search/bucket_*_folder tools. Use skill_search/memory_search/bucket_search for full-text search over body content (not just metadata) — bucket_search when you don't know which bucket something landed in. Use bucket_list_folders to see what named source directories (folders) are configured before passing a folder argument elsewhere, and bucket_create_folder/bucket_delete_folder to register or unregister one. Most operations have a _bulk_ variant (bulk_get/bulk_create/bulk_update/bulk_delete/bulk_rename, relocate_bulk) that take a list and return per-item success/failure — prefer these over looping single calls when acting on more than one item. A memory doc's key can be changed in place via memory_update(id, key: ...) — no separate rename tool needed. Before calling any *_create/*_update/relocate tool, call skill_get("memory-bucket-authoring") first to learn the exact frontmatter schema — don't guess the shape. Also exposes memory_channel_read/memory_channel_post/list_memory_channels — a SEPARATE, ephemeral in-memory layer for live cross-agent coordination (a shared scratchpad/discussion channel by name), never written to disk and never indexed by memory_search/bucket_search; do not confuse these with the persisted memory_* docs above.`;
|
|
75
|
+
const SERVER_INSTRUCTIONS = `${SERVER_DESCRIPTION} Exposes skill_* (reusable coding patterns, stored as agentskills.io-standard SKILL.md folders) and memory_* (point-in-time working context — plans, specs, SQL, session summaries — looked up by key) tools, plus shared relocate/bucket_search/bucket_*_folder tools. Use skill_search/memory_search/bucket_search for full-text search over body content (not just metadata) — bucket_search when you don't know which bucket something landed in. Use bucket_list_folders to see what named source directories (folders) are configured before passing a folder argument elsewhere, and bucket_create_folder/bucket_delete_folder to register or unregister one (bucket_delete_folder works for both local and remote folders). bucket_list_remote_folders/bucket_connect_remote_folder connect a folderfoo folder as a new source, once the user is logged in via the web UI. Most operations have a _bulk_ variant (bulk_get/bulk_create/bulk_update/bulk_delete/bulk_rename, relocate_bulk) that take a list and return per-item success/failure — prefer these over looping single calls when acting on more than one item. A memory doc's key can be changed in place via memory_update(id, key: ...) — no separate rename tool needed. Before calling any *_create/*_update/relocate tool, call skill_get("memory-bucket-authoring") first to learn the exact frontmatter schema — don't guess the shape. Also exposes memory_channel_read/memory_channel_post/list_memory_channels — a SEPARATE, ephemeral in-memory layer for live cross-agent coordination (a shared scratchpad/discussion channel by name), never written to disk and never indexed by memory_search/bucket_search; do not confuse these with the persisted memory_* docs above.`;
|
|
67
76
|
function buildMcpServer() {
|
|
68
77
|
const server = new McpServer({ name: 'memory-bucket', version: '0.1.0', description: SERVER_DESCRIPTION }, { capabilities: {}, instructions: SERVER_INSTRUCTIONS });
|
|
69
78
|
registerSkillTools(server, skillRepo);
|
|
70
79
|
registerMemoryTools(server, memoryRepo);
|
|
71
80
|
registerRelocateTool(server, skillRepo, memoryRepo);
|
|
72
81
|
registerSearchTool(server, db);
|
|
73
|
-
registerBucketFolderTools(server, config, skillRepo, memoryRepo, db, skillSpec, memorySpec);
|
|
82
|
+
registerBucketFolderTools(server, config, skillRepo, memoryRepo, db, skillSpec, memorySpec, identity);
|
|
74
83
|
registerAttachmentTools(server, attachmentRepo);
|
|
75
84
|
registerUiTool(server, PORT);
|
|
76
85
|
registerMemoryChannelTools(server);
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import { saveFolder, removeFolder as removeFolderFromConfig, sanitizeFolderName } from '../config.js';
|
|
4
|
+
import { saveFolder, saveRemoteFolder, removeFolder as removeFolderFromConfig, sanitizeFolderName, mirrorDirFor, } from '../config.js';
|
|
5
5
|
import { initialScan } from '../store/sync.js';
|
|
6
|
+
import { listFolders as listFolderfooFolders } from '../remote/folderfoo-client.js';
|
|
7
|
+
import { pollOne } from '../remote/remote-sync.js';
|
|
8
|
+
import { TENANT_ID } from './folderfoo-tenant.js';
|
|
6
9
|
const KIND = z.enum(['skill', 'memory']);
|
|
7
|
-
export function registerBucketFolderTools(mcp, config, skillRepo, memoryRepo, db, skillSpec, memorySpec) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
export function registerBucketFolderTools(mcp, config, skillRepo, memoryRepo, db, skillSpec, memorySpec, identity) {
|
|
11
|
+
const repoFor = (kind) => (kind === 'skill' ? skillRepo : memoryRepo);
|
|
12
|
+
const specFor = (kind) => (kind === 'skill' ? skillSpec : memorySpec);
|
|
13
|
+
mcp.tool('bucket_list_folders', 'Lists the configured skill and memory folders (the named source directories skills/memory docs live under, e.g. "super-skills", "demo-skills", "builtin"), each tagged `remote: true/false` for whether it syncs with folderfoo — use this to see what folders exist before passing a `folder` argument to a create/list/search tool, or before adding/removing one. To see UNconnected folderfoo folders available to connect, use bucket_list_remote_folders instead.', {}, async () => {
|
|
14
|
+
const skill = skillRepo.listFoldersWithRemoteInfo().map((f) => ({ ...f, kind: 'skill' }));
|
|
15
|
+
const memory = memoryRepo.listFoldersWithRemoteInfo().map((f) => ({ ...f, kind: 'memory' }));
|
|
11
16
|
return { content: [{ type: 'text', text: JSON.stringify({ skill, memory }, null, 2) }] };
|
|
12
17
|
});
|
|
13
18
|
mcp.tool('bucket_create_folder', 'Registers a new skill or memory folder: an existing absolute directory path becomes a new named source that skill_create/memory_create can target via `folder`. Scans it once and starts watching it live — never creates the directory itself, it must already exist.', {
|
|
@@ -35,7 +40,78 @@ export function registerBucketFolderTools(mcp, config, skillRepo, memoryRepo, db
|
|
|
35
40
|
return { content: [{ type: 'text', text: err.message }], isError: true };
|
|
36
41
|
}
|
|
37
42
|
});
|
|
38
|
-
mcp.tool('
|
|
43
|
+
mcp.tool('bucket_list_remote_folders', "Lists the current user's own folders on the connected folderfoo deployment, including ones not yet connected to this bucket — use this to pick a `folderPath` for bucket_connect_remote_folder. Each entry is tagged `connected` (the name it's already registered under, per kind, or null) so you don't connect the same remote folder twice. Errors with a clear message if folderfoo integration is off (needs --folderfoo-mode/FOLDERFOO_MODE) or nobody is logged in yet (log in via the web UI, bucket_open_ui, first — this tool cannot itself perform a folderfoo login).", {}, async () => {
|
|
44
|
+
if (config.folderfooMode === 'off' || !config.folderfooHost) {
|
|
45
|
+
return {
|
|
46
|
+
content: [{ type: 'text', text: 'folderfoo integration is off — restart with --folderfoo-mode dev|cloud (or set FOLDERFOO_MODE) to enable it' }],
|
|
47
|
+
isError: true,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const current = identity.current();
|
|
51
|
+
if (!current.username) {
|
|
52
|
+
return { content: [{ type: 'text', text: 'not logged in to folderfoo — open the web UI (bucket_open_ui) and log in first' }], isError: true };
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const server = config.folderfooHost;
|
|
56
|
+
const folders = await listFolderfooFolders(server, config.baseDir, TENANT_ID);
|
|
57
|
+
const connectedFor = (kind) => new Map(repoFor(kind)
|
|
58
|
+
.listRemoteFolders()
|
|
59
|
+
.filter((f) => f.server === server && f.tenantId === TENANT_ID)
|
|
60
|
+
.map((f) => [f.folderPath, f.name]));
|
|
61
|
+
const skillConnected = connectedFor('skill');
|
|
62
|
+
const memoryConnected = connectedFor('memory');
|
|
63
|
+
const annotated = folders.map((f) => ({
|
|
64
|
+
...f,
|
|
65
|
+
connected: { skill: skillConnected.get(f.path) ?? null, memory: memoryConnected.get(f.path) ?? null },
|
|
66
|
+
}));
|
|
67
|
+
return { content: [{ type: 'text', text: JSON.stringify(annotated, null, 2) }] };
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
return { content: [{ type: 'text', text: err.message }], isError: true };
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
mcp.tool('bucket_connect_remote_folder', "Connects one of the user's own folderfoo folders (see bucket_list_remote_folders for `folderPath` values) as a new skill or memory source — syncs like a folder added via bucket_create_folder, so skill_create/memory_create can target it via `folder` and edits push back to folderfoo automatically. Idempotent: if this exact folderPath is already connected under this kind, returns that existing folder instead of creating a duplicate. Same login requirement as bucket_list_remote_folders.", {
|
|
74
|
+
kind: KIND,
|
|
75
|
+
folderPath: z.string().describe("a `path` value from bucket_list_remote_folders ('' for the folderfoo root)"),
|
|
76
|
+
name: z.string().optional().describe("name for the folder; defaults to a sanitized version of folderPath's last segment"),
|
|
77
|
+
}, async ({ kind, folderPath, name }) => {
|
|
78
|
+
if (config.folderfooMode === 'off' || !config.folderfooHost) {
|
|
79
|
+
return {
|
|
80
|
+
content: [{ type: 'text', text: 'folderfoo integration is off — restart with --folderfoo-mode dev|cloud (or set FOLDERFOO_MODE) to enable it' }],
|
|
81
|
+
isError: true,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const current = identity.current();
|
|
85
|
+
if (!current.username) {
|
|
86
|
+
return { content: [{ type: 'text', text: 'not logged in to folderfoo — open the web UI (bucket_open_ui) and log in first' }], isError: true };
|
|
87
|
+
}
|
|
88
|
+
const server = config.folderfooHost;
|
|
89
|
+
const repo = repoFor(kind);
|
|
90
|
+
const already = repo.listRemoteFolders().find((f) => f.server === server && f.tenantId === TENANT_ID && f.folderPath === folderPath);
|
|
91
|
+
if (already) {
|
|
92
|
+
return {
|
|
93
|
+
content: [
|
|
94
|
+
{ type: 'text', text: JSON.stringify({ name: already.name, server, tenantId: TENANT_ID, folderPath, kind, alreadyConnected: true }, null, 2) },
|
|
95
|
+
],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const folderName = sanitizeFolderName(name || folderPath.split('/').filter(Boolean).pop() || TENANT_ID);
|
|
99
|
+
if (!folderName) {
|
|
100
|
+
return { content: [{ type: 'text', text: 'could not derive a valid folder name — provide one explicitly' }], isError: true };
|
|
101
|
+
}
|
|
102
|
+
const mirrorDir = mirrorDirFor(config.baseDir, current.mode, current.username, folderName);
|
|
103
|
+
const remote = { name: folderName, server, tenantId: TENANT_ID, folderPath, mirrorDir, mode: current.mode, username: current.username };
|
|
104
|
+
try {
|
|
105
|
+
repo.registerRemoteFolder(remote);
|
|
106
|
+
saveRemoteFolder(config, kind, { name: folderName, server, tenantId: TENANT_ID, folderPath, mode: current.mode, username: current.username });
|
|
107
|
+
await pollOne(db, specFor(kind), remote, config.baseDir);
|
|
108
|
+
return { content: [{ type: 'text', text: JSON.stringify({ name: folderName, server, tenantId: TENANT_ID, folderPath, kind }, null, 2) }] };
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
return { content: [{ type: 'text', text: err.message }], isError: true };
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
mcp.tool('bucket_delete_folder', 'Unregisters a skill or memory folder by name — works for both local and folderfoo-connected (remote) folders. Stops watching it and drops its cached entries from the index; never touches the user\'s own files, on disk or on folderfoo — for a remote folder, only its local mirror cache is deleted, which is rebuilt fresh if reconnected.', { kind: KIND, name: z.string() }, async ({ kind, name }) => {
|
|
39
115
|
try {
|
|
40
116
|
const repo = kind === 'skill' ? skillRepo : memoryRepo;
|
|
41
117
|
repo.removeFolder(name);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// memory-bucket's own fixed identity (X-Tenant-Id) as a folderfoo-consuming app — distinct from
|
|
2
|
+
// which folderfoo TENANT'S DATA a given remote skill/memory folder pulls from (could be this same
|
|
3
|
+
// tenant, or a different app's, e.g. mindfoo/bulletino). Has no node built-ins so it's safe to
|
|
4
|
+
// import from both the server (registering a remote folder) and the client bundle (the connect-a-
|
|
5
|
+
// folder modal) — the single source of truth for both, replacing what used to be two copies of the
|
|
6
|
+
// same string.
|
|
7
|
+
export const TENANT_ID = 'membkt';
|
|
@@ -16,7 +16,10 @@ const overridesSchema = z
|
|
|
16
16
|
tags: z.array(z.string()).optional(),
|
|
17
17
|
status: statusSchema(['stable', 'beta', 'unreviewed', 'active', 'shipped', 'abandoned']).optional(),
|
|
18
18
|
subfolder: z.string().optional().describe('optional subdirectory under the target folder'),
|
|
19
|
-
folder: z
|
|
19
|
+
folder: z
|
|
20
|
+
.string()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe('which configured folder to write into (a remote/folderfoo-connected folder works too — the file is pushed to folderfoo automatically); required if multiple folders exist for the target type'),
|
|
20
23
|
})
|
|
21
24
|
.optional();
|
|
22
25
|
export function registerRelocateTool(mcp, skillRepo, memoryRepo) {
|
|
@@ -132,7 +132,13 @@ export class SkillRepository {
|
|
|
132
132
|
scanSingleFolder(this.db, this.syncSpec, folder.path);
|
|
133
133
|
this.watcher?.add(folder.path);
|
|
134
134
|
}
|
|
135
|
-
/**
|
|
135
|
+
/**
|
|
136
|
+
* Unregisters a folder: stops watching it and drops its cached rows. Never touches the user's
|
|
137
|
+
* own files on disk. If `name` was a remote (folderfoo) source, also drops its RemoteFolder
|
|
138
|
+
* entry (so a same-named folder added afterwards, local or remote, isn't mistaken for the old
|
|
139
|
+
* connection by remoteFor()) and deletes its local mirror cache directory — that mirror is
|
|
140
|
+
* bucket-owned derived state, not user content, and gets recreated fresh on reconnect.
|
|
141
|
+
*/
|
|
136
142
|
removeFolder(name) {
|
|
137
143
|
const idx = this.folders.findIndex((f) => f.name === name);
|
|
138
144
|
if (idx <= 0)
|
|
@@ -140,6 +146,11 @@ export class SkillRepository {
|
|
|
140
146
|
const [removed] = this.folders.splice(idx, 1);
|
|
141
147
|
this.watcher?.unwatch(removed.path);
|
|
142
148
|
unregisterFolder(this.db, 'skills', name);
|
|
149
|
+
const remoteIdx = this.remoteFolders.findIndex((f) => f.name === name);
|
|
150
|
+
if (remoteIdx !== -1) {
|
|
151
|
+
const [removedRemote] = this.remoteFolders.splice(remoteIdx, 1);
|
|
152
|
+
fs.rmSync(removedRemote.mirrorDir, { recursive: true, force: true });
|
|
153
|
+
}
|
|
143
154
|
}
|
|
144
155
|
/** `includePaused` defaults to false: paused skills are hidden from discovery (see setPaused). */
|
|
145
156
|
list(query, folder, opts = {}) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-memory-bucket",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MCP server exposing skill_* (reusable coding patterns) and memory_* (point-in-time working context) tools over a markdown+frontmatter source, cached into SQLite at runtime.",
|
|
6
6
|
"repository": {
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { z } from 'zod';
|
|
4
|
-
import { saveRoot, removeRoot as removeRootFromConfig, sanitizeRootName } from '../config.js';
|
|
5
|
-
import { initialScan } from '../store/sync.js';
|
|
6
|
-
const KIND = z.enum(['skill', 'memory']);
|
|
7
|
-
export function registerBucketRootTools(mcp, config, skillRepo, memoryRepo, db, skillSpec, memorySpec) {
|
|
8
|
-
mcp.tool('bucket_list_roots', 'Lists the configured skill and memory roots (the named source directories skills/memory docs live under, e.g. "super-skills", "demo-skills", "builtin") — use this to see what roots exist before passing a `root` argument to a create/list/search tool, or before adding/removing one.', {}, async () => {
|
|
9
|
-
const skill = skillRepo.listRoots().map((r) => ({ ...r, kind: 'skill' }));
|
|
10
|
-
const memory = memoryRepo.listRoots().map((r) => ({ ...r, kind: 'memory' }));
|
|
11
|
-
return { content: [{ type: 'text', text: JSON.stringify({ skill, memory }, null, 2) }] };
|
|
12
|
-
});
|
|
13
|
-
mcp.tool('bucket_create_root', 'Registers a new skill or memory root: an existing absolute directory path becomes a new named source that skill_create/memory_create can target via `root`. Scans it once and starts watching it live — never creates the directory itself, it must already exist.', {
|
|
14
|
-
kind: KIND,
|
|
15
|
-
path: z.string().describe('absolute path to an existing directory'),
|
|
16
|
-
name: z.string().optional().describe('name for the root; defaults to a sanitized version of the directory\'s basename'),
|
|
17
|
-
}, async ({ kind, path: dirPath, name }) => {
|
|
18
|
-
try {
|
|
19
|
-
if (!path.isAbsolute(dirPath)) {
|
|
20
|
-
return { content: [{ type: 'text', text: 'path must be an absolute directory path' }], isError: true };
|
|
21
|
-
}
|
|
22
|
-
if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory()) {
|
|
23
|
-
return { content: [{ type: 'text', text: `not a directory: ${dirPath}` }], isError: true };
|
|
24
|
-
}
|
|
25
|
-
const rootName = sanitizeRootName(name || path.basename(dirPath));
|
|
26
|
-
if (!rootName) {
|
|
27
|
-
return { content: [{ type: 'text', text: 'could not derive a valid root name — provide one explicitly' }], isError: true };
|
|
28
|
-
}
|
|
29
|
-
const repo = kind === 'skill' ? skillRepo : memoryRepo;
|
|
30
|
-
repo.addRoot({ name: rootName, path: dirPath });
|
|
31
|
-
saveRoot(config, kind, { name: rootName, path: dirPath });
|
|
32
|
-
return { content: [{ type: 'text', text: JSON.stringify({ name: rootName, path: dirPath, kind }, null, 2) }] };
|
|
33
|
-
}
|
|
34
|
-
catch (err) {
|
|
35
|
-
return { content: [{ type: 'text', text: err.message }], isError: true };
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
mcp.tool('bucket_delete_root', 'Unregisters a skill or memory root by name: stops watching it and drops its cached entries from the index. Never touches files on disk — the directory and its contents are left in place.', { kind: KIND, name: z.string() }, async ({ kind, name }) => {
|
|
39
|
-
try {
|
|
40
|
-
const repo = kind === 'skill' ? skillRepo : memoryRepo;
|
|
41
|
-
repo.removeRoot(name);
|
|
42
|
-
removeRootFromConfig(config, kind, name);
|
|
43
|
-
return { content: [{ type: 'text', text: `Removed ${kind} root "${name}"` }] };
|
|
44
|
-
}
|
|
45
|
-
catch (err) {
|
|
46
|
-
return { content: [{ type: 'text', text: err.message }], isError: true };
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
mcp.tool('bucket_rebuild_cache', 'EMERGENCY USE ONLY. Wipes the entire SQLite cache (all skills, memory docs, the full-text search index, and the date index) and rebuilds it from scratch by rescanning every configured root from disk. Source markdown files on disk are never touched — this only affects the derived cache, which is always safe to discard and regenerate. Use this only when other tools return results that contradict what you can see in the actual files (e.g. stale search hits, a doc that clearly exists on disk but skill_get/memory_get can\'t find, or search_by_date returning wrong dates) and a normal create/update/relocate call hasn\'t resolved it — this is a last resort, not a routine maintenance step. Takes a moment to complete on a large root; nothing else should be called until it returns.', {}, async () => {
|
|
50
|
-
try {
|
|
51
|
-
db.exec(`DELETE FROM skills; DELETE FROM memory_docs; DELETE FROM search_index; DELETE FROM doc_dates;`);
|
|
52
|
-
initialScan(db, skillSpec);
|
|
53
|
-
initialScan(db, memorySpec);
|
|
54
|
-
const skillCount = db.prepare(`SELECT COUNT(*) AS n FROM skills`).get().n;
|
|
55
|
-
const memoryCount = db.prepare(`SELECT COUNT(*) AS n FROM memory_docs`).get().n;
|
|
56
|
-
return {
|
|
57
|
-
content: [
|
|
58
|
-
{ type: 'text', text: `Cache rebuilt from disk: ${skillCount} skill(s), ${memoryCount} memory doc(s) reindexed.` },
|
|
59
|
-
],
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
catch (err) {
|
|
63
|
-
return { content: [{ type: 'text', text: err.message }], isError: true };
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
}
|