feat(mcp-server): add MCP server with 10 mindmap tools #1

Merged
jesse merged 15 commits from feat/mcp-server into master 2026-05-03 16:34:59 +02:00
Owner

Summary

  • Add @mindnest/mcp-server package — a Model Context Protocol server exposing 10 tools for creating, loading, querying, and mutating .drawnix mindmap files over stdio (JSON-RPC 2.0)
  • Add .mcp.json project-level config so Claude Code and Claude Desktop can connect to the server out of the box
  • Add comprehensive architecture + API reference docs (docs/features/mcp-server.md)
  • Optimize CI: split monolith job into parallel lint/test/build/e2e with change detection

MCP Tools (10)

Tool Category
mindmap_create, mindmap_load Document
mindmap_summary, mindmap_list_nodes, mindmap_get_node Query
mindmap_add_child, mindmap_add_sibling, mindmap_add_summary, mindmap_set_shape, mindmap_remove_node Mutation

Architecture highlights

  • Headless PlaitBoard (Strategy A: createTestingBoard + fakeNodeWeakMap) — mutations use the real @plait/mind transforms, no DOM shim needed
  • Atomic document store — per-path async mutex + filesystem lock + revision hash; write via temp-file rename
  • Workspace sandbox — two-stage path traversal rejection + realpath containment; .drawnix-only extension allowlist
  • Frame size guardLimitedStdioServerTransport enforces per-frame byte cap; stdout reserved exclusively for JSON-RPC frames
  • Startup invariantTOOL_ALLOWLIST asserted against registered tools at boot; mismatch throws before accepting connections

CI improvements

  • Parallel lint / test / build (previously sequential in one job)
  • e2e only waits on test + build, not lint
  • Change detection skips all jobs on docs-only PRs (~17 min → ~30 sec)
  • cancel-in-progress drops stale runs on force-push
  • Playwright install moved to e2e job only

Test plan

  • nx build mcp-server + nx run mcp-server:link-runtime-deps produces working artifact
  • node dist/mcp-server/index.cjs.js < /dev/null exits 0 with pino log on stderr
  • All 10 tools respond correctly via JSON-RPC (verified in session with raw stdio harness)
  • All negative cases return structured errors: NODE_NOT_FOUND, SANDBOX_VIOLATION, ROOT_ELEMENT, INVALID_DOCUMENT, EEXIST
  • Claude Desktop / Claude Code connects via .mcp.json and lists 10 tools
## Summary - Add `@mindnest/mcp-server` package — a Model Context Protocol server exposing 10 tools for creating, loading, querying, and mutating `.drawnix` mindmap files over stdio (JSON-RPC 2.0) - Add `.mcp.json` project-level config so Claude Code and Claude Desktop can connect to the server out of the box - Add comprehensive architecture + API reference docs (`docs/features/mcp-server.md`) - Optimize CI: split monolith job into parallel lint/test/build/e2e with change detection ## MCP Tools (10) | Tool | Category | |------|----------| | `mindmap_create`, `mindmap_load` | Document | | `mindmap_summary`, `mindmap_list_nodes`, `mindmap_get_node` | Query | | `mindmap_add_child`, `mindmap_add_sibling`, `mindmap_add_summary`, `mindmap_set_shape`, `mindmap_remove_node` | Mutation | ## Architecture highlights - **Headless PlaitBoard** (Strategy A: `createTestingBoard + fakeNodeWeakMap`) — mutations use the real `@plait/mind` transforms, no DOM shim needed - **Atomic document store** — per-path async mutex + filesystem lock + revision hash; write via temp-file rename - **Workspace sandbox** — two-stage path traversal rejection + realpath containment; `.drawnix`-only extension allowlist - **Frame size guard** — `LimitedStdioServerTransport` enforces per-frame byte cap; stdout reserved exclusively for JSON-RPC frames - **Startup invariant** — `TOOL_ALLOWLIST` asserted against registered tools at boot; mismatch throws before accepting connections ## CI improvements - Parallel lint / test / build (previously sequential in one job) - e2e only waits on test + build, not lint - Change detection skips all jobs on docs-only PRs (~17 min → ~30 sec) - `cancel-in-progress` drops stale runs on force-push - Playwright install moved to e2e job only ## Test plan - [ ] `nx build mcp-server` + `nx run mcp-server:link-runtime-deps` produces working artifact - [ ] `node dist/mcp-server/index.cjs.js < /dev/null` exits 0 with pino log on stderr - [ ] All 10 tools respond correctly via JSON-RPC (verified in session with raw stdio harness) - [ ] All negative cases return structured errors: `NODE_NOT_FOUND`, `SANDBOX_VIOLATION`, `ROOT_ELEMENT`, `INVALID_DOCUMENT`, `EEXIST` - [ ] Claude Desktop / Claude Code connects via `.mcp.json` and lists 10 tools
- Pinned Nx baseline command (NX_DAEMON=false required; 80s, all 5 projects pass)
- Confirmed @plait/core v0.93.1 exports createTestingBoard + fakeNodeWeakMap (Strategy A)
- Headless board recipe: [withOptions, withI18n, withMind] plugins; 22/22 spike tests PASS
- All 8 transforms verified headlessly (insertChildNode, insertSiblingNode, insertAbstract,
  setShape, addEmoji, removeEmoji, replaceEmoji, CoreTransforms.removeElements)
- Full 10-tool Phase 1 surface confirmed viable (no deferrals except emoji tools by plan)
- Canonical hash rule: TDD RED→GREEN, 15/15 tests, sha256 NFC-input-only non-finite rejection
- Node identity: newNodeId via pre/post id-set diff (exactly 1 new id per insertion)
- Board/export reconciliation: elements from board.children, viewport/theme copied, verbatim fields
- Build wiring: esbuild bundle to dist/mcp-server/, no tsconfig.base.json alias needed
- Staged fixtures in plans-cc/discovery/mcp-server-fixtures/ (empty, single-root, mixed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Create packages/mcp-server with full Nx project wiring:
  package.json, project.json (@nx/rollup build target), tsconfig.json,
  tsconfig.lib.json (ES2022/CJS), tsconfig.spec.json, jest.config.ts,
  eslint.config.mjs
- Move test fixtures from plans-cc/discovery/mcp-server-fixtures/ to
  packages/mcp-server/test-fixtures/ (empty, single-root, mixed)
- Add src/index.ts: pino stderr-only logging, SIGINT/SIGTERM handlers,
  main().catch() fatal exit; zero console.log calls
- Add typed Phase 2/4 stubs: config.ts, logger.ts, mcp/server.ts,
  mcp/tools/registry.ts (interface contracts for downstream agents)
- Build wiring: @nx/esbuild absent → @nx/rollup:rollup fallback
- tsconfig.base.json alias: skipped per Phase 0 §0.9 (standalone CLI)
- Verified: nx test mcp-server (0 tests, exit 0) ✓
            nx lint mcp-server (0 errors, 3 stub warnings) ✓
Phase 2 TDD implementation (66 tests, all GREEN):

- config.ts: Zod-validated singleton; root-workspace rejection (POSIX /,
  win32 C:\); home-dir console.warn; MAX_MUTATION_DURATION_MS [1000,600000]
- logger.ts: pino → fd 2 (stderr) only; createLogger(module) child factory
- fs/safe-path.ts: two-stage sandbox (raw rejection + realpath containment);
  platform-injectable helpers for win32/darwin/POSIX test coverage; rejects
  UNC/device/drive-relative paths, traversal, symlink escapes, and bad exts
- fs/canonical-hash.ts: Phase 0 spike implementation (NFC hash-input only,
  key-sorted, non-finite rejection, sha256 hex)
- fs/document-store.ts: readDocument/writeDocument/updateDocument/
  getDocumentRevision; MAX_DOCUMENT_BYTES 5 MiB; POSIX atomic temp+rename;
  Windows best-effort retry; per-realpath async mutex LRU(256) with safe
  eviction (queue-empty + refCount=0 only); ownership-token timeout with
  late-result discard; passthrough Zod schema preserves unknown fields
- jest.config.ts: add testEnvironment: 'node' for setImmediate / Node globals
Phase 3 implementation (TDD: RED → GREEN). All 137 tests pass; 71 new
tests added on top of the 66-test Phase 2 baseline.

src/board/board-host.ts — withBoard<T>(document, fn) constructs a headless
PlaitBoard via the Phase 0-confirmed Strategy A recipe (createTestingBoard
+ fakeNodeWeakMap with [withOptions, withI18n, withMind] plugin chain),
runs fn(board), and returns { result, document, newNodeId? } using
pre/post mind-element id-set diff for the deterministic newNodeId
contract. Includes the Phase 0 §0.12 test-only failure injection guarded
by NODE_ENV=test + MCP_TEST_INJECT_BOARD_FAILURE=1.

src/board/mind-queries.ts — pure tree traversal helpers operating on
document.elements directly (no board construction): findMindElementById,
listMindElements, getRootTopics, getChildTopics, summarizeTree.

src/board/mind-transforms.ts — five wrappers (addChild, addSibling,
addSummary, setShape, removeNode) that resolve string ids, validate via
isMindElement, and persist mutations through document-store.updateDocument
(the per-realpath mutex). NodeNotFoundError, NotMindElementError, and
RootElementError surface structured errors with code fields. addSibling
explicitly rejects root mindmap elements per Phase 0 §0.4 PlaitMind.isMind
discovery outcome (the upstream transform silently no-ops; we throw).

jest.config.ts — three additions to make the @plait ESM packages testable:
1. moduleNameMapper points @plait/core and @plait/mind to workspace-level
   .mjs bundles to prevent duplicate-module WeakMap divergence (mcp-server
   has package-local copies that aren't used by @plait/common etc.)
2. transformIgnorePatterns whitelist for @plait/* and points-on-curve
   (the latter ships type=cjs but uses ESM export syntax)
3. @babel/plugin-transform-class-static-block for @plait/mind static {}
   class blocks (ES2022) that the base @nx/js/babel preset doesn't enable.

Coverage (global, all above 80% threshold):
  Statements 89.30%  Branches 82.97%  Functions 93.25%  Lines 90.71%

No DOM shim, no per-file branch override needed (Phase 0 Strategy A).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements Phase 4 of the MCP server plan: 10 tools fully registered,
tested, and wired into the stdio entrypoint.

New files:
- src/mcp/tools/schemas.ts — PathSchema, NodeIdSchema, MindShapeSchema, DepthSchema
- src/mcp/tools/result.ts — successResult, errorResult, toToolError (no stack-trace leak)
- src/mcp/tools/document-tools.ts — mindmap_create, mindmap_load
- src/mcp/tools/query-tools.ts — mindmap_summary, mindmap_list_nodes, mindmap_get_node
- src/mcp/tools/mutation-tools.ts — mindmap_add_child, mindmap_add_sibling,
  mindmap_add_summary, mindmap_set_shape, mindmap_remove_node
- src/mcp/tools/registry.ts — TOOL_ALLOWLIST + registerAllTools (mismatch guard)
- src/mcp/server.ts — createMcpServer (real SDK McpServer factory)
- 5 new spec files: document-tools, query-tools, mutation-tools, registry, result

Key details:
- Pinned zod to ^3.23.8 to match SDK's bundled zod (v3 compat required)
- All tools: resolveSafePath → handler → successResult/toToolError
- Output cap, no stdout writes, debug-only logging
- DRIFT TEST in document-tools.spec.ts imports from packages/drawnix
- 200 tests passing, 80.23% branch coverage (threshold: 80%)
Phase 5 deliverables:

- src/__tests__/stdio-purity.spec.ts (9 tests, all passing):
  * Spawned-process tests for stdout=MCP-only, stderr-no-frames
  * Error-path coverage: unknown tool, validation failure, sandbox violation,
    synthetic board failure injection (NODE_ENV=test + MCP_TEST_INJECT_BOARD_FAILURE=1)
  * Runtime inertness: NODE_ENV=production keeps injection guard inert
  * Phase 5.2 integration harness: parallel mutation lost-update prevention via spawned
- docs/inspector-smoke.md: build/launch/CLI-verification + minimal-PATH (env -i) instructions
- evaluations/mindmap-tools.eval.xml: 10-question suite covering all shipped tools,
  with mandatory negative questions (Q5 root-sibling, Q6 unknown-id, Q10 sandbox traversal)
  plus malformed-JSON load (Q2)

Build fixes required to produce a runnable artifact:
- src/index.ts: pino import via export= (Zod v3/v4 conflict mitigated by node_modules
  symlink in dist/mcp-server/, created via new link-runtime-deps target)
- src/mcp/tools/result.ts: ToolResult type updated to match SDK CallToolResult shape
- src/board/mind-transforms.ts: PlaitMindBoard / MindElement type casts for rollup TS check
- tsconfig.lib.json: esModuleInterop + allowSyntheticDefaultImports for pino default import
- project.json: link-runtime-deps target creates dist/mcp-server/node_modules symlink

Coverage (verified with --coverage):
- Statements 89.36%, Branches 80.23%, Functions 94.82%, Lines 90.79% (all ≥80%)
- 209 tests passing (200 in-process + 9 spawned stdio-purity)
- src/board/ overrides: 0 (Phase 0 used Strategy A — no DOM shim required)
- packages/mcp-server/README.md: full install, Claude Desktop/Code
  config snippets, Inspector smoke test, 10-tool catalog table, 7-var
  env-var table, and troubleshooting (PATH, stderr volume, stale cache)
- packages/mcp-server/package.json: fix bin entry index.js → index.cjs.js
- document-tools.spec.ts: add eslint-disable block for @nx/enforce-module-boundaries
  on intentional cross-package fixture-drift imports
- stdio-purity.spec.ts: increase TEST_TIMEOUT_MS 15 s → 60 s, pass
  TEST_TIMEOUT_MS to initializeServer's waitForResponse call, add
  jest.retryTimes(2) to absorb process-spawn latency under parallel load
- plans-cc/discovery/mcp-server-phase0.md: append Phase 6 Comparison
  section (4 runs, timing, issues resolved, no-regression verdict )
- plans-cc/mcp-server-http-plan.md: deferred Phase 7 HTTP transport stub
- plans-cc/mcp-server-todo.md: tick all Phase 6 and Acceptance Criteria
  checkboxes; leave two verification items unchecked with explanations

Phase 6 no-regression:  PASS (exit 0, 6 projects, 71 s / 80 s = 89%)
- Add @mindnest/mcp-server package: 10 MCP tools over stdio for .drawnix mindmap files
  (mindmap_create, mindmap_load, mindmap_summary, mindmap_list_nodes, mindmap_get_node,
   mindmap_add_child, mindmap_add_sibling, mindmap_add_summary, mindmap_set_shape,
   mindmap_remove_node)
- Headless PlaitBoard host (Strategy A: createTestingBoard + fakeNodeWeakMap, no DOM shim)
- Atomic document store with per-path async mutex, filesystem lock, and revision hashing
- Workspace-sandboxed path resolver with two-stage traversal rejection
- LimitedStdioServerTransport with per-frame byte guard
- Add .mcp.json project-level MCP server config pointing to test-fixtures workspace
- Add docs/features/mcp-server.md: comprehensive architecture + API reference
- Update README, docs/01/02/04/09/PROJECT_DOCUMENTATION to reflect new package
- Fix MindElementShape values in docs: actual enum is {round-rectangle, underline}
- Update .gitignore: exclude *.tgz tarballs and .claude/ project cache
- Add concurrency group with cancel-in-progress to drop stale PR runs
- Add change detection (dorny/paths-filter) — docs-only PRs skip all jobs
- Split single 'main' job into 4 parallel jobs: lint, test, build, e2e
- lint/test/build all needs: [changes] only — no artificial ordering
- e2e needs: [changes, test, build] — quality gate, NOT blocked by lint
- Playwright install moved to e2e job only (saves ~2min in lint/test/build)
- Remove hardcoded --base=origin/develop; let nrwl/nx-set-shas compute base
ci: fix runs-on label to match runner (docker → node:20)
Some checks failed
CI / changes (pull_request) Failing after 18s
CI / lint (pull_request) Has been skipped
CI / build (pull_request) Has been skipped
CI / test (pull_request) Has been skipped
CI / e2e (pull_request) Has been skipped
ef50acf196
Runner yuantech-runner only has label 'docker' (docker://node:20).
ubuntu-latest was causing all jobs to never be picked up.
ci: replace dorny/paths-filter with git diff (Forgejo pulls from code.forgejo.org, not GitHub)
Some checks failed
CI / changes (pull_request) Failing after 17s
CI / lint (pull_request) Has been skipped
CI / test (pull_request) Has been skipped
CI / build (pull_request) Has been skipped
CI / e2e (pull_request) Has been skipped
eed5b74b4c
dorny/paths-filter@v3 doesn't exist on code.forgejo.org causing changes job
to fail after 18s. Replaced with a plain git diff shell script that checks
for code changes against the PR base or push before SHA.
ci: use full code.forgejo.org URLs for actions, replace nrwl/nx-set-shas
Some checks failed
CI / changes (pull_request) Failing after 17s
CI / lint (pull_request) Has been skipped
CI / test (pull_request) Has been skipped
CI / build (pull_request) Has been skipped
CI / e2e (pull_request) Has been skipped
a7953b203e
Forgejo resolves bare action names (actions/checkout) against the local
instance by default. Since git.yuantech.uk has no 'actions' org, 'Set up
job' times out with a 17s failure on every run.

Fixes:
- Prefix all uses: with https://code.forgejo.org/ so the runner fetches
  from the public Forgejo actions registry directly
- Replace nrwl/nx-set-shas@v5 (not on code.forgejo.org) with inline shell
  that sets NX_BASE/NX_HEAD from PR/push event SHAs
- Move github context expressions into env: vars per security best practice

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ci: move to .forgejo/workflows with checkout@v4, setup-node@v4
Some checks failed
CI / changes (pull_request) Successful in 11s
CI / test (pull_request) Failing after 7m24s
CI / build (pull_request) Failing after 8m29s
CI / e2e (pull_request) Has been skipped
CI / lint (pull_request) Successful in 11m57s
b64682d350
The runner on git.yuantech.uk uses .forgejo/workflows/ (Forgejo-native
path). The .github/workflows/ version remains for compatibility.

Changes:
- Add .forgejo/workflows/ci.yml as the primary workflow path
- Downgrade checkout@v6 → @v4 and setup-node@v6 → @v4 (same versions
  used in working project_management CI on this instance)
- Add timeout-minutes to all jobs
- Add .forgejo/workflows/ to change-detection pattern

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix(ci): install mcp-server local deps and auto-build before tests
All checks were successful
CI / changes (pull_request) Successful in 13s
CI / lint (pull_request) Has been skipped
CI / test (pull_request) Has been skipped
CI / build (pull_request) Successful in 12m9s
CI / e2e (pull_request) Has been skipped
b97835f550
Build failure: packages/mcp-server has its own node_modules (pino,
@modelcontextprotocol/sdk) not hoisted to root. Added
`npm ci --prefix packages/mcp-server` to all jobs so rollup can
resolve these deps during build.

Test failure: stdio-purity.spec.ts spawns dist/mcp-server/index.cjs.js
which doesn't exist in a test-only run. Added `dependsOn: ["build"]`
to the mcp-server test target so nx always builds it first.

Cleanup: remove .github/workflows/ci.yml — .forgejo/workflows/ci.yml
is the canonical workflow for this Forgejo instance.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
jesse merged commit 72193201b8 into master 2026-05-03 16:34:59 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
jesse/mindnest!1
No description provided.