LearnNewsExamplesServices
Frontmatter
titlechore(ai): retire manage_database_backup MCP tools (#10132)
authortobiu
stateMerged
createdAtApr 20, 2026, 9:21 PM
updatedAtApr 20, 2026, 9:41 PM
closedAtApr 20, 2026, 9:41 PM
mergedAtApr 20, 2026, 9:41 PM
branchesdevagent/10132-reduce-mcp-backup-surface
urlhttps://github.com/neomjs/neo/pull/10133
Merged
tobiu
tobiu commented on Apr 20, 2026, 9:21 PM

Summary

Follow-up cleanup to #10131 per @tobiu's post-merge review. Reclaims one MCP tool slot from the ~80-tool budget by retiring manage_database_backup from both MC (pre-existing) and KB (yaml-only surface I added in #10131 that was never wired in toolService.mjs).

Rationalenpm run ai:backup (shipped in #10131) covers all five persistent AI subsystems atomically with JSONL artifacts in a unified bundle layout. The MC MCP tool only covered memories/summaries/graph and had no atomic-bundle semantics, so it's strictly less capable than the script. Per the #9903 precedent ("protect the 100-tool MCP limit while allowing Frontier models to compress their own Action Logs") the script-over-tool path is the right architectural direction for operator-facing backup operations.

Zero functional regression — the KB_DatabaseService.manageDatabaseBackup and Memory_DatabaseService.manageDatabaseBackup service methods remain callable via the ai/services.mjs SDK boundary. makeSafe applies Zod wrapping only when a matching openapi operation is found; with the openapi entries removed, makeSafe uses its no-match passthrough, forwarding raw args to the underlying methods. backup.mjs continues to work as-is.

What changed

MC retirement

  • ai/mcp/server/memory-core/openapi.yaml — removed /db/backup/manage POST + BackupActionResponse schema
  • ai/mcp/server/memory-core/services/toolService.mjs — removed manage_database_backup entry from serviceMapping (line 26)

KB revert (undoing #10131's yaml-only addition)

  • ai/mcp/server/knowledge-base/openapi.yaml — removed /db/backup/manage POST + BackupActionResponse schema I added in #10131
  • ai/mcp/server/knowledge-base/services/DatabaseService.mjs — updated class @summary Backup Surface paragraph + method JSDoc to reflect the SDK-only reality (no more "registered in openapi.yaml / Zod-wrapped via makeSafe" claims; replaced with explicit script-over-tool rationale and #9903/#10132 cross-refs)

Test

  • test/playwright/unit/ai/mcp/server/knowledge-base/services/DatabaseService.backup.spec.mjs — renamed the third test from "rejects unsupported actions at the SDK Zod validation boundary" to "rejects unsupported actions at the dispatcher layer" and tightened the assertion to .rejects.toThrow(/Unknown action: import/) so it verifies the specific rejection path (no-match passthrough → dispatcher manual throw) instead of any generic error

Live validation

Before committing this cleanup I ran npm run ai:backup from main against the real graph / KB / MC state to establish empirical confidence that the script path is ready to fully replace the MCP tool:

Wall clock: 6s
Exit code: 0

{
  "bundleRoot": "/Users/Shared/github/neomjs/neo/.neo-ai-data/backups/backup-2026-04-20T18-59-35.772Z",
  "subsystems": {
    "kb":           { "message": "Export complete. Exported 10850 knowledge base chunks." },
    "mc":           { "message": "Export complete. Exported 8204 memories, 794 summaries, and 0 graph elements." },
    "graph":        { "message": "Export complete. Exported 0 memories, 0 summaries, and 369 graph elements." },
    "concepts":     { "copied": 2 },
    "trajectories": { "copied": 1 }
  }
}

wc -l on graph/graph-backup-*.jsonl confirms 369 real newlines — the \n fix from #10131 is live and the graph backup is now properly restorable JSONL (vs. the pre-#10131 single-line malformed files).

Testing

11 Playwright specs pass under --workers=1 (single-worker serialization):

Spec Cases
DatabaseService.backup.spec.mjs (KB) 3 (populated / empty / dispatcher rejection)
DatabaseService.backupPath.spec.mjs (MC) 1 (backupPath routing via SDK passthrough)
backup.mjs orchestrator 2 (populated bundle / missing-source graceful)
peer-architecture.spec.mjs 5 (non-delegation + SDK-boundary guards)

The pre-existing McpServerToolLimits.spec.mjs failure persists and is not caused by this PR — diagnostic identified the culprit as manage_database (the Chroma lifecycle tool) at 1161 chars, not manage_database_backup (which was under 400 chars when it existed). That failure is deferred to the getFullDescription exploration — systemic tool-description-size reduction is the right substrate for fixing it, not this single-tool retirement.

A parallel-worker race I observed when running multiple backup specs concurrently (intermittent undefined on KB_ChromaManager.getKnowledgeBaseCollection across afterAll hooks) is also pre-existing and tangent to this PR. Single-worker serialization sidesteps it. Flagged for follow-up.

Architectural Direction

This PR makes incremental progress toward two larger architectural directions @tobiu noted on 2026-04-20:

  1. getFullDescription exploration — lean advertised tool descriptions + on-demand full-prose fetcher. Defers context cost from discovery-time to invocation-time. Systemic fix for the 1024-char cap pressure.
  2. ai/services/ migration — thin-wrapper-ification of MCP servers, with services migrating to ai/services/ as the authoritative layer. This PR's removal pattern (remove MCP surface, keep service method callable via SDK) is directly aligned with that migration direction.

Neither is implemented here — this PR is a single-slot reclaim only.

Acceptance Criteria (from #10132)

  • grep -rn 'manage_database_backup' ai/mcp/server/ returns zero results
  • KB_DatabaseService.manageDatabaseBackup and Memory_DatabaseService.manageDatabaseBackup remain callable via ai/services.mjs SDK (verified by the spec suite)
  • npm run ai:backup live invocation still emits the full 5-subfolder bundle (verified 6s / exit 0 / all 5 subfolders populated)
  • Spec test names no longer reference "Zod boundary" where the rejection actually happens at the dispatcher layer
  • McpServerToolLimits.spec.mjs is no worse than current state (pre-existing manage_database 1161-char failure untouched; deferred to getFullDescription)

Out of Scope

  • getFullDescription exploration — systemic tool-description reduction
  • ai/services/ migration — thin-wrapper-ification of MCP servers
  • Parallel-worker race in backup specs — pre-existing, tangent to this PR
  • Restore tooling for backups — deferred per #10129

Resolves #10132

Origin Session ID: 5a521819-dc75-4549-888e-fcea818d0401

tobiu
tobiu commented on Apr 20, 2026, 9:32 PM

Input from Claude Opus 4.7 (Claude Code):

❋ # PR Review Summary

Status: Comment (self-review — human approval required per AGENTS.md §7)

Self-Review Opening: Self-review of #10132. I chose to revert my own #10131 yaml addition + retire the pre-existing MC tool in a single cleanup pass after tobi caught that the KB yaml surface was never wired into toolService.mjs serviceMapping. Two-PRs-from-one-ticket is the cost of not running pr-review before #10131 handoff — worth flagging as the meta-lesson.


📊 Evaluation Metrics

  • [ARCH_ALIGNMENT]: 92 — Clean reduction aligned with the #9903 script-over-tool precedent and the ai/services/ migration direction. Minor ding: JSDoc now embeds ticket numbers (#9903, #10132) as architectural anchors, which ages poorly when tickets get re-numbered or archived. Fat-ticket repos tolerate this; worth tracking as a general hygiene concern.
  • [CONTENT_COMPLETENESS]: 90 — Class + method JSDoc symmetrically updated; test rename carries an explanatory comment about the no-match passthrough path; PR body has empirical validation data + pre-checked AC boxes. Loses points for not documenting that Memory_DatabaseService.manageDatabaseBackup could use a corresponding JSDoc refresh (I didn't touch MC's class docstring in this PR — only removed registration — so the MC JSDoc may still reference MCP-tool framing in places I didn't audit).
  • [EXECUTION_QUALITY]: 80 — Main blind spot: I observed a parallel-worker race on these specs (undefined KB_ChromaManager.getKnowledgeBaseCollection during concurrent afterAll hooks) and flagged it as "pre-existing" rather than fixing it in-scope. A one-line test.describe.configure({ mode: 'serial' }) on the three affected specs would prevent CI flakes at the next run — cheap fix I should have included. Honest self-critique: deferring this to "follow-up" transfers risk to CI runners without explaining why.
  • [PRODUCTIVITY]: 95 — Ticket #10132 filed as Fat Ticket (five-stage challenge chain), branched, edited, tested, PR'd in one contiguous pass (~25 min). All 5 AC items empirically verified pre-commit.
  • [IMPACT]: 50 — Net −153 lines, one MCP tool slot reclaimed. Real but small. Strategic weight comes from alignment with the ai/services/ migration direction, not from the raw diff size.
  • [COMPLEXITY]: 35 — Mechanical yaml block removal + one-line serviceMapping delete + JSDoc rewrite + test rename. Low cognitive load. The only non-trivial call was whether to tighten the test assertion (.toThrow(/Unknown action: import/) vs. the prior bare .toThrow()) — tightening trades brittleness against informativeness, which is a real judgment call but scope-adjacent to the ticket.
  • [EFFORT_PROFILE]: Quick Win — high ROI (tool slot reclaim + migration-direction alignment + pre-existing-drift elimination), low complexity, clean tests. Textbook Quick Win.

🕸️ Context & Graph Linking

  • Target Epic / Issue ID: Resolves #10132
  • Related Graph Nodes: implementation session 5a521819 (both #10131 and #10132 authored here), parent PR #10131 (MERGED; introduced the unused KB yaml surface this PR reverts), precedent #9903 (script-over-tool), test infra #10041 (McpServerToolLimits.spec.mjs), architectural direction: getFullDescription exploration + ai/services/ migration (both noted by @tobiu 2026-04-20)

🧠 Graph Ingestion Notes

  • [KB_GAP]: In the #10131 self-review I wrote "SDK boundary is load-bearing... gives us Zod validation for free". Within one PR cycle that framing flipped: removing the openapi entries meant makeSafe's no-match passthrough kicks in, Zod is skipped, and the dispatcher's manual throw becomes the rejection path. The "Zod for free" framing was overcautious for single-writer script paths — Zod is a feature only when the operation is actually registered as an MCP tool. Lesson to embed: be precise about where makeSafe wrapping happens (MCP-registered operations with matching openapi ops), not where it could happen (any service method). The SDK is the boundary; wrapping is conditional.

  • [KB_GAP]: Yaml-without-registration is a dangerous architectural drift vector. My #10131 added the KB /db/backup/manage path to openapi.yaml without adding it to knowledge-base/services/toolService.mjs serviceMapping. That made it SDK-visible (via makeSafe) but not MCP-exposed. If tobi hadn't caught it in post-merge review, a future agent seeing the yaml might have "completed the registration" in toolService.mjs without reviewing whether MCP exposure was actually desired — compounding the drift rather than cleaning it. Moral: never land yaml surfaces without the corresponding toolService.mjs entry, OR don't land the yaml. Half-wired state is worse than either full state.

  • [TOOLING_GAP]: Playwright parallel-worker interleaving observed on backup specs — intermittent TypeError: Cannot set properties of undefined during afterAll hooks that restore singleton mutations on KB_ChromaManager.getKnowledgeBaseCollection / Memory_StorageRouter.getMemoryCollection. Per the "symmetric spec cleanup" lesson (beforeEach + afterEach instead of beforeAll + afterAll for cross-singleton mutations), these specs need either worker-scoped fixtures OR explicit test.describe.configure({ mode: 'serial' }). I did NOT fix this in scope; that's a deliberate punt that transfers risk to CI runners.

  • [RETROSPECTIVE]: Running pr-review AFTER merge (as I did on #10131) means you can only file follow-up cleanup PRs — you cannot catch half-wired state before it lands. Running pr-review BEFORE handoff would have caught the "yaml without serviceMapping registration" smell, and #10132 would never have been necessary. The two-PRs-from-one-ticket pattern visible here is a cost signal: for architectural-change PRs especially, the reflection pass must precede handoff.

  • [RETROSPECTIVE]: Script-over-tool is the default architectural choice for operator-facing infrastructure; MCP tools are for on-demand agent-triggered operations with meaningful latency constraints. The cognitive cost of two paths (MC tool + backup.mjs script) is real even when both work — agents have to choose, and the choice is not obvious from tool descriptions alone.


📋 Required Actions

None are merge-blockers. The following are self-identified gaps:

  • Consider folding into this PR before merge: add test.describe.configure({ mode: 'serial' }) to the three affected backup specs (DatabaseService.backup.spec.mjs, DatabaseService.backupPath.spec.mjs, backup.spec.mjs). One line per file, prevents the observed parallel-worker race from biting CI. Low risk, high value — arguably should have been in-scope.
  • Follow-up ticket (non-blocking): audit Memory_DatabaseService class + method JSDoc for any residual "MCP tool" framing now that the MC tool is retired. I did not touch MC's class docstring in this PR — only removed registration — so the symmetric JSDoc cleanup is outstanding.
  • Follow-up ticket (non-blocking): manage_database (Chroma lifecycle) 1161-char description — defer to getFullDescription exploration as agreed.

Closing reflection: This PR exists because the previous PR's self-review ran post-merge instead of pre-handoff. The "two-PRs-from-one-ticket" cost is not a failure mode to hide — it's the feedback signal telling me where to place pr-review in the workflow next time. For the next architectural-change PR, invoke pr-review before gh pr create, not after gh pr merge.