mcp-memory-bucket 0.8.3 → 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.
|
@@ -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
|
@@ -72,14 +72,14 @@ if (config.skillFolders.length === 0 && config.memoryFolders.length === 0) {
|
|
|
72
72
|
// this server — surfaced both in serverInfo.description and instructions so
|
|
73
73
|
// clients that expose either to the model can make that association.
|
|
74
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.';
|
|
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. 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.`;
|
|
76
76
|
function buildMcpServer() {
|
|
77
77
|
const server = new McpServer({ name: 'memory-bucket', version: '0.1.0', description: SERVER_DESCRIPTION }, { capabilities: {}, instructions: SERVER_INSTRUCTIONS });
|
|
78
78
|
registerSkillTools(server, skillRepo);
|
|
79
79
|
registerMemoryTools(server, memoryRepo);
|
|
80
80
|
registerRelocateTool(server, skillRepo, memoryRepo);
|
|
81
81
|
registerSearchTool(server, db);
|
|
82
|
-
registerBucketFolderTools(server, config, skillRepo, memoryRepo, db, skillSpec, memorySpec);
|
|
82
|
+
registerBucketFolderTools(server, config, skillRepo, memoryRepo, db, skillSpec, memorySpec, identity);
|
|
83
83
|
registerAttachmentTools(server, attachmentRepo);
|
|
84
84
|
registerUiTool(server, PORT);
|
|
85
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": {
|