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 thecreateServer()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 existgrep -rn "McpServer\|StdioServerTransport\|registerTool" src/→ zero matchesgrep -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 (singleconsole.errorline per boot step, per existing repo convention).config.COLIBRI_STARTUP_TIMEOUT_MS: number— used bystart()to enforce a hard boot deadline perdocs/2-plugin/boot.md§”Startup Timeout”.assertNoDonorNamespace(env)already runs at module load; importingconfigtransitively enforces theAMS_*absence guard.src/server.tsdoes NOT re-checkAMS_*— 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 onAMS_MODEand on any non-uppercase value. This is the only mode entry point;src/server.tsMUST NOT readCOLIBRI_MODEdirectly.capabilitiesFor(mode)→ frozenModeCapabilitiesrecord withcanWriteDatabase,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 andsrc/server.tsMUST consult it before callingtransport.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.tscan 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 — seesrc/__tests__/config.test.tslines 10-14 + 133-142). - Subprocess harness for eager side effects. Only
src/__tests__/config.test.tsspawns atsxsubprocess withspawnSync, becausesrc/config.tsvalidates env at module-load time. The newsrc/server.tswill deliberately NOT do eager work at import (see §3 boot-order rationale), so subprocess tests are not required. - Explicit env stripping.
beforeEachdeletes everyCOLIBRI_*+AMS_*+NODE_ENVkey from{ ...ORIGINAL_ENV }. The server test follows the same pattern where the server factory reads env; tests that pass env explicitly can skip this. .jsimport extension —from '../server.js'for ESM compatibility under NodeNext resolution. The type-only imports usetypekeyword 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.tsis picked up for coverage automatically. No edit needed.tsconfig.json:include: ['src/**/*'],exclude: ['**/*.test.ts', '**/*.spec.ts']. The test file lands insidesrc/__tests__/which is NOT inexclude— but ts-jest has its own tsconfig override that handles the test transform (seejest.config.tstransformblock). No edit needed..eslintrc.json:overrides[0]for**/*.test.tssetsparserOptions.project: null+ relaxesno-explicit-anyandno-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/sdkStdioServerTransport. 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/sdkperforms the MCP handshake. Client callstools/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 abetter-sqlite3transaction 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/pingorserver_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-processMap<toolName, Promise>. A new call awaits the predecessor’s resolution before proceeding. The lock releases in afinallyafteraudit-exitwrites, 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 viafinally. There is no timeout in Phase 0.” - Stage 2
schema-validate(lines 60-72): “Zod parse failure raises an MCPInvalidParamserror (JSON-RPC code-32602) with the structured Zod error tree flattened into thedatafield. The chain stops beforeaudit-enter— a call that never validated never enters the decision trail.” - Stage 3
audit-enter(lines 76-88): writes anaudit_eventsrow withstep_indexassigned,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 toaudit-exit, which still runs viatry…finallyat chain level. - Stage 5
audit-exit(lines 112-124): writes secondaudit_eventsrow 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:
- SQLite schema ordering:
gsd_projectsbeforetasks. Not this task’s concern (P0.2.2+). dbListTasks()returns{items, total, …}: not this task’s concern (β pipeline).- Never override
process.stdout.write:src/server.tsMUST NOT callprocess.stdout.write = ...or monkey-patchprocess.stdout. All boot-time logging goes throughconsole.error(stderr). The SDK’sStdioServerTransportowns stdout for JSON-RPC. - 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. - Global
unhandledRejection+uncaughtExceptionhandlers:src/server.tsinstalls both at boot. Handlers log viaconsole.errorand 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).Implementationrequires{ name: string, version: string }(+ optionaltitle,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; thetool(...)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). OptionalReadable/Writableconstructor parameters — this is the test-fake seam. For unit tests, the server factory can accept injected streams; for production, the defaults areprocess.stdin/process.stdout.start(): Promise<void>— called byMcpServer.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:
- Read
package.jsonat runtime viareadFile+JSON.parse, resolved viaimport.meta.url→path.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 thedist/vssrc/directory layout at test time. - Bake as build-time const via a tsc
--defineor a smallsrc/version.tsfile updated by a prebuild hook. Pro: no runtime I/O. Con: requires release tooling not yet present (thescripts.buildis plaintsc). - Env var. Pro: trivial. Con: decouples from
package.jsontruth; 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 tomodes.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(thez.object({})forserver_ping). Failure → throwSchemaValidationError(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
finallyblock 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 toconsole.errorwhenCOLIBRI_LOG_LEVELisdebug. 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.mdStage 1 (lines 42-56): pure mutex, “The lock is acquisition-only — it cannot ‘fail’. “docs/2-plugin/modes.mdlines 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 typedToolNotAdmittedErrorat 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 landssrc/db/*independently.src/server.tswill IMPORTinitDb(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 onestart()call; P0.2.3 factors the split out. - P0.2.4 Middleware layer will re-home the inline middleware into
src/middleware/*.tsfiles; 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
AuditSinkinterface and provide acreateZetaAuditSink(db)thatsrc/server.tscan accept increateServer({ 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 tosrc/config.ts;src/server.tsinherits the guard. - Reference
data/ams.dbas 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:
McpServercreated withname: "colibri",versionread frompackage.jsonat runtime.StdioServerTransportis 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 testpasses with MCP handshake integration test (using an in-memory transport fake, not real stdio).start()enforcesCOLIBRI_STARTUP_TIMEOUT_MSviaPromise.race.- Global
unhandledRejection+uncaughtExceptionhandlers installed. AuditSinkinterface exported with a no-op default implementation.- Transport connects BEFORE any heavy init (donor bug #4).
process.stdout.writeis 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.tscoverage.
§11. Summary
- Surface: new module
src/server.ts+ test filesrc/__tests__/server.test.ts. Zero existing server code. - Integration points:
src/config.ts(eager frozenconfigimport),src/modes.ts(detectMode+capabilitiesFor),@modelcontextprotocol/sdk1.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. TheStdioServerTransportconstructor’s optionalReadable/Writableparameters 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 (realAuditSink). - 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.