LearnNewsExamplesServices
Frontmatter
id11107
titleMove toolService.mjs files back to MCP server directories (M6 retrospective architectural fix)
stateClosed
labels
enhancementairefactoringarchitecturemodel-experience
assigneesneo-opus-4-7
createdAtMay 10, 2026, 3:14 PM
updatedAtMay 12, 2026, 10:05 AM
githubUrlhttps://github.com/neomjs/neo/issues/11107
authorneo-opus-4-7
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtMay 12, 2026, 10:05 AM

Move toolService.mjs files back to MCP server directories (M6 retrospective architectural fix)

Closedenhancementairefactoringarchitecturemodel-experience
neo-opus-4-7
neo-opus-4-7 commented on May 10, 2026, 3:14 PM

Context

M6 epic #10986 ("Migrate Tier-1 MCP services to flat SDK boundary", closed 2026-05-09) moved 4 toolService.mjs files from ai/mcp/server/<name>/services/toolService.mjs to ai/services/<name>/toolService.mjs as part of the SDK-flat migration. Operator @tobiu surfaced post-merge:

"the goal was to move services outside mcp servers into the sdk. you guys rubber stamped it, and literally moved all one by one. for most services correct, except for the toolService files (one per server). these literally just map service methods to mcp tools. and these REALLY belong into the mcp servers."

Empirical anchor: github-workflow toolService.mjs is 48 lines of pure MCP-glue (imports ToolService MCP-base + 9 reusable service classes; defines serviceMapping MCP-tool-name → service-method; creates Neo.create(ToolService, {openApiFilePath, serviceMapping}); exports callTool / listTools). Same shape across all 4 (memory-core, knowledge-base, neural-link, github-workflow).

The Problem

The 4 toolService.mjs files were moved as part of M6's mechanical migration without the substrate-quality test asking: what makes a file SDK-suitable vs MCP-server-suitable?

Property SDK-suitable MCP-server-suitable
Cross-server reusable yes no
Independent of MCP framing yes no
Ships per-server openapi.yaml no yes
Wires MCP-tool-name → method no yes
Owns ToolService instantiation no yes

toolService.mjs is a perfect-match MCP-server-suitable file — every line is MCP-server-specific glue. Yet it lives at ai/services/<name>/, an SDK-suitable namespace. This created two co-resident bugs in github-workflow's case (ToolService import + openapi.yaml path) because relative paths from the new location reach awkwardly into ai/mcp/server/<name>/ to find their MCP-server-side dependencies. PR #11103 / #11104 fix the immediate boot crash; the architectural fix is to put these files back where they belong.

Same family as @tobiu's 4-item retrospective: rubber-stamp at M6 epic-level let the substrate-quality test slip; PR-level rubber-stamp let the resulting wrong-shape mechanical moves slip; result is wrong-shape architecture + multiple bugs hiding in awkward relative paths.

The Fix

Move all 4 toolService.mjs files back to MCP server directories:

-ai/services/memory-core/toolService.mjs
+ai/mcp/server/memory-core/toolService.mjs

-ai/services/knowledge-base/toolService.mjs
+ai/mcp/server/knowledge-base/toolService.mjs

-ai/services/neural-link/toolService.mjs
+ai/mcp/server/neural-link/toolService.mjs

-ai/services/github-workflow/toolService.mjs
+ai/mcp/server/github-workflow/toolService.mjs

Per-file relative-path updates:

  • ToolService import: '../../mcp/ToolService.mjs''../ToolService.mjs' (one less level)
  • openapi.yaml path: path.join(__dirname, '../../mcp/server/<name>/openapi.yaml')path.join(__dirname, 'openapi.yaml') (co-resident again)
  • Service-class imports: './IssueService.mjs''../../../services/<name>/IssueService.mjs' (now reaching INTO SDK for the reusable logic)

Naturally subsumes:

  • PR #11104's import fix (file moves entirely, fix obsoletes itself)
  • The openapi.yaml path bug (whether folded into PR #11106 or separately)

Acceptance Criteria

  • All 4 toolService.mjs files moved back to ai/mcp/server/<name>/toolService.mjs
  • Per-file imports updated: ToolService (1 dot up), service-class imports (reach into SDK), openapi.yaml (co-resident path)
  • Boot smoke test for each MCP server: node ai/mcp/server/<name>/mcp-server.mjs survives 3s probe
  • Listing/calling tools: listTools() and callTool() exercise actually loads openapi.yaml and resolves service-method bindings (the test that would catch lazy-load-time failures)
  • All existing test specs still pass (per-service tests in test/playwright/unit/ai/services/ are unaffected because they import service classes directly, not toolService.mjs)
  • PR body documents the architectural rationale + cites the substrate-quality test (cross-server reusable vs MCP-server-suitable)

Out of Scope

  • Other M6-migrated files (service classes themselves, ChromaManager, etc.) — those ARE SDK-suitable per the substrate-quality test and stay in ai/services/
  • The CI gap that hid these bugs (separate ticket)
  • ChromaManager dedup (separate ticket)
  • M6 retrospective meta-lesson + epic-review skill enhancement (separate Discussion)

Avoided Traps / Gold Standards Rejected

Decision Matrix

  1. Move 4 toolService.mjs back to MCP server dirs (Selected): Architectural-correct shape per the substrate-quality test. Naturally fixes both broken paths in github-workflow case. Symmetric across all 4 servers.
  2. Refactor toolService.mjs into a generic MCP-glue primitive: Rejected as scope-creep. Each toolService.mjs is per-server-specific (different serviceMapping); a generic primitive would obscure that specificity.
  3. Keep toolService.mjs in ai/services/<name>/ and just fix the relative paths: Rejected. Treats the symptom (broken paths) without addressing the root cause (wrong-shape placement). Per feedback_challenge_prescribed_fixes: when premise is wrong, fixing the prescription doesn't help.
  4. Move only github-workflow's toolService.mjs back (since it was the one with bugs): Rejected. Asymmetric — doesn't address the root cause for the other 3 (their relative paths happen to work but the architectural shape is still wrong).
  • Trap: Treating "everything moves to SDK" as the M6 success criterion. Rejection: The success criterion was substrate-correct architecture, not file-count moved. Mechanical migration without substrate-quality test = wrong-shape outcome.
  • Trap: Letting PR #11104's surgical import-fix + (potential) #11106's openapi-fix substitute for architectural correctness. Rejection: Surgical fixes unblock the boot crash; they don't restore correct architecture. Both are needed: surgical for short-term, architectural for substrate-correctness.

Related

  • M6 epic #10986 (closed; the migration that created this gap)
  • PR #10997 (sub-ticket execution that mechanically moved the toolService.mjs files)
  • PR #11104 (#11103) — surgical fix for github-workflow ToolService import
  • PR #11106 (#11105) — proposed home for the github-workflow openapi.yaml fix per @tobiu's piggyback directive
  • @tobiu's M6 4-item retrospective (this turn 2026-05-10)
  • AGENTS.md §3.5 V-B-A core value (would have caught the substrate-quality miss at M6 epic time)

Origin Session ID: c2912891-b459-4a03-b2af-154d5e264df1 Retrieval Hint: "M6 retrospective", "toolService MCP-glue misplacement", "SDK-suitable substrate test", "architectural fix vs surgical fix"

tobiu referenced in commit f8b76fa - "refactor(mcp): move 4 toolService.mjs files back to MCP server dirs (#11107) (#11232) on May 12, 2026, 10:05 AM
tobiu closed this issue on May 12, 2026, 10:05 AM
tobiu referenced in commit f7b2561 - "feat(github-workflow): atomic PR review create/update via dedicated MCP tool (#11273) (#11276) on May 13, 2026, 7:28 AM