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
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
- 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.
- 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.
- 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.
- 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"
Context
M6 epic #10986 ("Migrate Tier-1 MCP services to flat SDK boundary", closed 2026-05-09) moved 4
toolService.mjsfiles fromai/mcp/server/<name>/services/toolService.mjstoai/services/<name>/toolService.mjsas part of the SDK-flat migration. Operator @tobiu surfaced post-merge:Empirical anchor: github-workflow
toolService.mjsis 48 lines of pure MCP-glue (importsToolServiceMCP-base + 9 reusable service classes; definesserviceMappingMCP-tool-name → service-method; createsNeo.create(ToolService, {openApiFilePath, serviceMapping}); exportscallTool/listTools). Same shape across all 4 (memory-core, knowledge-base, neural-link, github-workflow).The Problem
The 4
toolService.mjsfiles 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?openapi.yamlToolServiceinstantiationtoolService.mjsis a perfect-match MCP-server-suitable file — every line is MCP-server-specific glue. Yet it lives atai/services/<name>/, an SDK-suitable namespace. This created two co-resident bugs in github-workflow's case (ToolServiceimport +openapi.yamlpath) because relative paths from the new location reach awkwardly intoai/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.mjsfiles 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.mjsPer-file relative-path updates:
ToolServiceimport:'../../mcp/ToolService.mjs'→'../ToolService.mjs'(one less level)openapi.yamlpath:path.join(__dirname, '../../mcp/server/<name>/openapi.yaml')→path.join(__dirname, 'openapi.yaml')(co-resident again)'./IssueService.mjs'→'../../../services/<name>/IssueService.mjs'(now reaching INTO SDK for the reusable logic)Naturally subsumes:
Acceptance Criteria
toolService.mjsfiles moved back toai/mcp/server/<name>/toolService.mjsToolService(1 dot up), service-class imports (reach into SDK),openapi.yaml(co-resident path)node ai/mcp/server/<name>/mcp-server.mjssurvives 3s probelistTools()andcallTool()exercise actually loadsopenapi.yamland resolves service-method bindings (the test that would catch lazy-load-time failures)test/playwright/unit/ai/services/are unaffected because they import service classes directly, nottoolService.mjs)Out of Scope
ai/services/Avoided Traps / Gold Standards Rejected
Decision Matrix
serviceMapping); a generic primitive would obscure that specificity.ai/services/<name>/and just fix the relative paths: Rejected. Treats the symptom (broken paths) without addressing the root cause (wrong-shape placement). Perfeedback_challenge_prescribed_fixes: when premise is wrong, fixing the prescription doesn't help.Related
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"