mcp-memory-bucket 0.8.1 → 0.8.2

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.
@@ -33,7 +33,7 @@
33
33
  z-index: 2147483000;
34
34
  }
35
35
  </style>
36
- <script type="module" crossorigin src="/assets/index-Y4C-4HnN.js"></script>
36
+ <script type="module" crossorigin src="/assets/index-_w_b_MQl.js"></script>
37
37
  </head>
38
38
  <body>
39
39
  <mem-bucket-app></mem-bucket-app>
@@ -5,9 +5,18 @@ const FOLDERFOO_CLOUD_HOST = 'https://files.cuul.cc';
5
5
  function isRemoteEntry(entry) {
6
6
  return typeof entry === 'object' && 'remote' in entry;
7
7
  }
8
- /** Local mirror directory a remote source's content is synced into - see the RemoteFolder doc comment above. */
9
- export function mirrorDirFor(baseDir, name) {
10
- return path.join(baseDir, '.memory-bucket-remote-cache', sanitizeFolderName(name));
8
+ /**
9
+ * Local mirror directory a remote source's content is synced into - see the
10
+ * RemoteFolder doc comment above. Nested under a {mode}_{username} segment
11
+ * so mirrors from different folderfoo deployments/logins never collide or
12
+ * get mistaken for one another (see identity.ts) - a mirror connected while
13
+ * logged into "dev" as "anatoli" lands at
14
+ * .memory-bucket-remote-cache/dev_anatoli/<name>/, distinct from the same
15
+ * folder name connected under "cloud" or a different username.
16
+ */
17
+ export function mirrorDirFor(baseDir, mode, username, name) {
18
+ const identitySegment = sanitizeFolderName(`${mode}_${username}`);
19
+ return path.join(baseDir, '.memory-bucket-remote-cache', identitySegment, sanitizeFolderName(name));
11
20
  }
12
21
  function memoryDirFlag(argv) {
13
22
  const idx = argv.indexOf('--memory-dir');
@@ -55,7 +64,7 @@ function resolveFolders(entries, baseDir) {
55
64
  return { name: nameFromPath(entry), path: path.resolve(baseDir, entry) };
56
65
  }
57
66
  if (isRemoteEntry(entry)) {
58
- const mirrorDir = mirrorDirFor(baseDir, entry.name);
67
+ const mirrorDir = mirrorDirFor(baseDir, entry.remote.mode, entry.remote.username, entry.name);
59
68
  remoteFolders.push({ name: entry.name, ...entry.remote, mirrorDir });
60
69
  return { name: entry.name, path: mirrorDir };
61
70
  }
@@ -130,7 +139,16 @@ export function saveRemoteFolder(config, kind, entry) {
130
139
  ...current,
131
140
  [key]: [
132
141
  ...existing,
133
- { name: entry.name, remote: { server: entry.server, tenantId: entry.tenantId, folderPath: entry.folderPath } },
142
+ {
143
+ name: entry.name,
144
+ remote: {
145
+ server: entry.server,
146
+ tenantId: entry.tenantId,
147
+ folderPath: entry.folderPath,
148
+ mode: entry.mode,
149
+ username: entry.username,
150
+ },
151
+ },
134
152
  ],
135
153
  };
136
154
  fs.writeFileSync(config.configPath, JSON.stringify(next, null, 2) + '\n');
@@ -11,6 +11,7 @@ import { applyBodyEdits } from '../shared/body-edits.js';
11
11
  import { attachmentsDirFor } from '../attachments/storage.js';
12
12
  import { normalizeKey } from '../types.js';
13
13
  import { readFile as readRemoteFile, writeFile as writeRemoteFile, joinRemoteFolderPath } from '../remote/folderfoo-client.js';
14
+ import { isFolderVisible } from '../remote/identity.js';
14
15
  /** Uppercases and strips everything but letters/digits — used to compare keys that differ only in
15
16
  * punctuation/whitespace formatting (e.g. `RMXS-15` and `RMXS15` strip to the same `RMXS15`). */
16
17
  export function stripKey(s) {
@@ -40,19 +41,37 @@ export class MemoryRepository {
40
41
  folders;
41
42
  remoteFolders;
42
43
  credentialsBaseDir;
44
+ identity;
43
45
  syncSpec;
44
46
  watcher;
45
- constructor(db, folders, remoteFolders = [], credentialsBaseDir) {
47
+ constructor(db, folders, remoteFolders = [], credentialsBaseDir, identity) {
46
48
  this.db = db;
47
49
  this.folders = folders;
48
50
  this.remoteFolders = remoteFolders;
49
51
  this.credentialsBaseDir = credentialsBaseDir;
52
+ this.identity = identity;
50
53
  this.syncSpec = memorySyncSpec(folders);
51
54
  }
52
55
  /** The RemoteFolder a NamedFolder name resolves to, or undefined for a local (non-remote) folder. */
53
56
  remoteFor(folderName) {
54
57
  return this.remoteFolders.find((f) => f.name === folderName);
55
58
  }
59
+ /** Whether `folderName` should be visible right now — always true for local folders; for a remote folder, only when it matches the current login (see identity.ts). No identity tracker configured means folderfoo integration is off entirely, so nothing remote is ever visible. */
60
+ isFolderNameVisible(folderName) {
61
+ const remote = this.remoteFor(folderName);
62
+ if (!remote)
63
+ return true;
64
+ if (!this.identity)
65
+ return false;
66
+ return isFolderVisible(remote, this.identity.current());
67
+ }
68
+ /** Names of every remote folder NOT matching the current identity — used to exclude their rows from list/search SQL. */
69
+ hiddenFolderNames() {
70
+ if (!this.identity)
71
+ return this.remoteFolders.map((f) => f.name);
72
+ const identity = this.identity.current();
73
+ return this.remoteFolders.filter((f) => !isFolderVisible(f, identity)).map((f) => f.name);
74
+ }
56
75
  /** Attaches the live chokidar watcher so addFolder/removeFolder can mutate it without a restart. */
57
76
  setWatcher(watcher) {
58
77
  this.watcher = watcher;
@@ -64,9 +83,9 @@ export class MemoryRepository {
64
83
  listRemoteFolders() {
65
84
  return [...this.remoteFolders];
66
85
  }
67
- /** Same as listFolders(), but tags each entry with whether it's a remote (folderfoo) source — for the web UI's folder list, e.g. to render remote folders in a distinct color. */
86
+ /** Same as listFolders(), but tags each entry with whether it's a remote (folderfoo) source — for the web UI's folder list, e.g. to render remote folders in a distinct color. Excludes remote folders that don't match the current login (see identity.ts). */
68
87
  listFoldersWithRemoteInfo() {
69
- return this.folders.map((f) => ({ ...f, remote: !!this.remoteFor(f.name) }));
88
+ return this.folders.filter((f) => this.isFolderNameVisible(f.name)).map((f) => ({ ...f, remote: !!this.remoteFor(f.name) }));
70
89
  }
71
90
  resolveFolder(folderName) {
72
91
  if (folderName) {
@@ -134,6 +153,11 @@ export class MemoryRepository {
134
153
  if (!opts.includePaused) {
135
154
  conditions.push('paused = 0');
136
155
  }
156
+ const hidden = this.hiddenFolderNames();
157
+ if (hidden.length > 0) {
158
+ conditions.push(`folder NOT IN (${hidden.map(() => '?').join(', ')})`);
159
+ params.push(...hidden);
160
+ }
137
161
  const rows = this.db
138
162
  .prepare(`SELECT * FROM memory_docs WHERE ${conditions.join(' AND ')}`)
139
163
  .all(...params);
@@ -168,6 +192,11 @@ export class MemoryRepository {
168
192
  if (!includePaused) {
169
193
  conditions.push('m.paused = 0');
170
194
  }
195
+ const hidden = this.hiddenFolderNames();
196
+ if (hidden.length > 0) {
197
+ conditions.push(`m.folder NOT IN (${hidden.map(() => '?').join(', ')})`);
198
+ params.push(...hidden);
199
+ }
171
200
  params.push(limit, offset);
172
201
  try {
173
202
  const rows = this.db
@@ -198,6 +227,8 @@ export class MemoryRepository {
198
227
  if (!row)
199
228
  return null;
200
229
  const doc = rowToDoc(row);
230
+ if (!this.isFolderNameVisible(doc.folder))
231
+ return null;
201
232
  const remote = this.remoteFor(doc.folder);
202
233
  if (!remote || !this.credentialsBaseDir)
203
234
  return doc;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Decodes the `username` claim out of a folderfoo JWT's payload — no
3
+ * signature verification, since this only reads a claim from a token the
4
+ * server already trusts enough to use against folderfoo itself (see
5
+ * credentials.ts). folderfoo's signToken() payload shape is
6
+ * {username, fullname, origIat, iat, exp} (folderfoo's own server.js) — no
7
+ * sub/id/email/tenantId claim exists, so username is the stable identity key.
8
+ */
9
+ export function decodeUsername(jwt) {
10
+ const payloadSegment = jwt.split('.')[1];
11
+ if (!payloadSegment)
12
+ throw new Error('malformed folderfoo token: missing payload segment');
13
+ const payload = JSON.parse(Buffer.from(payloadSegment, 'base64').toString('utf-8'));
14
+ if (!payload.username)
15
+ throw new Error('malformed folderfoo token: missing username claim');
16
+ return payload.username;
17
+ }
18
+ /**
19
+ * Process-wide "who's currently logged in" tracker. `mode` is fixed for the
20
+ * process's lifetime (only changes via a restart with a different
21
+ * --folderfoo-mode flag, per the settled design) - only `username` is
22
+ * mutable, driven by the browser's login/logout signals via
23
+ * /api/folderfoo/login and /api/folderfoo/logout. There is no per-tab or
24
+ * per-MCP-client identity: multiple browser tabs and MCP clients share this
25
+ * one process-wide value, and the most recent login anywhere wins for all of
26
+ * them (settled design decision — not a per-connection scope).
27
+ */
28
+ export class IdentityTracker {
29
+ mode;
30
+ username = null;
31
+ listeners = new Set();
32
+ constructor(mode) {
33
+ this.mode = mode;
34
+ }
35
+ current() {
36
+ return { mode: this.mode, username: this.username };
37
+ }
38
+ setUsername(username) {
39
+ if (this.username === username)
40
+ return;
41
+ this.username = username;
42
+ this.emit();
43
+ }
44
+ clearUsername() {
45
+ if (this.username === null)
46
+ return;
47
+ this.username = null;
48
+ this.emit();
49
+ }
50
+ onIdentityChange(cb) {
51
+ this.listeners.add(cb);
52
+ return () => this.listeners.delete(cb);
53
+ }
54
+ emit() {
55
+ const identity = this.current();
56
+ for (const cb of this.listeners)
57
+ cb(identity);
58
+ }
59
+ }
60
+ /** True when `folder` was connected under the identity currently logged in. */
61
+ export function matchesCurrentIdentity(folder, identity) {
62
+ return identity.username !== null && folder.mode === identity.mode && folder.username === identity.username;
63
+ }
64
+ /** Local (non-remote) folders are always visible; a remote folder only when it matches the current identity. */
65
+ export function isFolderVisible(remote, identity) {
66
+ return remote === undefined || matchesCurrentIdentity(remote, identity);
67
+ }
@@ -20,6 +20,7 @@ import { registerUiTool } from './web/ui-tool.js';
20
20
  import { registerMemoryChannelTools } from './channels/tools.js';
21
21
  import { startChannelSweep } from './channels/store.js';
22
22
  import { startRemotePolling } from './remote/remote-sync.js';
23
+ import { IdentityTracker } from './remote/identity.js';
23
24
  // server.ts is rebuilt from `buildMcpServer()` on every /mcp request (see below),
24
25
  // so tool schemas (which conditionally include `folder` based on folder count) always
25
26
  // reflect the current folders — no restart needed after an add/remove-folder call.
@@ -36,14 +37,15 @@ const builtinSkillsDir = path.join(__dirname, 'skills', 'builtin');
36
37
  const PORT = process.env.PORT ? Number(process.env.PORT) : 8767;
37
38
  const config = loadConfig();
38
39
  const db = openCache(config.cacheDbPath);
40
+ const identity = new IdentityTracker(config.folderfooMode);
39
41
  const skillSpec = skillSyncSpec([{ name: 'builtin', path: builtinSkillsDir }, ...config.skillFolders]);
40
42
  const memorySpec = memorySyncSpec(config.memoryFolders);
41
43
  initialScan(db, skillSpec);
42
44
  initialScan(db, memorySpec);
43
45
  const skillWatcher = watchSources(db, skillSpec);
44
46
  const memoryWatcher = watchSources(db, memorySpec);
45
- const skillRepo = new SkillRepository(db, [{ name: 'builtin', path: builtinSkillsDir }, ...config.skillFolders], config.remoteSkillFolders, config.baseDir);
46
- const memoryRepo = new MemoryRepository(db, config.memoryFolders, config.remoteMemoryFolders, config.baseDir);
47
+ const skillRepo = new SkillRepository(db, [{ name: 'builtin', path: builtinSkillsDir }, ...config.skillFolders], config.remoteSkillFolders, config.baseDir, identity);
48
+ const memoryRepo = new MemoryRepository(db, config.memoryFolders, config.remoteMemoryFolders, config.baseDir, identity);
47
49
  skillRepo.setWatcher(skillWatcher);
48
50
  memoryRepo.setWatcher(memoryWatcher);
49
51
  const attachmentRepo = new AttachmentRepository(memoryRepo, skillRepo);
@@ -77,7 +79,7 @@ function buildMcpServer() {
77
79
  startChannelSweep((name) => console.error(`[memory-bucket] sweeping idle memory channel: ${name}`));
78
80
  const app = express();
79
81
  app.use(express.json());
80
- app.use(buildWebRouter(db, config, skillRepo, memoryRepo, skillSpec, memorySpec, { skill: remoteSkillPoller, memory: remoteMemoryPoller }));
82
+ app.use(buildWebRouter(db, config, skillRepo, memoryRepo, skillSpec, memorySpec, identity, { skill: remoteSkillPoller, memory: remoteMemoryPoller }));
81
83
  app.use(express.static(path.join(packageRoot, 'dist', 'client')));
82
84
  app.post('/mcp', async (req, res) => {
83
85
  const server = buildMcpServer();
@@ -8,6 +8,7 @@ import { upsertFile, removeFile, scanSingleFolder, unregisterFolder, skillSyncSp
8
8
  import { SearchQueryError, sanitizeFtsQuery } from '../store/search.js';
9
9
  import { applyBodyEdits } from '../shared/body-edits.js';
10
10
  import { readFile as readRemoteFile, writeFile as writeRemoteFile, joinRemoteFolderPath } from '../remote/folderfoo-client.js';
11
+ import { isFolderVisible } from '../remote/identity.js';
11
12
  function rowToDoc(row) {
12
13
  return {
13
14
  name: row.id,
@@ -29,20 +30,38 @@ export class SkillRepository {
29
30
  folders;
30
31
  remoteFolders;
31
32
  credentialsBaseDir;
33
+ identity;
32
34
  syncSpec;
33
35
  watcher;
34
36
  /** `folders[0]` is always the builtin skills dir — never exposed for create()/removal. */
35
- constructor(db, folders, remoteFolders = [], credentialsBaseDir) {
37
+ constructor(db, folders, remoteFolders = [], credentialsBaseDir, identity) {
36
38
  this.db = db;
37
39
  this.folders = folders;
38
40
  this.remoteFolders = remoteFolders;
39
41
  this.credentialsBaseDir = credentialsBaseDir;
42
+ this.identity = identity;
40
43
  this.syncSpec = skillSyncSpec(folders);
41
44
  }
42
45
  /** The RemoteFolder a NamedFolder name resolves to, or undefined for a local (non-remote) folder. */
43
46
  remoteFor(folderName) {
44
47
  return this.remoteFolders.find((f) => f.name === folderName);
45
48
  }
49
+ /** Whether `folderName` should be visible right now — always true for local folders; for a remote folder, only when it matches the current login (see identity.ts). No identity tracker configured means folderfoo integration is off entirely, so nothing remote is ever visible. */
50
+ isFolderNameVisible(folderName) {
51
+ const remote = this.remoteFor(folderName);
52
+ if (!remote)
53
+ return true;
54
+ if (!this.identity)
55
+ return false;
56
+ return isFolderVisible(remote, this.identity.current());
57
+ }
58
+ /** Names of every remote folder NOT matching the current identity — used to exclude their rows from list/search SQL. */
59
+ hiddenFolderNames() {
60
+ if (!this.identity)
61
+ return this.remoteFolders.map((f) => f.name);
62
+ const identity = this.identity.current();
63
+ return this.remoteFolders.filter((f) => !isFolderVisible(f, identity)).map((f) => f.name);
64
+ }
46
65
  /** Pushes the just-written SKILL.md at filePath to folderfoo, if targetFolderName resolves to a remote source. No-op for a local folder. See get()'s comment on the folderPath/name split for a skill's directory-per-skill layout. */
47
66
  async pushToRemoteIfNeeded(targetFolderName, filePath) {
48
67
  const remote = this.remoteFor(targetFolderName);
@@ -64,9 +83,11 @@ export class SkillRepository {
64
83
  listRemoteFolders() {
65
84
  return [...this.remoteFolders];
66
85
  }
67
- /** Same as listFolders(), but tags each entry with whether it's a remote (folderfoo) source — for the web UI's folder list, e.g. to render remote folders in a distinct color. */
86
+ /** Same as listFolders(), but tags each entry with whether it's a remote (folderfoo) source — for the web UI's folder list, e.g. to render remote folders in a distinct color. Excludes remote folders that don't match the current login (see identity.ts). */
68
87
  listFoldersWithRemoteInfo() {
69
- return this.listFolders().map((f) => ({ ...f, remote: !!this.remoteFor(f.name) }));
88
+ return this.listFolders()
89
+ .filter((f) => this.isFolderNameVisible(f.name))
90
+ .map((f) => ({ ...f, remote: !!this.remoteFor(f.name) }));
70
91
  }
71
92
  resolveFolder(folderName) {
72
93
  const userFolders = this.listFolders();
@@ -131,6 +152,11 @@ export class SkillRepository {
131
152
  if (!opts.includePaused) {
132
153
  conditions.push('paused = 0');
133
154
  }
155
+ const hidden = this.hiddenFolderNames();
156
+ if (hidden.length > 0) {
157
+ conditions.push(`folder NOT IN (${hidden.map(() => '?').join(', ')})`);
158
+ params.push(...hidden);
159
+ }
134
160
  const where = conditions.length ? ` WHERE ${conditions.join(' AND ')}` : '';
135
161
  const rows = this.db
136
162
  .prepare(`SELECT id, description, owner, status, tags, trigger_phrases, folder, paused FROM skills${where}`)
@@ -182,6 +208,11 @@ export class SkillRepository {
182
208
  if (!includePaused) {
183
209
  conditions.push('s.paused = 0');
184
210
  }
211
+ const hidden = this.hiddenFolderNames();
212
+ if (hidden.length > 0) {
213
+ conditions.push(`s.folder NOT IN (${hidden.map(() => '?').join(', ')})`);
214
+ params.push(...hidden);
215
+ }
185
216
  params.push(limit, offset);
186
217
  try {
187
218
  const rows = this.db
@@ -211,6 +242,8 @@ export class SkillRepository {
211
242
  if (!row)
212
243
  return null;
213
244
  const doc = rowToDoc(row);
245
+ if (!this.isFolderNameVisible(doc.folder))
246
+ return null;
214
247
  const remote = this.remoteFor(doc.folder);
215
248
  if (!remote || !this.credentialsBaseDir)
216
249
  return doc;
@@ -12,6 +12,7 @@ import { attachmentsDirFor, guessMimeType } from '../attachments/storage.js';
12
12
  import { listFolders as listFolderfooFolders } from '../remote/folderfoo-client.js';
13
13
  import { setCredential } from '../remote/credentials.js';
14
14
  import { pollOne } from '../remote/remote-sync.js';
15
+ import { decodeUsername } from '../remote/identity.js';
15
16
  import { getChannel, listChannels } from '../channels/store.js';
16
17
  function asArray(v) {
17
18
  if (v === undefined)
@@ -272,7 +273,7 @@ function buildHealth(db) {
272
273
  .map((m) => m.id);
273
274
  return { danglingExtends, danglingRelatedTo, emptyTriggerPhrases, staleActiveMemoryDocs };
274
275
  }
275
- export function buildWebRouter(db, config, skillRepo, memoryRepo, skillSpec, memorySpec, remotePollers) {
276
+ export function buildWebRouter(db, config, skillRepo, memoryRepo, skillSpec, memorySpec, identity, remotePollers) {
276
277
  const router = express.Router();
277
278
  // Resyncs every remote source FIRST (force: true - always does real work,
278
279
  // including reconcileDeletions, regardless of the watermark check), so a
@@ -742,8 +743,40 @@ export function buildWebRouter(db, config, skillRepo, memoryRepo, skillSpec, mem
742
743
  return;
743
744
  }
744
745
  setCredential(config.baseDir, server, token);
746
+ // Updates the process-wide current identity (see identity.ts) - the most
747
+ // recent login from ANY browser tab becomes "current" for the whole
748
+ // process, including MCP tool calls. Logging in doesn't itself change
749
+ // which entries are stamped with this identity (that only happens at
750
+ // connect-time, see /api/remote-folders below) - it just changes which
751
+ // already-stamped entries become visible.
752
+ identity.setUsername(decodeUsername(token));
745
753
  res.json({ connected: true, server });
746
754
  });
755
+ // Signals that the browser's folderfoo login session just ended (the page
756
+ // detects folderfoo's own auth-change event going to logged-out) - clears
757
+ // the process-wide current identity, which hides every remote folder
758
+ // (everywhere: web UI and MCP tool results alike) until a new login
759
+ // stamps a fresh identity via the route above. No server/token body: this
760
+ // is "the browser lost its session," not scoped to one server.
761
+ router.post('/api/folderfoo/logout', (_req, res) => {
762
+ identity.clearUsername();
763
+ res.json({ connected: false });
764
+ });
765
+ // Server-sent-events stream of the current identity, so an open browser
766
+ // tab's folder list updates the instant a login/logout happens anywhere
767
+ // (this tab or another one) - no polling, no page reload. Sends the
768
+ // current value immediately on connect, then again on every change.
769
+ router.get('/api/folderfoo/identity-stream', (req, res) => {
770
+ res.writeHead(200, {
771
+ 'Content-Type': 'text/event-stream',
772
+ 'Cache-Control': 'no-cache',
773
+ Connection: 'keep-alive',
774
+ });
775
+ const send = (current) => res.write(`data: ${JSON.stringify(current)}\n\n`);
776
+ send(identity.current());
777
+ const unsubscribe = identity.onIdentityChange(send);
778
+ req.on('close', unsubscribe);
779
+ });
747
780
  // Lists the caller's own folderfoo folders for the "connect a folder"
748
781
  // picker, once already logged in to `server` (via the route above).
749
782
  router.get('/api/folderfoo/folders', async (req, res) => {
@@ -782,13 +815,21 @@ export function buildWebRouter(db, config, skillRepo, memoryRepo, skillSpec, mem
782
815
  res.status(400).json({ error: 'could not derive a valid folder name — provide one explicitly' });
783
816
  return;
784
817
  }
818
+ // A folder can only be connected while logged in — the identity it's
819
+ // stamped with (see identity.ts) is exactly the one that just made the
820
+ // "connect a folder" picker possible in the first place.
821
+ const current = identity.current();
822
+ if (!current.username) {
823
+ res.status(401).json({ error: 'not logged in to folderfoo' });
824
+ return;
825
+ }
785
826
  const repo = kind === 'skill' ? skillRepo : memoryRepo;
786
827
  const spec = kind === 'skill' ? skillSpec : memorySpec;
787
- const mirrorDir = mirrorDirFor(config.baseDir, folderName);
788
- const remote = { name: folderName, server, tenantId, folderPath, mirrorDir };
828
+ const mirrorDir = mirrorDirFor(config.baseDir, current.mode, current.username, folderName);
829
+ const remote = { name: folderName, server, tenantId, folderPath, mirrorDir, mode: current.mode, username: current.username };
789
830
  try {
790
831
  repo.registerRemoteFolder(remote);
791
- saveRemoteFolder(config, kind, { name: folderName, server, tenantId, folderPath });
832
+ saveRemoteFolder(config, kind, { name: folderName, server, tenantId, folderPath, mode: current.mode, username: current.username });
792
833
  await pollOne(db, spec, remote, config.baseDir);
793
834
  res.json({ name: folderName, server, tenantId, folderPath, kind });
794
835
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-memory-bucket",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
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": {