bantamkit-mcp 0.34.2 → 0.35.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/README.md CHANGED
@@ -5,7 +5,7 @@ store, JSON Schema validation and shift-work tools. Install it from **npm** with
5
5
  command, connect it to **Claude Code**, **Claude Desktop**, **Cursor** or **GitHub Copilot in VS
6
6
  Code**, and every later launch starts **offline**. No Python, `pip`, `uv`, `pipx` or venv.
7
7
 
8
- It serves the same twelve tools, `bantamkit_status` prompt and two resource templates as the
8
+ It serves the same fourteen tools, `bantamkit_status` prompt and two resource templates as the
9
9
  Python server on **PyPI** (`pip install "bantamkit[mcp]"`), and reads and writes the same memory
10
10
  store. The two are compared frame by frame: 8,130 conformance cases on 2026-09-15, with every
11
11
  intentional difference written down as a ruling.
@@ -30,7 +30,7 @@ intentional difference written down as a ruling.
30
30
  | [The operator CLI: `bantamkit-memory`](#the-operator-cli-bantamkit-memory) | The second bin: status, lint, compact, archive, restore |
31
31
  | [Memory stores are layered by default](#memory-stores-are-layered-by-default) | Why not to add `--store` by reflex |
32
32
  | [The asset pack](#the-asset-pack) | What ships in `assets/` and how it is found |
33
- | [Sharing a store with the Python server](#sharing-a-store-with-the-python-server) | Concurrent writes, and one hand-edit to avoid |
33
+ | [Where the Python and Node servers differ](#where-the-python-and-node-servers-differ) | One store; concurrent writes, one hand-edit to avoid |
34
34
  | [Development](#development) | Build, test, conformance |
35
35
  | [Measurements and history](#measurements-and-history) | Where the measured numbers behind this page live |
36
36
 
@@ -50,9 +50,9 @@ npx -y bantamkit-mcp@latest --install claude # or claude-desktop, copilot, cur
50
50
  From 0.34.0 this runs `npm install --prefix ~/.bantamkit/mcp` once (Windows:
51
51
  `%USERPROFILE%\.bantamkit\mcp`, not measured) and records
52
52
  `<the node that ran it> ~/.bantamkit/mcp/node_modules/bantamkit-mcp/dist/cli.js`, both absolute.
53
- No later launch needs npx, the registry or your PATH. Measured from the published 0.34.0:
54
- install exit 0 in 8.14 s; the recorded command then served 12 tools in 0.09 s with the network
55
- cut.
53
+ No later launch needs npx, the registry or your PATH. Measured from the published 0.34.0
54
+ (served-tools: dated — the surface was twelve then): install exit 0 in 8.14 s; the recorded
55
+ command then served 12 tools in 0.09 s with the network cut.
56
56
 
57
57
  - **`@latest` matters:** without it npx may reuse an older cached resolve.
58
58
  - **It skips npm when the kept install already reports this version or a newer one.** It never
@@ -235,10 +235,15 @@ startup. In Claude Code: `/mcp` → reconnect. In Claude Desktop: a full restart
235
235
  **Verify with `bantamkit_status`, not the install log.** It reports the version and the
236
236
  `build_id` of *the code that is answering you*:
237
237
 
238
+ *(A capture from 0.30.0. The version and build-digest bytes are left as a dated transcript
239
+ rather than hand-edited to a number nobody ran; **line 3 is the exception and tracks the
240
+ surface**, for the reason `docs/status.md` gives — the tool count is a live claim this repo
241
+ gates against a running server, and a marker would only hide it from that gate.)*
242
+
238
243
  ```
239
244
  bantamkit Active 🟢
240
245
  version 0.30.0, build sha256:4441619d…
241
- serving 12 tools, 1 prompt, 2 resource templates
246
+ serving 14 tools, 1 prompt, 2 resource templates
242
247
  ```
243
248
 
244
249
  A version that moved and a `build_id` that did not means you are reading a config, not a process.
@@ -261,7 +266,7 @@ dependency in a temp directory that will be deleted, and `npm update` cannot hel
261
266
  | Two machines with one config run different builds | [Silent version float](#silent-version-float): compare `build_id` |
262
267
  | `bantamkit_status` still shows the old version after an update | The host has not reconnected; restart the server |
263
268
  | Recall finds nothing, or the wrong store | Set `BANTAMKIT_MEMORY_DIR`; the reply names the store it searched |
264
- | `malformed fact file …` | A hand-edited fact has a bare non-string field; see [Sharing a store](#sharing-a-store-with-the-python-server) |
269
+ | `malformed fact file …` | A hand-edited fact has a bare non-string field; see [Where the Python and Node servers differ](#where-the-python-and-node-servers-differ) |
265
270
  | `AssetNotFound: no assets directory found; set BANTAMKIT_ASSETS` | See [The asset pack](#the-asset-pack) |
266
271
 
267
272
  ## Configuration
@@ -381,9 +386,12 @@ the pack ships whole — 91 files / 238,751 bytes on 2026-09-15 — or `assets_d
381
386
  3. `<repo>/assets/` — two levels above `dist/`, for a dev checkout with nothing vendored.
382
387
  4. Otherwise `AssetNotFound: no assets directory found; set BANTAMKIT_ASSETS`.
383
388
 
384
- ## Sharing a store with the Python server
389
+ ## Where the Python and Node servers differ
385
390
 
386
- Both servers read and write the same memory store; byte-compatibility is the product.
391
+ Both servers read and write the same on-disk store; byte-compatibility is the product. Every
392
+ feature lands in both in one change, and a conformance case compares their answers. Differences
393
+ are deliberate and each is ruled in the
394
+ [divergence table](https://github.com/Ink01101011/bantamkit/blob/main/docs/porting.md#where-the-two-runtimes-deliberately-differ):
387
395
 
388
396
  - **Concurrent writes are safe for the memory store and lossy for the checkpoint, in both
389
397
  runtimes.** `index.md` is derived from `facts/`, so the last writer re-enumerates everything
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "shiftwork_plan",
3
+ "description": "Shift-work plan: read-only batch view of a checkpoint — which units its `depends_on` graph permits to run in parallel. Answers `{\"result\": \"plan\", \"batches\", \"ready\", \"sequence\", \"width\", \"cursor\"}`: `ready` is `batches[0]`, the units dispatchable right now, and `width` is the largest batch length, the widest fan-out. Units with status `done` or `dropped` are satisfied — dropped from the graph, and edges pointing at them treated as already resolved; `todo`, `in_progress` and `blocked` stay in it. Every unit has priority 0, because the checkpoint schema has no priority field, so order inside a batch is `plan.units` order. Same refusal sentences as `work_plan` for a duplicate id, an unknown dependency or a cycle, and the same refusals `shiftwork_status` gives for a checkpoint it cannot read. It reports what the dependency graph permits; it does not move the cursor, which is echoed unchanged so the single-pointer contract and the batch view can be read side by side. An orchestrator stays responsible for what it actually dispatches: `depends_on` encodes logical order, not file contention. Never mutates.",
4
+ "surfaces": [
5
+ "mcp"
6
+ ],
7
+ "parameters": {
8
+ "properties": {
9
+ "checkpoint": {
10
+ "title": "Checkpoint",
11
+ "type": "string"
12
+ }
13
+ },
14
+ "required": [
15
+ "checkpoint"
16
+ ],
17
+ "type": "object",
18
+ "title": "shiftwork_planArguments"
19
+ },
20
+ "output_schema": {
21
+ "type": "object",
22
+ "additionalProperties": true,
23
+ "title": "shiftwork_planDictOutput"
24
+ }
25
+ }
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "work_plan",
3
+ "description": "Work plan: turn a dependency graph into the batches that may run in parallel. Takes `nodes`, each `{id, depends_on, priority}` — `priority` defaults to 0 — and answers `{\"result\": \"plan\", \"batches\", \"sequence\", \"width\"}`: batch k holds every node whose dependencies all appear in batches below k, `sequence` is those batches flattened in order, and `width` is the largest batch length — the widest fan-out, which is the number an orchestrator needs to decide whether it can afford the batch. Order inside a batch is priority descending, then the order the nodes were given; both halves are contract, because determinism inside a batch is what two implementations are held to. Empty `nodes` is an ANSWER, not a refusal: no batches, no sequence, width 0. Three refusals, checked in this order and one sentence each: `duplicate node id <id>`, `node <id> depends on <dep>, which no node declares`, and `the graph has a cycle: <a> -> <b> -> <a>`. It reports what the graph permits and decides nothing about what may actually be dispatched together — `depends_on` encodes logical order, not file contention, so a batch this tool calls parallel may still hold work that writes one file. Pure computation over the nodes it was handed: it opens no file and takes no path. Never mutates.",
4
+ "surfaces": [
5
+ "mcp"
6
+ ],
7
+ "parameters": {
8
+ "properties": {
9
+ "nodes": {
10
+ "title": "Nodes",
11
+ "type": "array",
12
+ "items": {
13
+ "type": "object",
14
+ "properties": {
15
+ "id": {
16
+ "type": "string",
17
+ "description": "The node's identifier, unique across `nodes`"
18
+ },
19
+ "depends_on": {
20
+ "type": "array",
21
+ "items": {
22
+ "type": "string"
23
+ },
24
+ "description": "Ids this node waits for; every one of them must be declared by some node"
25
+ },
26
+ "priority": {
27
+ "type": "integer",
28
+ "description": "Tie-break inside a batch, descending. Optional; 0 when absent"
29
+ }
30
+ },
31
+ "required": [
32
+ "id",
33
+ "depends_on"
34
+ ]
35
+ }
36
+ }
37
+ },
38
+ "required": [
39
+ "nodes"
40
+ ],
41
+ "type": "object",
42
+ "title": "work_planArguments"
43
+ },
44
+ "output_schema": {
45
+ "type": "object",
46
+ "additionalProperties": true,
47
+ "title": "work_planDictOutput"
48
+ }
49
+ }
package/dist/eventlog.js CHANGED
@@ -55,8 +55,8 @@
55
55
  * notice — the failure mode a diagnostic can least afford.
56
56
  */
57
57
  import { appendFileSync, mkdirSync, renameSync, statSync } from 'node:fs';
58
- import { cmpCodepoint, osErrorClassName, pyExists, PyOSError, pyJoin, pyName, pyParent, pyParents } from './memory/pyfs.js';
59
- import { ensureBantamkitGitignore } from './memory/store.js';
58
+ import { cmpCodepoint, osErrorClassName, pyExists, PyOSError, pyJoin, pyName, pyParent } from './memory/pyfs.js';
59
+ import { bantamkitDirFor, ensureBantamkitGitignore } from './memory/store.js';
60
60
  /**
61
61
  * Environment switch. Unset or `off`/`0`/`false`/`no`/empty -> disabled. `on`/`1`/`true`/
62
62
  * `yes` -> the default file inside the memory store. Anything else is taken as the literal
@@ -254,13 +254,12 @@ export class EventLog {
254
254
  // USER RULING #2 (J51-8b): the ignore file is written only when THIS call is what
255
255
  // creates `.bantamkit`, so existence has to be checked BEFORE the mkdir below -- after
256
256
  // it, the directory unconditionally exists and the question is unanswerable.
257
- let bantamkitDir = null;
258
- for (const parent of pyParents(path)) {
259
- if (pyName(parent) === '.bantamkit') {
260
- bantamkitDir = parent;
261
- break;
262
- }
263
- }
257
+ //
258
+ // J54-3: the walk moved into `bantamkitDirFor` beside the writer it feeds, so the two
259
+ // creators in this runtime ask the same question in the same words rather than keeping a
260
+ // copy each. The answer here is unchanged -- a log file is never itself named
261
+ // `.bantamkit`, so self-or-ancestors and ancestors give the same directory.
262
+ const bantamkitDir = bantamkitDirFor(path);
264
263
  const bantamkitDirExistedBefore = bantamkitDir !== null && pyExists(bantamkitDir);
265
264
  // `pyParent` and not `node:path`'s `dirname`: this module is reached from a Windows
266
265
  // host through the same code, and the reference's `Path(...).parent` is the spelling
@@ -37,6 +37,7 @@ import { homedir } from 'node:os';
37
37
  import { dirname, join } from 'node:path';
38
38
  import { fileURLToPath } from 'node:url';
39
39
  import { currentInstall, Undetermined } from './mcp/identity.js';
40
+ import { bantamkitDirFor, ensureBantamkitGitignore } from './memory/store.js';
40
41
  import { compareVersions, keptCli, keptManifest, keptPrefix, PACKAGE, runInstaller, shlexJoin } from './npminstall.js';
41
42
  import { dumpJson, fromJs } from './pyjson.js';
42
43
  export { keptCli, keptPrefix };
@@ -319,7 +320,28 @@ export function thisCommand(options = {}) {
319
320
  }
320
321
  const command = ['npm', 'install', '--prefix', prefix, `${PACKAGE}@${version}`];
321
322
  const rendered = shlexJoin(command);
323
+ // THE THIRD `.bantamkit` CREATOR (J54-3), and the one that creates the most of it: npm makes
324
+ // a missing `--prefix` itself, so this line — not `MemoryStore`, not `EventLog` — is what
325
+ // brings `<homedir>/.bantamkit` into existence on a machine that has never saved a memory.
326
+ // Measured on the machine this was found on: 28 MB of install tree under a `~/.bantamkit`
327
+ // with no `.gitignore`, every byte of it untracked in a `$HOME` that is a git repository.
328
+ //
329
+ // Checked BEFORE npm runs, per user ruling #2, for the reason both other creators check it
330
+ // before their own mkdir: afterwards the directory exists either way and the question
331
+ // "did THIS call create it" is unanswerable. A `~/.bantamkit` that was already there —
332
+ // holding an older kept install, a memory store, or an ignore file the operator deleted on
333
+ // purpose — is left exactly as it is.
334
+ const bantamkitDir = bantamkitDirFor(prefix);
335
+ const bantamkitDirExistedBefore = bantamkitDir !== null && existsSync(bantamkitDir);
322
336
  const [code, output] = (options.installer ?? runInstaller)(command);
337
+ // BEFORE THE REFUSALS, AND THAT ORDER IS MEASURED, not tidy. A `npm install --prefix` that
338
+ // FAILS still leaves the prefix behind: measured 2026-09-18 with `npm_config_offline=true`
339
+ // against an empty cache — exit 1, and `<home>/.bantamkit/mcp` there afterwards. A refusal
340
+ // that returned first would leave exactly the directory this closes over: made by bantamkit's
341
+ // own command, holding nothing anybody asked for, and visible to git. The helper never
342
+ // creates `bantamkitDir` itself, so an installer that made nothing writes nothing here.
343
+ if (bantamkitDir !== null)
344
+ ensureBantamkitGitignore(bantamkitDir, !bantamkitDirExistedBefore);
323
345
  const refused = `could not make the kept install at ${prefix}, so no host configuration was changed: ${rendered} exited ${code}`;
324
346
  if (code !== 0)
325
347
  throw new InstallError(`${refused}: ${lastLine(output)}`);
@@ -33,6 +33,14 @@ export declare const BANTAMKIT_GITIGNORE_TEXT: string;
33
33
  * a `.bantamkit` that git can see (or ignore one it should not), which is the whole bug this
34
34
  * closes.
35
35
  *
36
+ * THE CREATORS, ENUMERATED (J54-3), because "anything else" is how one got missed: in THIS
37
+ * runtime there are exactly three — `MemoryStore.ensureDirs`, `EventLog`'s append-time
38
+ * `mkdirSync`, and `hostinstall.thisCommand`, where `--install` from an `npx` cache has npm
39
+ * create `<homedir>/.bantamkit/mcp` (npm makes a missing `--prefix` itself). The first two ask
40
+ * `bantamkitDirFor` which directory the decision is about rather than assuming the root's
41
+ * parent is it; the third is the one the reference does not have at all, because the offline
42
+ * install is Node-only by user ruling. `docs/porting.md` carries the row.
43
+ *
36
44
  * NEVER REWRITES. A `.gitignore` that already exists -- whatever its bytes -- is left
37
45
  * exactly as it is, kept as a second guard even when `created` is `true`: an operator who
38
46
  * deleted it to commit the directory keeps that decision, and this never diffs its own
@@ -48,6 +56,27 @@ export declare const BANTAMKIT_GITIGNORE_TEXT: string;
48
56
  * bring `.bantamkit` into existence early.
49
57
  */
50
58
  export declare function ensureBantamkitGitignore(directory: string, created: boolean): void;
59
+ /**
60
+ * The `.bantamkit` directory a recursive mkdir of `path` would create or fill, or `null` when
61
+ * there is none: `path` itself if it is named `.bantamkit`, otherwise its nearest ancestor
62
+ * that is. `runtime-py/src/bantamkit/memory/store.py`'s `bantamkit_dir_for` is the reference.
63
+ *
64
+ * WHY A WALK AND NOT `pyParent(path)` (J54-3). Every creator has to hand
65
+ * `ensureBantamkitGitignore` the directory the decision is ABOUT, and the parent is the right
66
+ * answer for exactly one shape of path, `<x>/.bantamkit/memory`. A store rooted AT the
67
+ * `.bantamkit` directory (`--store ~/.bantamkit`) or nested deeper under it creates a
68
+ * `.bantamkit` whose gitignore decision was taken about the wrong directory — so no decision
69
+ * was taken at all, and git could see the whole tree. MEASURED, not hypothetical: `~/.bantamkit`
70
+ * on the machine this was found on holds an empty `facts/` and `archive/` beside `memory/`,
71
+ * left by a store once rooted at it, and no `.gitignore`.
72
+ *
73
+ * NEAREST AND NOT OUTERMOST, which is the choice `EventLog`'s append already made and this
74
+ * function now carries for both creators.
75
+ *
76
+ * READ-ONLY. It looks at names, never at the filesystem, so it is equally valid before or after
77
+ * the mkdir — but `created` is answered by checking THIS path's existence BEFORE.
78
+ */
79
+ export declare function bantamkitDirFor(path: string): string | null;
51
80
  /** `sorted(VALID_TYPES)` is what the error message interpolates, so the order is load-bearing. */
52
81
  export declare const VALID_TYPES: readonly ["feedback", "project", "reference", "user"];
53
82
  /**
@@ -42,7 +42,7 @@ import { basename } from 'node:path';
42
42
  import { BantamError } from '../errors.js';
43
43
  import { formatFact, parseFrontmatter, pySplit, pyStrip, todayLocal } from './factfile.js';
44
44
  import { PyScalar } from './pyyaml.js';
45
- import { asPyOSError, cmpCodepoint, matchesMd, pyExists, pyJoin, pyLexists, pyMkdirParents, pyMtimeDate, pyName, pyParent, pyReadText, pyReplace, pyScandirNames, pySuffix, pyUnlink, pyWithSuffix, pyWriteText, PyOSError, sortedPathNames, } from './pyfs.js';
45
+ import { asPyOSError, cmpCodepoint, matchesMd, pyExists, pyJoin, pyLexists, pyMkdirParents, pyMtimeDate, pyName, pyParent, pyParents, pyReadText, pyReplace, pyScandirNames, pySuffix, pyUnlink, pyWithSuffix, pyWriteText, PyOSError, sortedPathNames, } from './pyfs.js';
46
46
  /**
47
47
  * The exact bytes written into a `.bantamkit/.gitignore` the first time such a directory is
48
48
  * brought into existence (user ruling 2026-09-15, `.shiftwork/notes-job51/P0-probes.md`).
@@ -76,6 +76,14 @@ export const BANTAMKIT_GITIGNORE_TEXT = '# Created by bantamkit: this directory
76
76
  * a `.bantamkit` that git can see (or ignore one it should not), which is the whole bug this
77
77
  * closes.
78
78
  *
79
+ * THE CREATORS, ENUMERATED (J54-3), because "anything else" is how one got missed: in THIS
80
+ * runtime there are exactly three — `MemoryStore.ensureDirs`, `EventLog`'s append-time
81
+ * `mkdirSync`, and `hostinstall.thisCommand`, where `--install` from an `npx` cache has npm
82
+ * create `<homedir>/.bantamkit/mcp` (npm makes a missing `--prefix` itself). The first two ask
83
+ * `bantamkitDirFor` which directory the decision is about rather than assuming the root's
84
+ * parent is it; the third is the one the reference does not have at all, because the offline
85
+ * install is Node-only by user ruling. `docs/porting.md` carries the row.
86
+ *
79
87
  * NEVER REWRITES. A `.gitignore` that already exists -- whatever its bytes -- is left
80
88
  * exactly as it is, kept as a second guard even when `created` is `true`: an operator who
81
89
  * deleted it to commit the directory keeps that decision, and this never diffs its own
@@ -103,6 +111,33 @@ export function ensureBantamkitGitignore(directory, created) {
103
111
  /* best-effort: a missing ignore file is not worth a lost fact */
104
112
  }
105
113
  }
114
+ /**
115
+ * The `.bantamkit` directory a recursive mkdir of `path` would create or fill, or `null` when
116
+ * there is none: `path` itself if it is named `.bantamkit`, otherwise its nearest ancestor
117
+ * that is. `runtime-py/src/bantamkit/memory/store.py`'s `bantamkit_dir_for` is the reference.
118
+ *
119
+ * WHY A WALK AND NOT `pyParent(path)` (J54-3). Every creator has to hand
120
+ * `ensureBantamkitGitignore` the directory the decision is ABOUT, and the parent is the right
121
+ * answer for exactly one shape of path, `<x>/.bantamkit/memory`. A store rooted AT the
122
+ * `.bantamkit` directory (`--store ~/.bantamkit`) or nested deeper under it creates a
123
+ * `.bantamkit` whose gitignore decision was taken about the wrong directory — so no decision
124
+ * was taken at all, and git could see the whole tree. MEASURED, not hypothetical: `~/.bantamkit`
125
+ * on the machine this was found on holds an empty `facts/` and `archive/` beside `memory/`,
126
+ * left by a store once rooted at it, and no `.gitignore`.
127
+ *
128
+ * NEAREST AND NOT OUTERMOST, which is the choice `EventLog`'s append already made and this
129
+ * function now carries for both creators.
130
+ *
131
+ * READ-ONLY. It looks at names, never at the filesystem, so it is equally valid before or after
132
+ * the mkdir — but `created` is answered by checking THIS path's existence BEFORE.
133
+ */
134
+ export function bantamkitDirFor(path) {
135
+ for (const candidate of [path, ...pyParents(path)]) {
136
+ if (pyName(candidate) === '.bantamkit')
137
+ return candidate;
138
+ }
139
+ return null;
140
+ }
106
141
  /** `sorted(VALID_TYPES)` is what the error message interpolates, so the order is load-bearing. */
107
142
  export const VALID_TYPES = ['feedback', 'project', 'reference', 'user'];
108
143
  /**
@@ -598,8 +633,13 @@ export class MemoryStore {
598
633
  // Checked BEFORE the mkdir below, per user ruling #2: this is the one moment that can
599
634
  // tell whether `.bantamkit` already existed. After the mkdir it unconditionally exists
600
635
  // either way, so the answer has to be captured now or not at all.
601
- const bantamkitDir = pyParent(this.root);
602
- const bantamkitDirExistedBefore = pyExists(bantamkitDir);
636
+ //
637
+ // J54-3: `bantamkitDirFor`, not `pyParent`. The mkdirs below create every missing
638
+ // component of `this.root` — a root that IS the `.bantamkit` directory, or one nested
639
+ // deeper under it, brought a `.bantamkit` into existence while the decision was being
640
+ // taken about some other directory, which meant no decision at all.
641
+ const bantamkitDir = bantamkitDirFor(this.root);
642
+ const bantamkitDirExistedBefore = bantamkitDir !== null && pyExists(bantamkitDir);
603
643
  try {
604
644
  pyMkdirParents(pyJoin(this.root, 'facts'));
605
645
  pyMkdirParents(pyJoin(this.root, 'archive'));
@@ -610,7 +650,8 @@ export class MemoryStore {
610
650
  throw new MemoryValidationError(`memory store could not be created: ${this.root}; the directory is not` +
611
651
  ' there and this filesystem would not make it, so nothing was written');
612
652
  }
613
- ensureBantamkitGitignore(bantamkitDir, !bantamkitDirExistedBefore);
653
+ if (bantamkitDir !== null)
654
+ ensureBantamkitGitignore(bantamkitDir, !bantamkitDirExistedBefore);
614
655
  }
615
656
  // ---- ops ----
616
657
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bantamkit-mcp",
3
- "version": "0.34.2",
3
+ "version": "0.35.0",
4
4
  "description": "Memory MCP server for Claude Code, Cursor, VS Code Copilot and Claude Desktop: per-person memory, JSON Schema validation, shift-work tools. Pure Node; install once with npx, run offline.",
5
5
  "keywords": [
6
6
  "mcp",