Filed 2026-05-08 as the M2 prep ticket on the v13 release path. Per learn/agentos/v13-path.md §4 D2 (Critical Architectural Decision):
Each MCP server currently bootstraps its own infrastructure independently. A Common Base Server Class with extension points (registerTools, registerServices, registerMiddleware, registerHealthChecks) standardizes the per-server-author pattern, reduces boilerplate, and prepares uniform substrate for M6 SDK migration.
Sequences as M2 on the milestone path: M1 (substrate stabilization, in flight) → M2 (Common Base Server Class) → M3 (Orchestrator Daemon, #10956, @neo-gemini-3-1-pro's lane) → M4-M7 downstream.
Server Inventory (corrected 2026-05-08 per @tobiu)
There are 5 MCP servers, partitioned into two tiers:
Tier-2 — Local-Only (gemma4-style local agent only; NOT deployment-target):
ai/mcp/server/file-system/Server.mjs (149 lines)
Tier-2 is included in M2 migration scope for consistency (shared base class, lower per-server-author cognitive load) but is out of scope for M6 SDK migration (deployment-only concern).
The Problem
The 5 server entry points (ai/mcp/server/*/mcp-server.mjs) are thin CLI wrappers (~11-35 lines each) that just Neo.create(Server).ready(). The actual bootstrap logic lives in per-server Server.mjs files (totaling ~1444 lines across the 5).
Each Server.mjs independently:
Creates the MCP transport layer (McpServer from @modelcontextprotocol/sdk)
Wires setupRequestHandlers(mcpServer) for ListToolsRequestSchema + CallToolRequestSchema
Loads aiConfig from ./config.mjs (gitignored; cloned from config.template.mjs at npm prepare)
Runs healthcheck + log startup status
Connects transport (StdioServerTransport for stdio mode OR TransportService.setup() for sse mode)
There's no shared base class for the server itself. ai/mcp/server/shared/ currently holds service-level primitives (BaseConfig.mjs, services/AuthService.mjs, services/RequestContextService.mjs, services/StdioIdentityResolver.mjs, services/AuthMiddleware.mjs, services/TransportService.mjs, helpers/), but each per-server Server.mjs composes these primitives independently.
Knowledge-base bootstraps differently from memory-core (which has 580 lines of bootstrap), which differs from github-workflow — drift accumulates per-server-author session, and middleware-ordering bugs (e.g., auth-before-context-establishment) recur per-server because there's no single source of truth.
A Common Base Server Class with explicit extension points eliminates this drift, gives each server a clear "what to override" surface, and creates uniform substrate for M6 SDK migration of the Tier-1 servers.
The Architectural Reality
Current Server.mjs shape (each extends Neo.core.Base):
Each per-server Server.mjs becomes a thin extension overriding only the 4-5 hooks. Bootstrap order, transport setup, default middleware, healthcheck-aggregation, and ListTools/CallTool wiring become base-class concerns.
The Fix
Create ai/mcp/server/BaseServer.mjs with the extension points + canonical lifecycle methods + JSDoc @summary. Class extends Neo.core.Base per Neo conventions.
Migrate each of the 5 servers to extend BaseServer:
Tier-1 (4 cloud-native): full migration in scope
Tier-2 (file-system, local-only): migration in scope for consistency (lower priority; can defer if Tier-1 surfaces unforeseen complexity)
Each migration: lift current bootstrap code into the appropriate registerXxx() override; remove duplicated transport/middleware/healthcheck wiring; verify healthcheck output unchanged before/after
Document the extension contract in learn/agentos/tooling/Introduction.md (or new MCPServerBaseClass.md if scope warrants).
Test surface: unit specs for the base class (test/playwright/unit/ai/mcp/server/BaseServer.spec.mjs), plus per-server smoke tests verifying behavior unchanged after migration.
Contract Ledger Matrix
Target Surface
Source of Authority
Proposed Behavior
Fallback
Docs
Evidence
Neo.ai.mcp.server.BaseServer class
This ticket; v13-path.md D2
Provides extension points + canonical bootstrap order; final initAsync/start/stop/setupRequestHandlers
None — inheritance is the contract
learn/agentos/tooling/MCPServerBaseClass.md (new) or §extension in Introduction.md
Per-server Server.mjs extends BaseServer; unit specs cover extension-point invocation
Per-server Server.mjs
This ticket
Each becomes a thin extension overriding the hooks
AC1:ai/mcp/server/BaseServer.mjs created, extends Neo.core.Base per Neo class-system conventions, has JSDoc @summary on every method.
AC2: Class exposes the extension hooks: getServerMetadata, registerTools, registerServices, registerMiddleware, registerHealthChecks. Default implementations are no-ops returning the input collection unchanged (or sentinel for getServerMetadata).
AC3: Canonical lifecycle methods initAsync(), start(), stop() orchestrate the registration flow in deterministic order; per-server overrides cannot reorder. Double-start throws.
AC4: All 4 Tier-1 cloud-native servers (knowledge-base, memory-core, github-workflow, neural-link) migrated to extend BaseServer.
AC5: Tier-2 file-system server migrated to extend BaseServer (consistency with Tier-1; can defer to a follow-up ticket if Tier-1 surfaces unforeseen complexity).
AC6: Each per-server Server.mjs reduced significantly post-migration (rough target: drift below ~80 lines per file is the success signal, not a hard cap; the ~1444 total LOC across 5 Server.mjs files should drop substantially).
AC7: Per-server healthcheck JSON shape unchanged after migration (snapshot diff captured in PR evidence).
AC8: Per-server tool count + tool surface unchanged (verified via integration smoke against openapi.yaml).
AC10: Documentation: either new learn/agentos/tooling/MCPServerBaseClass.md OR §-section added to learn/agentos/tooling/Introduction.md describing the extension contract + when to override which hook.
AC11: No regression in CI: integration row + unit row both green post-migration.
Out of Scope
M3 Orchestrator daemon — separate primitive, sibling to bridge per v13-path D3, tracked via #10956. The Orchestrator daemon does NOT extend BaseServer (different class hierarchy — daemons vs servers).
M6 SDK migration per-server — this ticket creates the internal class-hierarchy substrate; M6 exposes per-server SDKs for external consumption, scoped to Tier-1 only. Separate tickets per Tier-1 server, sequenced after M2 lands.
Config-substrate cleanup — tracked via #10822. M2 ingests existing BaseConfig shape; does NOT change the config layer.
Importable-defaults config refactor — separate downstream ticket. M2 creates the substrate; refactoring aiConfig from './config.mjs' → aiConfigDefaults from './config.template.mjs' + optional override layer is a separate per-server change. Coordinated with #10964 (clean-checkout deploy bootstrap), which uses Path A (Docker/entrypoint generates config.mjs) for immediate clean-deployment unblock.
Runtime Factory pattern — the per-process LOCAL servers still use isPrimary for single-writer guard; the REMOTE Factory (RequestContextService + Mcp-Session-Id) operates at a different layer. M2 is class-hierarchy-only, not concurrency-substrate.
Single-writer enforcement audit — tracked via #10186. M2 doesn't add or remove single-writer behavior.
Bridge daemon — pre-existing separate daemon process; not an MCP server; not affected by this ticket.
Avoided Traps / Gold Standards Rejected
Rejected: skip the base class, use composition only. Composition would scatter the bootstrap-order discipline across each server (re-creating today's drift). Inheritance + final lifecycle methods + open extension points enforces order at the language level.
Rejected: factory function returning a configured server. A factory hides the extension surface and breaks Neo's class system conventions. Inheritance is more discoverable for downstream maintainers + matches Neo.core.Base patterns elsewhere.
Rejected: collapse registerTools and registerServices into one hook. Tools and services have different lifecycles — tools are static (defined at openapi.yaml load time); services may be dynamic (initAsync). Separate hooks keep the contract explicit.
Rejected: bake middleware ordering into registerMiddleware. Per-server overrides should APPEND to base middleware, not replace it. Base class controls canonical-order; per-server adds at end.
Rejected: re-implement Express/Hono-style routing in BaseServer. MCP transport is JSON-RPC over stdio + HTTP; routing is dictated by the MCP spec, not the BaseServer. Routing logic stays in transport-layer services.
Rejected: pre-empt M6 SDK boundary inside M2. M6 exposes a stable external SDK surface; M2 standardizes internal class hierarchy. Coupling them creates premature lock-in.
Rejected: exclude file-system server from M2 because it's local-only. The substrate gain (uniform base class, single source of truth for MCP boilerplate) outweighs the slight cost of including the Tier-2 server. Excluding it would leave one stale-shape server lurking that future server-authors would clone-copy.
Rejected: refactor config-import shape inside M2. Importable-defaults config refactor is a separate per-server change. Coupling it with BaseServer migration would balloon scope. Path A (Docker/entrypoint) covers #10964's immediate need; importable-defaults can come after M2 substrate lands.
Retrieval Hint: query_raw_memories(query="MCP server common base class extension points registerTools registerServices registerMiddleware registerHealthChecks v13 M2 milestone Tier-1 cloud-native Tier-2 local-only")
tobiu referenced in commit 77f71e7 - "feat(ai): MCP server common base class scaffold + unit specs (#10965) (#10966) on May 8, 2026, 5:44 PM
tobiu referenced in commit b368cbb - "feat(ai): migrate github-workflow/Server to extend BaseServer (#10965) (#10974) on May 8, 2026, 6:39 PM
tobiu referenced in commit 6e8607c - "feat(ai): migrate neural-link/Server to extend BaseServer + boot() seam (#10965) (#10975) on May 8, 2026, 6:43 PM
tobiu referenced in commit 2405528 - "feat(ai): migrate memory-core/Server to extend BaseServer + beforeToolDispatch hook (#10965) (#10977) on May 8, 2026, 6:45 PM
tobiu referenced in commit dfb71b4 - "feat(ai): migrate knowledge-base/Server to extend BaseServer (#10965) (#10973) on May 8, 2026, 7:08 PM
tobiu referenced in commit f3340b1 - "feat(ai): migrate file-system/Server to extend BaseServer (#10965) (#10976) on May 8, 2026, 7:09 PM
tobiu referenced in commit 0d09943 - "docs(agentos): add MCPServerBaseClass documentation (#10965) (#10981) on May 8, 2026, 7:39 PM
Context
Filed 2026-05-08 as the M2 prep ticket on the v13 release path. Per
learn/agentos/v13-path.md§4 D2 (Critical Architectural Decision):Sequences as M2 on the milestone path: M1 (substrate stabilization, in flight) → M2 (Common Base Server Class) → M3 (Orchestrator Daemon, #10956, @neo-gemini-3-1-pro's lane) → M4-M7 downstream.
Server Inventory (corrected 2026-05-08 per @tobiu)
There are 5 MCP servers, partitioned into two tiers:
Tier-1 — Cloud-Native (deployment targets, primary M2 + M6 scope):
ai/mcp/server/knowledge-base/Server.mjs(277 lines)ai/mcp/server/memory-core/Server.mjs(580 lines)ai/mcp/server/github-workflow/Server.mjs(226 lines)ai/mcp/server/neural-link/Server.mjs(212 lines)Tier-2 — Local-Only (gemma4-style local agent only; NOT deployment-target):
ai/mcp/server/file-system/Server.mjs(149 lines)Tier-2 is included in M2 migration scope for consistency (shared base class, lower per-server-author cognitive load) but is out of scope for M6 SDK migration (deployment-only concern).
The Problem
The 5 server entry points (
ai/mcp/server/*/mcp-server.mjs) are thin CLI wrappers (~11-35 lines each) that justNeo.create(Server).ready(). The actual bootstrap logic lives in per-serverServer.mjsfiles (totaling ~1444 lines across the 5).Each
Server.mjsindependently:McpServerfrom@modelcontextprotocol/sdk)setupRequestHandlers(mcpServer)for ListToolsRequestSchema + CallToolRequestSchemaDatabaseService,HealthService,KBRecorderService, etc.)aiConfigfrom./config.mjs(gitignored; cloned fromconfig.template.mjsatnpm prepare)StdioServerTransportfor stdio mode ORTransportService.setup()for sse mode)There's no shared base class for the server itself.
ai/mcp/server/shared/currently holds service-level primitives (BaseConfig.mjs,services/AuthService.mjs,services/RequestContextService.mjs,services/StdioIdentityResolver.mjs,services/AuthMiddleware.mjs,services/TransportService.mjs,helpers/), but each per-serverServer.mjscomposes these primitives independently.Knowledge-base bootstraps differently from memory-core (which has 580 lines of bootstrap), which differs from github-workflow — drift accumulates per-server-author session, and middleware-ordering bugs (e.g., auth-before-context-establishment) recur per-server because there's no single source of truth.
A Common Base Server Class with explicit extension points eliminates this drift, gives each server a clear "what to override" surface, and creates uniform substrate for M6 SDK migration of the Tier-1 servers.
The Architectural Reality
Current Server.mjs shape (each extends
Neo.core.Base):class Server extends Base { static config = { className: 'Neo.ai.mcp.server.X.Server' } mcpServer = null configFile = null createMcpServer() { /* per-server: name, version, capabilities */ } async initAsync() { await super.initAsync(); // load config, init services, wait for ready, healthcheck, connect transport } setupRequestHandlers(mcpServer) { /* per-server: ListTools + CallTool handlers */ } logStartupStatus(health) { /* per-server: warn/info logging */ } } export default Neo.setupClass(Server);All 5 servers already partially share a common shape via
extends Base+static config+initAsync()+Neo.setupClass(). The divergence is in:createMcpServer()— server name + version + capabilitiesinitAsync()— service initialization order + which services to awaitsetupRequestHandlers()— duplicated boilerplate for ListTools/CallTool wiringlogStartupStatus()— per-server health-output formattingExisting shared primitives (service-level, not server-class-level):
ai/mcp/server/shared/BaseConfig.mjs— config substrateai/mcp/server/shared/services/{AuthService,RequestContextService,StdioIdentityResolver,AuthMiddleware,TransportService}.mjsai/mcp/server/shared/helpers/Missing primitive (this ticket):
ai/mcp/server/BaseServer.mjs— class extendingNeo.core.Basewith template-method-pattern extension points:class BaseServer extends Neo.core.Base { static config = { className: 'Neo.ai.mcp.server.BaseServer' } // Lifecycle (final, NOT overridden by per-server): async initAsync() { /* load config, init services, healthcheck, connect transport */ } async start() { /* orchestrates registerXxx() calls in canonical order */ } async stop() { /* graceful shutdown sequence */ } setupRequestHandlers(mcpServer) { /* ListTools + CallTool boilerplate */ } logStartupStatus(health) { /* canonical startup-status logging */ } // Extension points (per-server overrides): getServerMetadata() { /* { name, version, capabilities } */ } registerTools(toolService) { /* per-server tool surface */ } registerServices(serviceRegistry){ /* per-server service wiring */ } registerMiddleware(chain) { /* per-server middleware additions (append, not replace) */ } registerHealthChecks(healthService) { /* per-server healthcheck shape */ } }Each per-server
Server.mjsbecomes a thin extension overriding only the 4-5 hooks. Bootstrap order, transport setup, default middleware, healthcheck-aggregation, and ListTools/CallTool wiring become base-class concerns.The Fix
ai/mcp/server/BaseServer.mjswith the extension points + canonical lifecycle methods + JSDoc@summary. Class extendsNeo.core.Baseper Neo conventions.BaseServer:registerXxx()override; remove duplicated transport/middleware/healthcheck wiring; verify healthcheck output unchanged before/afterlearn/agentos/tooling/Introduction.md(or newMCPServerBaseClass.mdif scope warrants).test/playwright/unit/ai/mcp/server/BaseServer.spec.mjs), plus per-server smoke tests verifying behavior unchanged after migration.Contract Ledger Matrix
Neo.ai.mcp.server.BaseServerclassinitAsync/start/stop/setupRequestHandlerslearn/agentos/tooling/MCPServerBaseClass.md(new) or §extension in Introduction.mdServer.mjsextends BaseServer; unit specs cover extension-point invocationServer.mjs@summarydescribes extension shapegetServerMetadata()createMcpServer()content{ name, version, capabilities }; base class composes McpServer instanceinitAsyncregisterTools(toolService)openapi.yamlregisterServices(serviceRegistry)*/services/initAsync()start()registerMiddleware(chain)shared/services/AuthMiddleware.mjsalready canonicalregisterHealthChecks(healthService)services/HealthService.mjs(varies)Acceptance Criteria
ai/mcp/server/BaseServer.mjscreated, extendsNeo.core.Baseper Neo class-system conventions, has JSDoc@summaryon every method.getServerMetadata,registerTools,registerServices,registerMiddleware,registerHealthChecks. Default implementations are no-ops returning the input collection unchanged (or sentinel forgetServerMetadata).initAsync(),start(),stop()orchestrate the registration flow in deterministic order; per-server overrides cannot reorder. Double-start throws.knowledge-base,memory-core,github-workflow,neural-link) migrated to extendBaseServer.file-systemserver migrated to extendBaseServer(consistency with Tier-1; can defer to a follow-up ticket if Tier-1 surfaces unforeseen complexity).Server.mjsreduced significantly post-migration (rough target: drift below ~80 lines per file is the success signal, not a hard cap; the ~1444 total LOC across 5 Server.mjs files should drop substantially).openapi.yaml).test/playwright/unit/ai/mcp/server/BaseServer.spec.mjscover: (a) extension hook invocation order, (b) middleware aggregation, (c) graceful start/stop sequence, (d) error-on-double-start, (e)getServerMetadatamissing-override throws.learn/agentos/tooling/MCPServerBaseClass.mdOR §-section added tolearn/agentos/tooling/Introduction.mddescribing the extension contract + when to override which hook.Out of Scope
BaseServer(different class hierarchy — daemons vs servers).BaseConfigshape; does NOT change the config layer.aiConfig from './config.mjs'→aiConfigDefaults from './config.template.mjs'+ optional override layer is a separate per-server change. Coordinated with #10964 (clean-checkout deploy bootstrap), which uses Path A (Docker/entrypoint generatesconfig.mjs) for immediate clean-deployment unblock.isPrimaryfor single-writer guard; the REMOTE Factory (RequestContextService+Mcp-Session-Id) operates at a different layer. M2 is class-hierarchy-only, not concurrency-substrate.Avoided Traps / Gold Standards Rejected
Neo.core.Basepatterns elsewhere.registerToolsandregisterServicesinto one hook. Tools and services have different lifecycles — tools are static (defined at openapi.yaml load time); services may be dynamic (initAsync). Separate hooks keep the contract explicit.registerMiddleware. Per-server overrides should APPEND to base middleware, not replace it. Base class controls canonical-order; per-server adds at end.Related
learn/agentos/v13-path.md§4 D2 + §6 M2 (closed PR #10958)package.jsonruntime-deps split + config-bootstrap pathOrigin Session ID:
005b6edf-85d8-4980-9e17-486b6b8bed3fRetrieval Hint:
query_raw_memories(query="MCP server common base class extension points registerTools registerServices registerMiddleware registerHealthChecks v13 M2 milestone Tier-1 cloud-native Tier-2 local-only")