P0.2.1 — Step 1 Audit

Inventory of the worktree against the task spec for P0.2.1 MCP Server Bootstrap (α System Core task group, first task in that group). This is the first task where the α runtime actually boots: prior Phase 0 PRs (P0.1.1–P0.1.4 + P0.4.1) landed package.json, tsconfig.json, jest.config.ts, src/config.ts, and src/modes.ts, but none of them wire the MCP transport.

Baseline: worktree E:/AMS/.worktrees/claude/p0-2-1-mcp-server/ at commit a64d7349 (P0.4.1 runtime mode enum + capability matrix just landed via PR #119).


§1. Surface being added

Targets this task creates:

  • src/server.ts — new module. Holds the createServer() factory, registerColibriTool() helper, start() entry point, and the 5-stage α middleware assembly. Does not yet exist.
  • src/__tests__/server.test.ts — new test file, co-located with other Phase-0 tests (see §2e for the location reconciliation).

No companion module (src/middleware/*, src/audit-sink.ts) exists. The P0.2.4 middleware-layer targets listed in CLAUDE.md §9.1 are NOT part of this task — P0.2.1 ships the 5-stage chain inline in src/server.ts as composed nested async wrappers, and the separate src/middleware/* files are a P0.2.4 re-home / re-factor. S17 §4 and docs/2-plugin/middleware.md both name src/middleware/tool-lock.ts etc. as “P0.2.4 targets”, not P0.2.1.

A worktree scan confirms absence of the authoring targets:

  • ls src/server.ts → “No such file or directory”
  • ls src/middleware/ → directory does not exist
  • grep -rn "McpServer\|StdioServerTransport\|registerTool" src/ → zero matches
  • grep -rn "server/ping\|server_ping\|AuditSink" src/ → zero matches

This is a greenfield module set.


§2. Adjacent code that the new module must integrate with

2a. src/config.ts (95 lines — P0.1.4, commit 3bd154a7)

Authoritative Phase-0 environment wrapper. The eager config export is the single source of truth the new server must read at boot; the pure loadConfig(env) factory is available for tests. Relevant fields:

  • config.NODE_ENV: 'development' | 'test' | 'production' — influences logging verbosity defaults.
  • config.COLIBRI_DB_PATH: string — consumed by P0.2.2, not by this task.
  • config.COLIBRI_LOG_LEVEL: 'silent' | 'error' | 'warn' | 'info' | 'debug' — consumed by the boot-time log lines (single console.error line per boot step, per existing repo convention).
  • config.COLIBRI_STARTUP_TIMEOUT_MS: number — used by start() to enforce a hard boot deadline per docs/2-plugin/boot.md §”Startup Timeout”.
  • assertNoDonorNamespace(env) already runs at module load; importing config transitively enforces the AMS_* absence guard. src/server.ts does NOT re-check AMS_* — config has already thrown if any is present.

2b. src/modes.ts (186 lines — P0.4.1, commit a64d7349)

Runtime mode enum + capability matrix. The new server imports detectMode(env) and capabilitiesFor(mode) at boot:

  • detectMode(process.env)RuntimeMode{ FULL, READONLY, TEST, MINIMAL }. Throws on AMS_MODE and on any non-uppercase value. This is the only mode entry point; src/server.ts MUST NOT read COLIBRI_MODE directly.
  • capabilitiesFor(mode) → frozen ModeCapabilities record with canWriteDatabase, canAcceptMCPConnections, canDispatchExternalIO, canRunIntegrationTests. Consumed by the tool-lock middleware stage (see §3 for the semantic mismatch to resolve in the contract).
  • Every four modes have canAcceptMCPConnections: true, so for Phase 0 this field is not a boot-gate — but the field exists and src/server.ts MUST consult it before calling transport.connect() so a future non-Phase-0 mode can keep the transport closed without a schema change.
  • Pure: no side effects at import time. src/server.ts can import it unconditionally.

2c. src/__tests__/config.test.ts (207 lines — P0.1.4) and src/__tests__/modes.test.ts (209 lines — P0.4.1)

Establish the test-authoring conventions the new test file MUST follow:

  • Pure-factory testing pattern. Every test passes explicit dependencies (env object, module-under-test export) into a pure function. No jest.isolateModulesAsync (documented broken under ts-jest ESM + zod v3 locale cache — see src/__tests__/config.test.ts lines 10-14 + 133-142).
  • Subprocess harness for eager side effects. Only src/__tests__/config.test.ts spawns a tsx subprocess with spawnSync, because src/config.ts validates env at module-load time. The new src/server.ts will deliberately NOT do eager work at import (see §3 boot-order rationale), so subprocess tests are not required.
  • Explicit env stripping. beforeEach deletes every COLIBRI_* + AMS_* + NODE_ENV key from { ...ORIGINAL_ENV }. The server test follows the same pattern where the server factory reads env; tests that pass env explicitly can skip this.
  • .js import extensionfrom '../server.js' for ESM compatibility under NodeNext resolution. The type-only imports use type keyword per @typescript-eslint/consistent-type-imports.

2d. Jest + TypeScript + ESLint configuration (NOT changing in P0.2.1)

  • jest.config.ts: collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts', '!src/**/__tests__/**']. src/server.ts is picked up for coverage automatically. No edit needed.
  • tsconfig.json: include: ['src/**/*'], exclude: ['**/*.test.ts', '**/*.spec.ts']. The test file lands inside src/__tests__/ which is NOT in exclude — but ts-jest has its own tsconfig override that handles the test transform (see jest.config.ts transform block). No edit needed.
  • .eslintrc.json: overrides[0] for **/*.test.ts sets parserOptions.project: null + relaxes no-explicit-any and no-console. Inherits automatically. No edit needed.
  • package.json: scripts.test = node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage; scripts.lint = eslint src. Both pick up new files without modification.

2e. Test file location reconciliation — tests/ vs src/__tests__/

The task-breakdown row for P0.2.1 writes “Output: src/server.ts, tests/server.test.ts” (line 103). However CLAUDE.md §9.1 lists tests/ only as a “target”, and the actual harness landed under src/__tests__/*.test.ts (P0.1.2 PR #116 decision, confirmed by P0.1.4 + P0.4.1 following suit — three test files all under src/__tests__/).

The discrepancy is between task-breakdown (written early) and the harness as built. Decision: src/__tests__/server.test.ts — matches the three existing test files, matches jest.config.ts’s roots: ['<rootDir>/src'], matches the ESLint override. Using tests/ would require editing Jest roots + ESLint + tsconfig. The contract will record this decision explicitly.

2f. src/index.ts (3 lines)

Placeholder (export {}). P0.2.1 re-homes the entry point: src/index.ts will import start() from src/server.ts and call it, OR the package.json "main" + "bin" targets already point at dist/server.js so src/server.ts IS the entry when compiled. The contract will specify which.

package.json currently has:

"main": "dist/server.js",
"bin": { "colibri": "dist/server.js" }

So the compiled artifact is dist/server.js (i.e. from src/server.ts), and src/index.ts is vestigial. The task may either (a) delete src/index.ts, (b) leave it as-is (placeholder), or (c) have src/index.ts re-export from src/server.ts. Contract §1 will decide.


§3. Spec documents consulted

3a. docs/spec/s17-mcp-surface.md (123 lines)

Authority on transport and middleware order:

  • §2 Transport (lines 33-39): “@modelcontextprotocol/sdk StdioServerTransport. JSON-RPC over stdin/stdout. Language: TypeScript 5.3+, ESM, NodeNext. Entry point: src/server.ts (P0.2.1). No HTTP surface. No dashboard, no REST endpoint, no websocket — not in Phase 0.”
  • §3 Tool lifecycle (lines 41-47): “Client launches the server as a subprocess and connects via stdio. @modelcontextprotocol/sdk performs the MCP handshake. Client calls tools/list → the server returns the 19 tool definitions with name, description, and Zod-derived JSON schema. Client calls a tool → the middleware chain executes → the handler runs inside a better-sqlite3 transaction as needed → the response is returned. Every call is audited at both enter and exit.”
  • §4 Middleware chain (lines 49-65): canonical order tool-lock → validate → audit enter → dispatch → audit exit. Stage 1 description: “Serializes handlers; a handler cannot run concurrently with another” — pure mutex semantics, NOT capability-gate.
  • §6 Response shape (lines 74-87): uniform envelope { ok: true, data: {...} } for success; { ok: false, error: { code, message, details? } } for failure.
  • §8 Acceptance criteria (lines 100-110): notes that Phase 0 end-state is 19 tools registered, each audited, each passing the full 5-stage chain. P0.2.1 lands one (server/ping or server_ping — see §4a) as the first; the remaining 18 are handed to P0.3–P0.9.

3b. docs/2-plugin/middleware.md (160 lines)

More detailed middleware contract. Relevant:

  • Stage 1 tool-lock (lines 42-56): “Serialize concurrent invocations of the same tool name… In-process Map<toolName, Promise>. A new call awaits the predecessor’s resolution before proceeding. The lock releases in a finally after audit-exit writes, so the serialisation boundary is the whole chain, not just the handler… The lock is acquisition-only — it cannot ‘fail’. If the handler throws, the lock still releases via finally. There is no timeout in Phase 0.”
  • Stage 2 schema-validate (lines 60-72): “Zod parse failure raises an MCP InvalidParams error (JSON-RPC code -32602) with the structured Zod error tree flattened into the data field. The chain stops before audit-enter — a call that never validated never enters the decision trail.”
  • Stage 3 audit-enter (lines 76-88): writes an audit_events row with step_index assigned, event_type = 'tool_enter', args_hash = sha256(stableSerialize(args)). Failure is hard-stop.
  • Stage 4 dispatch (lines 92-106): direct-lookup dispatch (no reflection). Exceptions propagate outward to audit-exit, which still runs via try…finally at chain level.
  • Stage 5 audit-exit (lines 112-124): writes second audit_events row linking back to the entry. Failure is hard-stop.

3c. docs/2-plugin/modes.md (71 lines)

CONTRADICTION WITH 3a/3b. Lines 26-28: “The tool-lock stage of the α chain (stage 1 of tool-lock → schema-validate → audit-enter → dispatch → audit-exit) consults the active mode. A request whose tool is not in the admitted set is rejected with a typed ToolNotAdmittedError at the lock stage, before schema validation, dispatch, or auditing run.”

So modes.md says stage 1 is concurrency mutex AND capability-gate combined, while middleware.md + s17 say stage 1 is purely concurrency mutex. This is a real spec contradiction, flagged to T0 in the packet.

3d. docs/2-plugin/boot.md (232 lines)

Authority on the 6-step boot sequence. Verbatim:

Step 1: Create Server · Step 2: Register Handlers · Step 3: Connect Transport · Step 4: Resolve initReady · Step 5: Load Database · Step 6: Load Domains

Critical note at line 65: “Why transport first? The MCP handshake has a timeout (typically 10-30 seconds, depending on the client). If we wait to connect the transport until after the database is open, and the database takes longer than the timeout to initialize, the client gives up and closes the connection. By connecting first, we guarantee the handshake completes before the database initialization begins.”

This is the canonical statement of donor bug #4 (transport-before-heavy-init). The P0.2.1 server MUST implement steps 1-3 in order; steps 5-6 are P0.2.2+.

Lines 122-145 define a Promise gate: tool handlers await dbReadyPromise at the top, so calls arriving during the boot-interim are queued rather than failed. P0.2.1 SHIPS the gate plumbing (as a Promise<void> field on the server context that resolves to void immediately in P0.2.1 because there’s no DB yet), but the actual DB wiring is P0.2.2.

Exit codes (lines 149-174):

Code Meaning
0 Clean shutdown
1 Generic crash
73 Config error
75 Resource error

src/server.ts in P0.2.1 uses only 0/1 (no DB yet); 73 is the config layer’s job (src/config.ts throws, uncaught throw → node exits with 1, but we’ll install the global handlers in §3f below so exit code is more structured).

3e. docs/architecture/decisions/ADR-004-tool-surface.md (192 lines)

Authority on tool count. Phase 0 target is exactly 19 tools. P0.2.1 lands server_ping (ADR-004 line 67 says server_ping, S17 §1 says server_ping). However task-breakdown line 108 says "server/ping" and the p0.2 task-prompt line 63 says "server/ping" — with a slash. Name reconciliation:

  • ADR-004 Phase-0 inventory table (authoritative for naming, line 67): server_ping
  • S17 §1 tool table: calls the category but lists thought_record, merkle_root, server_ping (underscore) style throughout
  • Task-breakdown line 108 + task-prompt: server/ping (slash)

The task-breakdown style with a slash is a snake-case-with-namespace convention, the ADR-004 style without is pure snake_case. Both can work in MCP (tool names are opaque strings), but Phase 0 doc surface predominantly uses snake_case. Decision: server_ping (snake_case, per ADR-004 + S17) — the task-prompt appears to be an older draft with server/ping. Contract records the decision.

3f. AMS donor bugs to mitigate (memory index)

Five donor bugs cited in the dispatch brief:

  1. SQLite schema ordering: gsd_projects before tasks. Not this task’s concern (P0.2.2+).
  2. dbListTasks() returns {items, total, …}: not this task’s concern (β pipeline).
  3. Never override process.stdout.write: src/server.ts MUST NOT call process.stdout.write = ... or monkey-patch process.stdout. All boot-time logging goes through console.error (stderr). The SDK’s StdioServerTransport owns stdout for JSON-RPC.
  4. MCP transport connects BEFORE heavy init: s17 §2 + boot.md §”Why transport first?”. transport.connect() runs before any later steps that could block the event loop.
  5. Global unhandledRejection + uncaughtException handlers: src/server.ts installs both at boot. Handlers log via console.error and exit with code 1. This prevents silent crashes and surfaces stack traces to the client.

3g. @modelcontextprotocol/sdk (resolved to 1.29.0)

node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.d.ts:

  • class McpServer { constructor(serverInfo: Implementation, options?: ServerOptions) } (line 24). Implementation requires { name: string, version: string } (+ optional title, websiteUrl, description, icons).
  • connect(transport: Transport): Promise<void> (line 40).
  • close(): Promise<void> (line 44).
  • registerTool<OutputArgs, InputArgs>(name: string, config: { title?, description?, inputSchema?, outputSchema?, annotations?, _meta? }, cb: ToolCallback<InputArgs>): RegisteredTool (line 150). This is the current API; the tool(...) overloads at lines 112-146 are all marked @deprecated.
  • isConnected(): boolean (line 190).
  • server: Server — the underlying low-level server for advanced use.

node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.d.ts:

  • class StdioServerTransport implements Transport { constructor(_stdin?: Readable, _stdout?: Writable) } (line 14). Optional Readable/Writable constructor parameters — this is the test-fake seam. For unit tests, the server factory can accept injected streams; for production, the defaults are process.stdin / process.stdout.
  • start(): Promise<void> — called by McpServer.connect(), not directly by user code.
  • send(message: JSONRPCMessage): Promise<void> — used internally.

Import path per package.json#exports: "@modelcontextprotocol/sdk/server/mcp.js" + "@modelcontextprotocol/sdk/server/stdio.js". The SDK package exports the ./server subpath; under NodeNext the .js extension is mandatory.


§4. The server_ping tool — concrete shape

4a. Name

server_ping (snake_case, no slash) per ADR-004 + S17 §1. The task-breakdown + task-prompt "server/ping" text is treated as a heritage draft and overridden here; contract records the decision.

4b. Input schema

Zod z.object({}) — no parameters.

4c. Output schema

Per the dispatch brief + task-breakdown line 108 ({ status: "ok", version }) plus Sigma refinements (mode, uptime_ms):

{
  ok: true,
  data: {
    version: string,       // e.g. "0.0.1"
    mode: RuntimeMode,     // e.g. "FULL"
    uptime_ms: number,     // Math.floor(performance.now() - bootStart)
  },
}

The outer envelope { ok, data } matches s17 §6. version is a string from package.json; mode is the frozen boot-time RuntimeMode; uptime_ms is a non-negative integer (floor to avoid fractional ms leaking into the wire format).

4d. Version source — decision space

The dispatch brief explicitly asks for this. Three options:

  1. Read package.json at runtime via readFile + JSON.parse, resolved via import.meta.urlpath.resolve(__dirname, '../package.json'). Pro: single source of truth. Con: adds filesystem I/O to the hot path (on boot only — still non-zero), requires path resolution under NodeNext ESM, coupling to the dist/ vs src/ directory layout at test time.
  2. Bake as build-time const via a tsc --define or a small src/version.ts file updated by a prebuild hook. Pro: no runtime I/O. Con: requires release tooling not yet present (the scripts.build is plain tsc).
  3. Env var. Pro: trivial. Con: decouples from package.json truth; one more env var to document and guard with Zod.

Recommendation in the contract: option 1 (read package.json at boot) — one-time filesystem hit, tsx/ts-jest can resolve ../package.json via the compiler’s module resolution, and import.meta.url is available under "module": "NodeNext". Option 3 is rejected because it adds env noise for no gain. Option 2 is deferred to a later packaging task.

The contract will lock in option 1; the packet will specify the exact resolution idiom.

4e. No AMS_* env dependency

Per s17 acceptance criterion 7 (line 110): “No tool depends on any AMS_* environment variable or any src/*.js file.” The server’s env reads go through config (from src/config.ts) which has already guarded AMS_*; server.ts MUST NOT read process.env.* directly.


§5. 5-stage middleware — how it’s implemented in P0.2.1

5a. Location: inline in src/server.ts, not yet in src/middleware/

CLAUDE.md §9.1 lists src/middleware/ as a P0.2.4 target. src/server.ts in P0.2.1 ships the 5 stages as composed nested async functions inside the registerColibriTool() helper — one module, one file. P0.2.4 will split them into src/middleware/tool-lock.ts etc. without touching src/server.ts’s API.

5b. Stage ordering (canonical, per s17 §4 + middleware.md)

tool-lock → schema-validate → audit-enter → dispatch → audit-exit

Every stage runs on every call. No feature flags.

5c. Stage-by-stage semantics (authorities cross-cut)

  • Stage 1 tool-lock. Per-tool mutex via Map<toolName, Promise<void>>. Serializes concurrent invocations of the same tool. Does NOT gate on mode capabilities in P0.2.1 — that is deferred to modes.md-aligned treatment in P0.2.3 (two-phase startup) or P0.4.2. See §7 for the spec contradiction.
  • Stage 2 schema-validate. Zod parse against inputSchema (the z.object({}) for server_ping). Failure → throw SchemaValidationError (custom class); chain stops before stage 3.
  • Stage 3 audit-enter. Calls auditSink.enter({ tool, args, timestamp, correlationId }). Failure → throw; chain stops.
  • Stage 4 dispatch. Calls the user-provided handler. Exceptions propagate outward.
  • Stage 5 audit-exit. In a finally block at the chain level. Always runs, logs result OR error, releases the tool-lock mutex.

5d. Audit-sink seam design (for P0.7 ζ Decision Trail)

ζ is P0.7 and does not exist yet. P0.2.1 ships a pluggable AuditSink interface + a no-op default. The interface shape:

interface AuditSink {
  enter(event: {
    tool: string;
    args: unknown;
    timestamp: number;
    correlationId: string;
  }): Promise<void> | void;
  exit(event: {
    tool: string;
    correlationId: string;
    durationMs: number;
    result?: unknown;
    error?: Error;
  }): Promise<void> | void;
}

Two default implementations:

  • createNoOpAuditSink() — returns immediately. Used in production MINIMAL / FULL modes until ζ lands.
  • createConsoleAuditSink() — writes to console.error when COLIBRI_LOG_LEVEL is debug. Optional; could be deferred.

P0.7 lands a createZetaAuditSink(db) implementation that writes to the audit_events table. Swapping implementations is done by passing a different sink to createServer({ auditSink }).

The contract §2 will finalize the exact interface shape; T0 is asked to sign off on it in the packet.


§6. Boot sequence — P0.2.1 scope within the 6-step canonical sequence

boot.md defines 6 steps; P0.2.1 ships 1-4 plus no-op-versions of 5-6:

boot.md step P0.2.1 behavior
1. Create Server new McpServer({ name: 'colibri', version })
2. Register Handlers registerColibriTool('server_ping', ...) — 1 tool only; P0.3+ adds the other 18
3. Connect Transport new StdioServerTransport() + server.connect(transport) — BEFORE DB init (donor bug #4)
4. Resolve initReady await server.isConnected() poll OR just trust connect() resolved
5. Load Database No-op in P0.2.1 — the Promise gate resolves immediately; P0.2.2 fills this in
6. Load Domains No-op in P0.2.1 — 0 domain handlers registered; P0.3+ fills this in

The start() function wraps the whole sequence in a Promise.race against a setTimeout(COLIBRI_STARTUP_TIMEOUT_MS) to enforce the hard deadline per boot.md §”Startup Timeout”.

6a. Global handlers

Install immediately at the top of start():

process.on('unhandledRejection', (reason) => {
  console.error('[colibri] unhandledRejection:', reason);
  process.exit(1);
});
process.on('uncaughtException', (err) => {
  console.error('[colibri] uncaughtException:', err);
  process.exit(1);
});

These are installed exactly once per process; under Jest the test harness owns the process (no start() call), so the handlers are installed only by the live entry path.


§7. Spec contradictions discovered

7a. Tool-lock stage: concurrency mutex OR capability-gate?

  • docs/spec/s17-mcp-surface.md §4 (line 55): “Serializes handlers; a handler cannot run concurrently with another”
  • docs/2-plugin/middleware.md Stage 1 (lines 42-56): pure mutex, “The lock is acquisition-only — it cannot ‘fail’. “
  • docs/2-plugin/modes.md lines 26-28: “The tool-lock stage of the α chain… consults the active mode. A request whose tool is not in the admitted set is rejected with a typed ToolNotAdmittedError at the lock stage”

Two different specs disagree. modes.md conflates “tool-lock” with capability-gating; middleware.md and s17 treat tool-lock as pure mutex.

Audit decision: tool-lock in P0.2.1 is pure mutex, matching s17 + middleware.md (two of three; the authoritative layer for α is docs/2-plugin/, and middleware.md is the full spec; modes.md is a modes surface that leans on the chain). Capability-gating is a SEPARATE concern that either (a) happens at tool registration time (a tool not admitted by the current mode simply never gets a registered handler) or (b) gets added as a sixth implicit stage in P0.4.2 or P0.2.3.

For P0.2.1 concretely: server_ping is in MINIMAL mode per modes.md (“MINIMAL: 2 (server_ping, server_info)” — at time of audit; R75 Wave H later struck server_info and the MINIMAL pair is now server_ping, server_health), so in P0.2.1 every mode admits it. There is no case in P0.2.1 where a tool must be denied on capability grounds. This lets us cleanly ship stage 1 as pure mutex and defer the capability-gate question to a later task.

The contract §2 flags this to T0 as an Open Question and proposes the pure-mutex reading; T0 can direct a different reading before packet approval.

7b. Tool name: server_ping vs server/ping

ADR-004 + S17 use snake_case (server_ping); task-breakdown + task-prompt use server/ping. Audit decision: server_ping (snake_case). Rationale: ADR-004 is the authority on tool-count + inventory naming. Contract §3 records this.

7c. Test file location: tests/ vs src/__tests__/

task-breakdown says tests/server.test.ts (line 103); existing harness uses src/__tests__/. Audit decision: src/__tests__/server.test.ts. Contract §1 records this.


§8. Consumers — who uses what src/server.ts exports

Upstream / downstream dependency map:

  • P0.2.2 SQLite Initialization will NOT import from src/server.ts — it lands src/db/* independently. src/server.ts will IMPORT initDb(path) from P0.2.2 in a FUTURE task (P0.2.3 two-phase startup wires DB open inside the boot sequence). P0.2.1 ships without the DB import; P0.2.3 adds it.
  • P0.2.3 Two-phase startup will split start() into two stages: start() runs stages 1-4 and returns; startHeavyInit(context) runs stages 5-6. P0.2.1 ships both-together in one start() call; P0.2.3 factors the split out.
  • P0.2.4 Middleware layer will re-home the inline middleware into src/middleware/*.ts files; P0.2.1 does not touch that directory.
  • P0.3+ domain handlers will call registerColibriTool(name, { inputSchema, outputSchema }, handler) — the exported helper is the stable API.
  • P0.7 ζ Decision Trail will import the AuditSink interface and provide a createZetaAuditSink(db) that src/server.ts can accept in createServer({ auditSink }).

§9. Forbidden-phrase check

Per contract pattern from P0.4.1 and CI guard in .github/workflows/ci.yml (P0.1.3 forbidden-term scan), Phase-0 code MUST NOT:

  • Read any AMS_* env variable. Enforcement is delegated to src/config.ts; src/server.ts inherits the guard.
  • Reference data/ams.db as a live target (not touching the DB at all in P0.2.1).
  • Cite “78 tables”, “484 tools”, “11 middleware” as Colibri facts (referenced only in heritage notes).
  • Override process.stdout.write.
  • Use HTTP / WebSocket transport (stdio only per s17 §2).

§10. Acceptance-criteria inventory

Lifted from docs/guides/implementation/task-breakdown.md §P0.2.1 lines 101-110, refined by the Sigma dispatch brief:

  • McpServer created with name: "colibri", version read from package.json at runtime.
  • StdioServerTransport is the only transport in Phase 0 per S17 §2. No HTTP, no WebSocket imports.
  • Server exports registerColibriTool(name, config, handler) helper that composes the 5-stage α middleware chain: tool-lock → schema-validate → audit-enter → dispatch → audit-exit.
  • At least 1 registered tool: server_ping → returns { ok: true, data: { version, mode, uptime_ms } }.
  • npm test passes with MCP handshake integration test (using an in-memory transport fake, not real stdio).
  • start() enforces COLIBRI_STARTUP_TIMEOUT_MS via Promise.race.
  • Global unhandledRejection + uncaughtException handlers installed.
  • AuditSink interface exported with a no-op default implementation.
  • Transport connects BEFORE any heavy init (donor bug #4).
  • process.stdout.write is NOT monkey-patched (donor bug #3).
  • 100% statement + function + line coverage on src/server.ts; ≥ 90% branches.
  • No regression on src/config.ts / src/modes.ts coverage.

§11. Summary

  • Surface: new module src/server.ts + test file src/__tests__/server.test.ts. Zero existing server code.
  • Integration points: src/config.ts (eager frozen config import), src/modes.ts (detectMode + capabilitiesFor), @modelcontextprotocol/sdk 1.29.0 (McpServer, StdioServerTransport, registerTool).
  • Test pattern: pure-factory + dependency-injection. createServer({ auditSink, transport? }) returns a server context that tests can exercise without touching real stdio. The StdioServerTransport constructor’s optional Readable/Writable parameters are the fake-stream seam; tests use PassThrough streams.
  • Unblocks: P0.2.2 (DB init — orthogonal), P0.2.3 (two-phase startup — splits start()), P0.2.4 (middleware re-home), P0.3+ (domain tool registration), P0.7 (real AuditSink).
  • Out of scope: SQLite DB wiring (P0.2.2), real ζ trail (P0.7), middleware file split (P0.2.4), HTTP transport (never per s17), additional tools beyond server_ping (P0.3+).
  • Spec contradictions surfaced: tool-lock semantics (§7a), tool-naming style (§7b), test-file location (§7c). All three resolved in the contract with T0 asked to sign off on the tool-lock reading specifically.

P0.2.1 Step 1 Audit — 2026-04-16 — branch feature/p0-2-1-mcp-server @ baseline a64d7349. Next: Step 2 Contract.


Back to top

Colibri — documentation-first MCP runtime. Apache 2.0 + Commons Clause.

This site uses Just the Docs, a documentation theme for Jekyll.