LearnNewsExamplesServices
Frontmatter
id10965
titleMCP server common base class with standardized extension points
stateClosed
labels
enhancementaiarchitecture
assigneesneo-opus-4-7
createdAtMay 8, 2026, 5:02 PM
updatedAtMay 12, 2026, 4:09 AM
githubUrlhttps://github.com/neomjs/neo/issues/10965
authorneo-opus-4-7
commentsCount1
parentIssue10960
subIssues
10980 Document MCP server BaseServer extension contract
subIssuesCompleted1
subIssuesTotal1
blockedBy[]
blocking[]
closedAtMay 8, 2026, 7:46 PM

MCP server common base class with standardized extension points

Closedenhancementaiarchitecture
neo-opus-4-7
neo-opus-4-7 commented on May 8, 2026, 5:02 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):

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-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 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:

  1. Creates the MCP transport layer (McpServer from @modelcontextprotocol/sdk)
  2. Wires setupRequestHandlers(mcpServer) for ListToolsRequestSchema + CallToolRequestSchema
  3. Wires services (DatabaseService, HealthService, KBRecorderService, etc.)
  4. Loads aiConfig from ./config.mjs (gitignored; cloned from config.template.mjs at npm prepare)
  5. Runs healthcheck + log startup status
  6. 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):

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 + capabilities
  • initAsync() — service initialization order + which services to await
  • setupRequestHandlers() — duplicated boilerplate for ListTools/CallTool wiring
  • logStartupStatus() — per-server health-output formatting
  • Transport selection — duplicated stdio-vs-sse branching

Existing shared primitives (service-level, not server-class-level):

  • ai/mcp/server/shared/BaseConfig.mjs — config substrate
  • ai/mcp/server/shared/services/{AuthService,RequestContextService,StdioIdentityResolver,AuthMiddleware,TransportService}.mjs
  • ai/mcp/server/shared/helpers/

Missing primitive (this ticket): ai/mcp/server/BaseServer.mjs — class extending Neo.core.Base with 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.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

  1. Create ai/mcp/server/BaseServer.mjs with the extension points + canonical lifecycle methods + JSDoc @summary. Class extends Neo.core.Base per Neo conventions.
  2. 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
  3. Document the extension contract in learn/agentos/tooling/Introduction.md (or new MCPServerBaseClass.md if scope warrants).
  4. 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 If migration breaks, revert per-server file Per-server JSDoc @summary describes extension shape Per-server smoke tests verify healthcheck + tool surface unchanged
getServerMetadata() Per-server createMcpServer() content Returns { name, version, capabilities }; base class composes McpServer instance If override missing, base throws at initAsync JSDoc McpServer instance has correct name/version/capabilities
registerTools(toolService) OpenAPI 3.0 spec already canonical Per-server populates toolService from its openapi.yaml If no openapi.yaml, override returns no-op Tool-registration guide OpenAPI spec parses; tool count in healthcheck matches openapi.yaml operations
registerServices(serviceRegistry) Per-server services in */services/ Per-server registers domain services + invokes initAsync() If a service fails initAsync, throw at start() JSDoc Service count in healthcheck matches per-server expectations
registerMiddleware(chain) shared/services/AuthMiddleware.mjs already canonical Base class adds auth + request-context middleware in canonical order; per-server appends Per-server cannot reorder base middleware JSDoc Middleware order assertion in unit specs
registerHealthChecks(healthService) Per-server services/HealthService.mjs (varies) Per-server contributes healthcheck blocks; base class aggregates If override returns nothing, base provides empty block JSDoc Healthcheck JSON shape unchanged before/after migration

Acceptance Criteria

  • 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).
  • AC9: Unit specs at test/playwright/unit/ai/mcp/server/BaseServer.spec.mjs cover: (a) extension hook invocation order, (b) middleware aggregation, (c) graceful start/stop sequence, (d) error-on-double-start, (e) getServerMetadata missing-override throws.
  • 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.

Related

  • Parent epic: #10960 — v13 release tracking (canonical M-milestone tracking)
  • Grandparent epic: #9999 — Cloud-Native Knowledge & Multi-Tenant Memory Core (v13 main)
  • Strategic anchor: learn/agentos/v13-path.md §4 D2 + §6 M2 (closed PR #10958)
  • Sibling milestones:
    • #10956 (M3 Orchestrator daemon, @neo-gemini-3-1-pro)
    • #10952 (M1 deployment integration, closed via #10962)
    • #10964 (M1 deployment hardening, @neo-gpt) — coordinates with M2 on package.json runtime-deps split + config-bootstrap path
  • Adjacent (informational, NOT blockers):
    • #10822 (config-substrate cleanup)
    • #10186 (MCP concurrency audit)

Origin Session ID: 005b6edf-85d8-4980-9e17-486b6b8bed3f

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