Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,164 @@
# Design — enterprise-mcp
## Goal
Land `@suaveplan/mcp@0.1.0` as a universal-tier MCP host-side library implementing the Model Context Protocol's full method surface over a pluggable transport.
## Architecture
```text
┌───────────────────────────┐ ┌───────────────────────────┐
│ MCPClient │ │ MCPServer │
│ (consumer of remote tools)│ │ (owner of tools/resources)│
└─────────────┬─────────────┘ └─────────────┬─────────────┘
│ │
▼ ▼
┌────────────────┐ send(JsonRpcMessage) ┌────────────────┐
│ JsonRpcDispatch│ ◄──────────────────────────►│ JsonRpcDispatch│
└────────┬───────┘ └────────┬───────┘
│ │
▼ ▼
┌────────────────┐ ── Uint8Array frames ──► ┌────────────────┐
│ Transport │ ◄──────────────────────────►│ Transport │
│ (in-mem|stdio| │ │ (in-mem|stdio| │
│ http+sse) │ │ http+sse) │
└────────────────┘ └────────────────┘
```
Both ends share the same `JsonRpcDispatcher`. The dispatcher owns id allocation, the pending-request map (`Map<JsonRpcId, { resolve, reject, signal }>`), method handler registry, batch+notification handling, and timeout enforcement. `MCPServer` and `MCPClient` are thin domain wrappers around a dispatcher: they expose registration / call surfaces and translate method names ↔ Zod-validated params.
## Universal-tier transport strategy
`stdio` and `http+sse` would naively pull Node/browser-only modules into a universal package. We dodge that by structurally typing the transport's dependencies:
- `StdioTransport` accepts `readable: AsyncIterable<Uint8Array>` and `writable: { write(chunk: Uint8Array): Promise<void>; close(): Promise<void> }`. Consumers wire `Readable.toWeb(process.stdin)`, `Bun.stdin.stream()`, or any other source.
- `HttpSseTransport` accepts `fetchImpl: typeof fetch` and `eventSourceImpl: EventSourceCtor`. Production code can default both to `globalThis.fetch` and `globalThis.EventSource`; the package itself imports neither.
This is the same dependency-injection pattern `@suaveplan/health-check` uses for its HTTP / TCP probes.
## JSON-RPC framing
MCP rides JSON-RPC 2.0:
```jsonc
{ "jsonrpc": "2.0", "id": "req-1", "method": "tools/call", "params": { … } }
{ "jsonrpc": "2.0", "id": "req-1", "result": { … } }
{ "jsonrpc": "2.0", "id": "req-1", "error": { "code": -32601, "message": "Method not found" } }
{ "jsonrpc": "2.0", "method": "notifications/progress", "params": { … } } // no id => notification
```
Standard error codes from JSON-RPC 2.0 (`-32700` parse, `-32600` invalid request, `-32601` method-not-found, `-32602` invalid-params, `-32603` internal) plus MCP-reserved codes (`-32001` unauthorized, `-32002` request-cancelled). The dispatcher exposes these as constants and maps them to `MCP_CODES` namespace strings.
## Method registry
The dispatcher carries `handlers: Map<MethodName, (params, ctx) => Promise<unknown>>`. `MCPServer.start(transport)` registers handlers for all server-side methods; `MCPClient.connect(transport)` registers handlers for sampling, roots, and notification listeners. Symmetric handler registration means the same dispatcher code runs on both ends.
## Abort + progress
When a caller passes an `AbortSignal`:
1. The dispatcher sends a `notifications/cancelled` message with the request id.
2. The pending-request entry is rejected with `MCPError(REQUEST_CANCELLED)`.
3. Server-side: on receiving `notifications/cancelled`, the dispatcher looks up the in-flight handler invocation by id and aborts its `AbortController`. The handler observes the abort and stops.
Progress is the inverse: when a tool handler is invoked it receives a `progress(token, value, total?)` callback. Calling it sends a `notifications/progress` to the client. The client's `callTool` accepts a `onProgress` callback in its options bag.
## Resource subscriptions
`MCPServer.markResourceUpdated(uri)` fans out `notifications/resources/updated` to every subscribed dispatcher session. Subscriptions live in `Map<DispatcherId, Set<URI>>`. The server tracks dispatcher ids on transport attach.
## File layout
```text
packages/devtools/tooling/mcp/
package.json
README.md
tsconfig.lib.json
vite.config.ts
vitest.config.ts
src/
index.ts # barrel — explicit named exports
types/types.ts # shared scalar types (JsonRpcId, ProgressToken, Cursor, MethodName)
constants/index.ts # method-name constants, default protocol version
error-codes/error-codes.ts (.md, .test.ts)
schemas/schemas.ts (.md, .test.ts) # every Zod schema for wire types
telemetry/telemetry.ts (.md, .test.ts)
events/events.ts (.md, .test.ts)
jsonrpc/
dispatcher/
dispatcher.ts (.md, .types.ts, .test.ts, index.ts)
transport/
transport/
transport.ts (.md, .types.ts, index.ts)
in-memory/
in-memory.ts (.md, .test.ts, index.ts)
stdio/
stdio.ts (.md, .test.ts, index.ts)
http-sse/
http-sse.ts (.md, .test.ts, index.ts)
server/
mcp-server/
mcp-server.ts (.md, .types.ts, .test.ts, index.ts)
client/
mcp-client/
mcp-client.ts (.md, .types.ts, .test.ts, index.ts)
e2e/
handshake.test.ts # full server↔client over in-memory + stdio (no production .ts)
```
## Test strategy
- Per-feature unit tests cover happy + error paths exhaustively (100 % coverage gate).
- `e2e/handshake.test.ts` wires a `MCPServer` + `MCPClient` over `InMemoryTransport.pair()` and walks initialize → tools/list → tools/call (with progress + abort) → resources/list → resources/read → resources/subscribe → mark-updated → notification observed → prompts/list → prompts/get → completion/complete → sampling/createMessage → roots/list → logging/setLevel → disconnect.
- Stdio transport tested with two `ReadableStream` / `WritableStream` pairs in memory — no real `process.stdin`.
- HTTP+SSE transport tested with injected `fetchImpl` that returns canned responses and `eventSourceImpl` that emits events from a queue.
## Error code budget
```
MCP.PROTOCOL_INVALID - malformed JSON-RPC envelope
MCP.METHOD_NOT_FOUND - method not registered
MCP.INVALID_PARAMS - Zod validation failure on params
MCP.INTERNAL_ERROR - handler threw
MCP.UNAUTHORIZED - authorize() returned false
MCP.REQUEST_CANCELLED - AbortSignal aborted
MCP.TIMEOUT - request exceeded timeoutMs
MCP.TRANSPORT_CLOSED - send() after close()
MCP.TRANSPORT_ERROR - underlying transport raised
MCP.NOT_INITIALIZED - method called before initialize handshake
MCP.ALREADY_CONNECTED - connect() called twice
MCP.DUPLICATE_REGISTRATION - tool/resource/prompt name collision
MCP.NOT_FOUND - lookup miss (tool, resource, prompt, root)
MCP.PROTOCOL_VERSION_MISMATCH - initialize negotiated incompatible version
MCP.HANDLER_FAILED - generic handler raise (wraps cause)
```
15 codes — every one reachable from a real call site exercised by tests (per AGENTS §0a parity rule and rule 7).
## Telemetry
Meter name: `@suaveplan/mcp`. Instruments:
- `mcp_request_total` counter — `{ direction, method, status }` (status: success | error | cancelled).
- `mcp_notification_total` counter — `{ direction, method }`.
- `mcp_request_duration_ms` histogram — `{ direction, method }`.
`direction` is `client` (initiated by `MCPClient`) or `server` (initiated by `MCPServer`).
## EventBus payloads
`MCPEventMap`:
```
"connection:opened" -> { side: "client" | "server", dispatcherId: string }
"connection:closed" -> { side; dispatcherId; reason?: string }
"request:received" -> { side; method: string; id: JsonRpcId }
"request:completed" -> { side; method; id; durationMs: number; outcome: "success" | "error" | "cancelled" }
"notification:emitted" -> { side; method; params: unknown }
```
## Trade-offs
- **No WebSocket today.** Adding it would force a pre-emptive port of `@suaveplan/network-websockets` (multi-day work). The Transport interface accepts the eventual adapter without source changes.
- **Newline-delimited JSON over stdio** is the MCP spec default. It cannot frame embedded newlines inside string content, which Zod-encoded JSON never produces (JSON strings escape control characters), so this is safe.
- **HTTP+SSE rather than streamable-http.** MCP 2024-11-05 supports both; SSE is the more widely deployed and is straightforward over injected `EventSource`. A future change can add `streamable-http`.

View File

@@ -0,0 +1,175 @@
# OpenSpec Change: enterprise-mcp
- **Change ID**: enterprise-mcp
- **Package**: `@suaveplan/mcp`
- **Category**: devtools / tooling
- **Target path**: `packages/devtools/tooling/mcp`
- **Status**: PROPOSED
- **Date**: 2026-05-25
- **Reference spec**: `/mnt/s/web/suaveplan/enterprise/mcp.md`
- **Design**: ./design.md
- **Spec delta**: ./specs/mcp/spec.md
---
## Summary
Deliver `@suaveplan/mcp` on the genesis branch from scratch at enterprise quality. The package provides a Model Context Protocol (MCP) server and client implementation. `MCPServer` lets host apps register named tools, URI-addressed resources, and prompt templates; `MCPClient` mirrors the surface from the consumer side. Genesis ships the existing scaffold (42 tests, high coverage, Zod configs) plus: real transport implementations (stdio, SSE, WebSocket), `AbortSignal` cancellation, streaming tool results (`invokeToolStream` / `callToolStream`), a tool/resource ACL hook, all DTOs derived from Zod schemas, OTel spans per invocation, and an E2E test wiring server + client over stdio. `"private": true` is removed; the package is published to Verdaccio.
---
## Current-state audit (genesis)
Does not exist on genesis — fresh build (the main-branch package was a non-published scaffold; genesis rewrites it cleanly per the enterprise spec).
---
## Available workspace deps on genesis
| Package | Role |
|---|---|
| `@suaveplan/error` (`workspace:^`) | `MCPError` / `MCPToolError` / `MCPTransportError` hierarchy |
| `@suaveplan/types` (`workspace:^`) | `LoggerLike` |
| `@suaveplan/events` (`workspace:^`) | Resource-change notifications |
| `@suaveplan/logger` (`workspace:^`) | Structured logs |
| `zod` (`catalog:forms`) | Single source of truth for all wire types |
| `@opentelemetry/api` (`catalog:observability`) | `mcp.*` spans per invocation |
| `tslib` (`catalog:build`) | Runtime helpers |
---
## Public API surface (from main + genesis additions)
```ts
// Server
class MCPServer {
constructor(config: MCPServerConfig)
registerTool(definition: MCPToolDefinition): void
unregisterTool(name: string): void
registerResource(definition: MCPResourceDefinition): void
unregisterResource(uri: string): void
registerPrompt(definition: MCPPromptDefinition): void
unregisterPrompt(name: string): void
invokeTool(name: string, args: unknown, signal?: AbortSignal): Promise<MCPToolResult>
invokeToolStream(name: string, args: unknown, signal?: AbortSignal): AsyncIterable<MCPContentItem>
readResource(uri: string, signal?: AbortSignal): Promise<MCPResourceContent>
getPromptMessages(name: string, args?: Record<string, string>, signal?: AbortSignal): Promise<MCPPromptMessage[]>
getInfo(): MCPServerInfo
start(transport: Transport): Promise<void>
stop(): Promise<void>
setAuthorizer(fn: (request: MCPAuthRequest) => Promise<boolean>): void
}
// Client
class MCPClient {
constructor(config: MCPClientConfig)
connect(transport: Transport): Promise<void>
disconnect(): Promise<void>
refreshTools(): Promise<void>
refreshResources(): Promise<void>
refreshPrompts(): Promise<void>
callTool(name: string, args: unknown, signal?: AbortSignal): Promise<MCPToolResult>
callToolStream(name: string, args: unknown, signal?: AbortSignal): AsyncIterable<MCPContentItem>
readResource(uri: string, signal?: AbortSignal): Promise<MCPResourceContent>
getPrompt(name: string, args?: Record<string, string>, signal?: AbortSignal): Promise<MCPPromptMessage[]>
tools(): RemoteTool[]
resources(): RemoteResource[]
prompts(): RemotePrompt[]
}
// Transport interface
interface Transport {
send(message: MCPMessage): Promise<void>
onMessage(handler: (message: MCPMessage) => void): () => void
close(): Promise<void>
}
// Concrete transports (sub-path exports)
function createStdioTransport(): Transport // Node process.stdin/stdout
function createSseTransport(url: string): Transport
function createWebSocketTransport(url: string): Transport
// Zod schemas (all wire types)
const MCPServerConfigSchema: z.ZodSchema<MCPServerConfig>
const MCPClientConfigSchema: z.ZodSchema<MCPClientConfig>
const MCPToolDefinitionSchema: z.ZodSchema<MCPToolDefinition>
const MCPResourceDefinitionSchema: z.ZodSchema<MCPResourceDefinition>
const MCPPromptDefinitionSchema: z.ZodSchema<MCPPromptDefinition>
const MCPToolResultSchema: z.ZodSchema<MCPToolResult>
const MCPContentItemSchema: z.ZodSchema<MCPContentItem>
const MCPResourceContentSchema: z.ZodSchema<MCPResourceContent>
const MCPPromptMessageSchema: z.ZodSchema<MCPPromptMessage>
// Errors
class MCPError extends SuaveplanError { }
class MCPToolError extends MCPError { }
class MCPTransportError extends MCPError { }
function registerMCPErrorCodes(): void
```
---
## File layout
```text
packages/devtools/tooling/mcp/
package.json
tsconfig.json
vitest.config.ts
src/
index.ts
error-codes.ts
types.ts # Zod schemas + z.infer types for all wire types
constants/index.ts
server/
mcp-server/
mcp-server.ts / .md / .types.ts / .test.ts / index.ts
client/
mcp-client/
mcp-client.ts / .md / .types.ts / .test.ts / index.ts
transport/
transport/
transport.ts / transport.md / transport.types.ts / index.ts
stdio/
stdio.ts / stdio.md / stdio.test.ts / index.ts
sse/
sse.ts / sse.md / sse.test.ts / index.ts
websocket/
websocket.ts / websocket.md / websocket.test.ts / index.ts
tests/
e2e/
stdio-handshake.test.ts
```
---
## Impact
Downstream: AI agent integration, tool registration for LLM workflows. The package is the MCP foundation for the entire platform.
**Prerequisites on Verdaccio**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events`, `@suaveplan/logger`, `@suaveplan/opentelemetry`.
### Affected dependents
None. `@suaveplan/mcp` is currently unreferenced by every other genesis package (per `MANIFEST.md`).
### Deviations from upstream
1. Upstream is a scaffold with no JSON-RPC framing, no real transport, no abort, no progress, no authorize, no observability. Genesis ships a full implementation of every MCP 2024-11-05 host-side method.
2. Upstream's wire DTOs are hand-rolled interfaces; only the two config objects are Zod. Genesis derives every wire type from a Zod schema (`MCPServerConfigSchema`, `JsonRpcRequestSchema`, `MCPToolCallParamsSchema`, etc.) and `z.infer`s the TypeScript type.
3. Upstream declared a `stdio | sse | websocket` transport union but shipped no transport. Genesis ships a `Transport` interface plus three concrete adapters: `InMemoryTransport.pair()` (universal, used by tests and in-process bridges), `createStdioTransport({ readable, writable })` (universal — accepts injectable byte streams so the same code runs on Node, Bun, Deno, Edge, browser), `createHttpSseTransport({ endpoint, fetchImpl, eventSourceImpl })` (universal — accepts injectable `fetch` + `EventSource` constructor).
4. WebSocket transport is intentionally deferred to a sibling change once `@suaveplan/network-websockets` lands. The `Transport` interface is shaped so a WebSocket adapter is drop-in.
5. The package gains a stable `MCP_CODES` registry built via `defineCodes("MCP", […])` and a `MCPError` / `MCPProtocolError` / `MCPTransportError` hierarchy.
6. Every public method on `MCPServer` / `MCPClient` accepts an `AbortSignal`; cancellation surfaces over the wire as `notifications/cancelled`.
7. Progress notifications: `MCPServer.invokeTool` invokes the handler with a `progress(token, value, total?)` callback that forwards `notifications/progress` to the client.
8. Authorize hook: `MCPServer.setAuthorizer(fn)` gates every incoming method; rejection yields a JSON-RPC `-32001` error mapped to `MCP.UNAUTHORIZED`.
9. Resource subscriptions: server pushes `notifications/resources/updated` to every subscribed client when a resource is marked dirty.
### Out of scope (explicitly deferred to follow-up changes, not silently dropped)
- **WebSocket transport** — requires `@suaveplan/network-websockets`, not yet migrated.
- **React renderers for `MCPContentItem`** — belong in a separate `react-mcp` package under `packages/ui/react/`.
- **`suaveplan-mcp` CLI binary** — belongs in a `tooling/mcp-cli` sibling.
Each deferral is justified above; this is not silent omission per AGENTS §2b.

View File

@@ -0,0 +1,326 @@
<!-- markdownlint-disable MD024 -->
# Capability Spec Delta: mcp (mcp)
**Package**: `@suaveplan/mcp`
**Status**: PROPOSED
> Every requirement below carries a `**Verified by:**` line at authoring time.
## ADDED Requirements
### Requirement: MCP_CODES registry via defineCodes
The package SHALL publish a stable `MCP_CODES` registry built with `defineCodes("MCP", […])` containing the leaves `PROTOCOL_INVALID`, `METHOD_NOT_FOUND`, `INVALID_PARAMS`, `INTERNAL_ERROR`, `UNAUTHORIZED`, `REQUEST_CANCELLED`, `TIMEOUT`, `TRANSPORT_CLOSED`, `TRANSPORT_ERROR`, `NOT_INITIALIZED`, `ALREADY_CONNECTED`, `DUPLICATE_REGISTRATION`, `NOT_FOUND`, `PROTOCOL_VERSION_MISMATCH`, and `HANDLER_FAILED`. The package SHALL export `MCPError`, `MCPTransportError`, and `MCPProtocolError` classes extending `SuaveplanError<MCPCode>` with public constructors that delegate to `super`. Every registered code MUST be reachable from a real call site exercised by a test.
#### Scenario: codes use the MCP namespace
- **WHEN** code consumers inspect `MCP_CODES.UNAUTHORIZED`
- **THEN** the value is the string `"MCP.UNAUTHORIZED"` (per `defineCodes` output convention)
#### Scenario: every code is thrown by at least one call site
- **WHEN** the test suite runs to completion
- **THEN** the per-code reachability harness records at least one throw / report carrying each registered code
**Verified by:** `src/error-codes/error-codes.test.ts::"namespaces every code under MCP."` AND `src/error-codes/error-codes.test.ts::"exposes the expected leaf names"` AND `src/error-codes/error-codes.test.ts::"carries the supplied code, message, cause, and context"` AND `src/error-codes/error-codes.test.ts::"every registered code is reachable from a real call site"`.
---
### Requirement: Zod schemas are the single source of truth for every wire type
The package SHALL define a Zod schema for every type that crosses the JSON-RPC wire: `JsonRpcRequestSchema`, `JsonRpcResponseSchema`, `JsonRpcNotificationSchema`, `JsonRpcMessageSchema` (discriminated union), `InitializeParamsSchema`, `InitializeResultSchema`, `ServerCapabilitiesSchema`, `ClientCapabilitiesSchema`, `ImplementationSchema`, `ToolSchema`, `ToolCallParamsSchema`, `ToolCallResultSchema`, `ContentItemSchema` (text/image/resource union), `ResourceSchema`, `ResourceTemplateSchema`, `ResourceContentSchema`, `ResourceReadParamsSchema`, `ResourceSubscribeParamsSchema`, `PromptSchema`, `PromptMessageSchema`, `PromptGetParamsSchema`, `PromptGetResultSchema`, `CompletionRequestSchema`, `CompletionResultSchema`, `SamplingMessageSchema`, `CreateMessageParamsSchema`, `CreateMessageResultSchema`, `RootSchema`, `LoggingLevelSchema`, `LoggingMessageParamsSchema`, `ProgressNotificationParamsSchema`, `CancelledNotificationParamsSchema`. Every public TypeScript type for a wire shape MUST be derived via `z.infer<>` from one of these schemas. No hand-rolled interface MAY substitute for a wire-type schema.
#### Scenario: every wire schema parses canonical sample payloads
- **WHEN** the test calls `.parse(sample)` on each exported schema with a canonical example
- **THEN** parsing succeeds and the inferred type matches expectations
#### Scenario: invalid payloads are rejected
- **WHEN** the test calls `.safeParse(brokenSample)` on a schema
- **THEN** `success === false` and the error path identifies the offending field
**Verified by:** `src/schemas/schemas.test.ts::"every exported schema parses its canonical example"` AND `src/schemas/schemas.test.ts::"rejects malformed JSON-RPC envelopes"` AND `src/schemas/schemas.test.ts::"discriminates request vs response vs notification"` AND `src/schemas/schemas.test.ts::"derives TypeScript types via z.infer"`.
---
### Requirement: Transport interface with three reference implementations
The package SHALL export a `Transport` interface with methods `send(message: JsonRpcMessage): Promise<void>`, `onMessage(handler: (message: JsonRpcMessage) => void): () => void`, and `close(): Promise<void>`, plus a `closed: boolean` getter. The package SHALL ship three reference implementations: `InMemoryTransport.pair()` returning two paired transports for in-process bridges; `createStdioTransport({ readable, writable })` framing newline-delimited JSON over injectable `AsyncIterable<Uint8Array>` and a writable handle; `createHttpSseTransport({ endpoint, fetchImpl, eventSourceImpl })` posting requests to `endpoint` via injected `fetch` and consuming server-pushed messages via the injected `EventSource` constructor. Every transport MUST raise `MCPTransportError(TRANSPORT_CLOSED)` if `send` is called after `close`.
#### Scenario: in-memory pair round-trips a message
- **WHEN** transport A's `send(message)` is called
- **THEN** transport B's registered `onMessage` handler observes the same message
#### Scenario: stdio transport frames newline-delimited JSON
- **WHEN** writable receives a frame
- **THEN** the frame is `JSON.stringify(message) + "\n"` and the reading side parses each line as a separate JSON-RPC message
#### Scenario: http+sse transport posts via fetchImpl and receives via eventSourceImpl
- **WHEN** the client `send`s a request
- **THEN** the injected `fetchImpl` is invoked with the endpoint URL and the JSON-stringified body; messages arriving on the injected `EventSource` are dispatched to `onMessage`
#### Scenario: every transport refuses send after close
- **WHEN** `close()` resolves and `send(message)` is invoked
- **THEN** the call rejects with `MCPTransportError(TRANSPORT_CLOSED)`
**Verified by:** `src/transport/in-memory/in-memory.test.ts::"round-trips a message between paired transports"` AND `src/transport/in-memory/in-memory.test.ts::"raises TRANSPORT_CLOSED after close"` AND `src/transport/stdio/stdio.test.ts::"frames newline-delimited JSON on send"` AND `src/transport/stdio/stdio.test.ts::"parses newline-delimited JSON from readable"` AND `src/transport/stdio/stdio.test.ts::"raises TRANSPORT_CLOSED after close"` AND `src/transport/http-sse/http-sse.test.ts::"posts requests via fetchImpl"` AND `src/transport/http-sse/http-sse.test.ts::"dispatches messages from eventSourceImpl"` AND `src/transport/http-sse/http-sse.test.ts::"raises TRANSPORT_CLOSED after close"`.
---
### Requirement: JSON-RPC dispatcher correlates requests, notifications, and cancellations
The package SHALL ship `JsonRpcDispatcher` providing: id allocation via injectable `IdGenerator`; pending-request map keyed by `JsonRpcId`; `request(method, params, options?)` returning `Promise<unknown>` that resolves on matching response and rejects on `error` envelope, `AbortSignal` abort (mapped to `MCPError(REQUEST_CANCELLED)`), or `timeoutMs` elapse (mapped to `MCPError(TIMEOUT)`); `notify(method, params)` that sends a notification (no id); `registerHandler(method, fn)` and `registerNotificationListener(method, fn)`; cancellation that on incoming `notifications/cancelled` aborts the matching in-flight handler invocation. The dispatcher MUST reject pending requests with `MCPTransportError(TRANSPORT_CLOSED)` when the underlying transport closes. It MUST reject unknown methods with JSON-RPC code `-32601` mapped to `MCPError(METHOD_NOT_FOUND)`. It MUST reject invalid params with `-32602` mapped to `MCPError(INVALID_PARAMS)`.
#### Scenario: a request is correlated with its response
- **WHEN** `request("ping")` is issued
- **THEN** the dispatcher writes a JSON-RPC envelope with a fresh id and resolves the returned promise when a response with the matching id arrives
#### Scenario: abort sends notifications/cancelled and rejects the pending request
- **WHEN** the caller aborts the supplied `AbortSignal` while a request is in flight
- **THEN** the dispatcher sends `notifications/cancelled` with the request id AND rejects the pending promise with `MCPError(REQUEST_CANCELLED)`
#### Scenario: incoming cancellation aborts the handler
- **WHEN** the dispatcher receives `notifications/cancelled` for an in-flight handler invocation
- **THEN** the handler's `AbortSignal` is aborted
#### Scenario: timeout rejects with TIMEOUT
- **WHEN** `request(method, params, { timeoutMs: 1 })` is issued and no response arrives within 1 ms (per injected clock+timers)
- **THEN** the promise rejects with `MCPError(TIMEOUT)`
#### Scenario: transport closure rejects every pending request
- **WHEN** the underlying transport closes with one or more pending requests
- **THEN** each pending promise rejects with `MCPTransportError(TRANSPORT_CLOSED)`
#### Scenario: unknown method yields -32601
- **WHEN** the dispatcher receives a request whose method has no registered handler
- **THEN** the dispatcher responds with JSON-RPC error code `-32601` and an `MCPError(METHOD_NOT_FOUND)`-shaped payload
#### Scenario: invalid params yield -32602
- **WHEN** the registered Zod schema for a method rejects the params
- **THEN** the dispatcher responds with JSON-RPC error code `-32602` and an `MCPError(INVALID_PARAMS)`-shaped payload carrying the Zod issue path
**Verified by:** `src/jsonrpc/dispatcher/dispatcher.test.ts::"correlates a request with its response"` AND `src/jsonrpc/dispatcher/dispatcher.test.ts::"aborts in-flight request and emits notifications/cancelled"` AND `src/jsonrpc/dispatcher/dispatcher.test.ts::"aborts handler invocation on incoming notifications/cancelled"` AND `src/jsonrpc/dispatcher/dispatcher.test.ts::"rejects with TIMEOUT after timeoutMs"` AND `src/jsonrpc/dispatcher/dispatcher.test.ts::"rejects pending requests on transport close"` AND `src/jsonrpc/dispatcher/dispatcher.test.ts::"responds with -32601 for unknown method"` AND `src/jsonrpc/dispatcher/dispatcher.test.ts::"responds with -32602 for Zod-rejected params"`.
---
### Requirement: MCPServer registers and exposes tools, resources, and prompts
The package SHALL ship `MCPServer` with `registerTool(definition)`, `unregisterTool(name)`, `registerResource(definition)`, `unregisterResource(uri)`, `registerPrompt(definition)`, `unregisterPrompt(name)`, `registerRoot(root)`, `unregisterRoot(uri)`, `listTools()`, `listResources()`, `listResourceTemplates()`, `listPrompts()`, `listRoots()`, `invokeTool(name, params, options?)`, `readResource(uri, options?)`, `getPromptMessages(name, args?, options?)`, `setAuthorizer(fn)`, `setSamplingProvider(fn)`, `setCompletionProvider(fn)`, `setLogLevel(level)`, `markResourceUpdated(uri)`, `start(transport)`, `stop()`, and a `getInfo()` summary. Duplicate registration MUST throw `MCPError(DUPLICATE_REGISTRATION)`. Lookup miss MUST throw `MCPError(NOT_FOUND)`. `invokeTool` MUST validate `params` against the tool's declared `inputSchema` (when present as a Zod schema) and reject with `MCPError(INVALID_PARAMS)` on failure. Handler throws MUST be wrapped into `MCPToolResult.isError === true` content carrying the failure message. `start` MUST throw `MCPError(ALREADY_CONNECTED)` if called twice without an intervening `stop`.
#### Scenario: register and list a tool
- **WHEN** `registerTool({ name: "echo", description, inputSchema, handler })` is called and then `listTools()` is queried
- **THEN** the returned list contains the tool
#### Scenario: duplicate registration is rejected
- **WHEN** `registerTool` is called twice with the same name
- **THEN** the second call throws `MCPError(DUPLICATE_REGISTRATION)`
#### Scenario: invoke validates params against Zod inputSchema
- **WHEN** `invokeTool("echo", invalidParams)` is called and `inputSchema` rejects `invalidParams`
- **THEN** the call rejects with `MCPError(INVALID_PARAMS)` and `notifications/cancelled` is NOT emitted
#### Scenario: handler throws produce an error tool result
- **WHEN** the tool handler throws
- **THEN** `invokeTool` resolves with `{ content: [{ type: "text", text: "Error: …" }], isError: true }`
#### Scenario: start twice raises ALREADY_CONNECTED
- **WHEN** `start(transport)` is called a second time without `stop()`
- **THEN** the second call rejects with `MCPError(ALREADY_CONNECTED)`
#### Scenario: authorize hook gates the call
- **WHEN** `setAuthorizer(req => req.method !== "tools/call")` is set and a `tools/call` arrives
- **THEN** the dispatcher responds with `-32001` mapped to `MCPError(UNAUTHORIZED)`
#### Scenario: AbortSignal propagates to the handler
- **WHEN** `invokeTool("slow", params, { signal })` is called and the signal is aborted
- **THEN** the handler's `AbortSignal` fires and the call rejects with `MCPError(REQUEST_CANCELLED)`
#### Scenario: progress callback emits notifications/progress
- **WHEN** the handler invokes `progress(token, 50, 100)`
- **THEN** the dispatcher sends `notifications/progress` with `{ progressToken: token, progress: 50, total: 100 }`
#### Scenario: markResourceUpdated fans out notifications/resources/updated
- **WHEN** a client has subscribed to URI `mem://x` and the server calls `markResourceUpdated("mem://x")`
- **THEN** the dispatcher sends `notifications/resources/updated` with `{ uri: "mem://x" }`
#### Scenario: getInfo summarises registrations
- **WHEN** `getInfo()` is queried after registering 2 tools, 1 resource, and 1 prompt
- **THEN** the result reports `tools: 2`, `resources: 1`, `prompts: 1`, `running` reflecting the lifecycle
**Verified by:** `src/server/mcp-server/mcp-server.test.ts::"registers and lists a tool"` AND `src/server/mcp-server/mcp-server.test.ts::"rejects duplicate tool registration"` AND `src/server/mcp-server/mcp-server.test.ts::"validates params against Zod inputSchema"` AND `src/server/mcp-server/mcp-server.test.ts::"wraps handler errors into isError result"` AND `src/server/mcp-server/mcp-server.test.ts::"raises ALREADY_CONNECTED on second start"` AND `src/server/mcp-server/mcp-server.test.ts::"authorize hook rejects calls"` AND `src/server/mcp-server/mcp-server.test.ts::"AbortSignal aborts handler and rejects with REQUEST_CANCELLED"` AND `src/server/mcp-server/mcp-server.test.ts::"progress callback emits notifications/progress"` AND `src/server/mcp-server/mcp-server.test.ts::"markResourceUpdated fans out notifications/resources/updated"` AND `src/server/mcp-server/mcp-server.test.ts::"getInfo summarises registrations"` AND `src/server/mcp-server/mcp-server.test.ts::"resource and prompt registration mirrors tool behaviour"`.
---
### Requirement: MCPClient performs the MCP initialize handshake and exposes the full method surface
The package SHALL ship `MCPClient` with `connect(transport)`, `disconnect()`, `initialized` boolean getter, `refreshTools()`, `refreshResources()`, `refreshResourceTemplates()`, `refreshPrompts()`, `refreshRoots()`, `callTool(name, params, options?)`, `readResource(uri, options?)`, `subscribeResource(uri)`, `unsubscribeResource(uri)`, `getPrompt(name, args?, options?)`, `complete(request, options?)`, `setLogLevel(level)`, `setSamplingProvider(fn)`, `setRootsProvider(fn)`, and accessors `tools()`, `resources()`, `resourceTemplates()`, `prompts()`, `roots()`. `connect(transport)` MUST: open the transport, send `initialize` with declared `ClientCapabilities`, await the `InitializeResult`, emit `notifications/initialized`, populate the initial tool/resource/prompt/root caches via `tools/list` / `resources/list` / `prompts/list` / `roots/list`. Calling any other method before `connect` MUST throw `MCPError(NOT_INITIALIZED)`. `connect` called twice without `disconnect` MUST throw `MCPError(ALREADY_CONNECTED)`. Protocol version mismatch on the `initialize` result MUST throw `MCPError(PROTOCOL_VERSION_MISMATCH)`.
#### Scenario: connect performs the MCP initialize handshake
- **WHEN** `connect(transport)` is called against a paired server
- **THEN** the server observes `initialize` then `notifications/initialized` and `client.initialized === true` resolves
#### Scenario: methods before connect raise NOT_INITIALIZED
- **WHEN** `callTool` is invoked before `connect`
- **THEN** the call rejects with `MCPError(NOT_INITIALIZED)`
#### Scenario: connect twice raises ALREADY_CONNECTED
- **WHEN** `connect(transport)` is called a second time without `disconnect`
- **THEN** the second call rejects with `MCPError(ALREADY_CONNECTED)`
#### Scenario: protocol version mismatch raises PROTOCOL_VERSION_MISMATCH
- **WHEN** the server's `InitializeResult.protocolVersion` is not in the client's accepted set
- **THEN** `connect` rejects with `MCPError(PROTOCOL_VERSION_MISMATCH)` and the transport is closed
#### Scenario: callTool round-trips with the server
- **WHEN** the client calls `callTool("echo", { msg: "hi" })`
- **THEN** the server's registered handler executes and the client receives the tool's `MCPToolResult`
#### Scenario: progress notifications are forwarded to onProgress
- **WHEN** `callTool` is invoked with `{ onProgress }` and the server emits `notifications/progress`
- **THEN** `onProgress` is invoked with `{ progress, total }`
#### Scenario: AbortSignal cancels via notifications/cancelled
- **WHEN** the caller aborts the supplied signal mid-call
- **THEN** the server observes `notifications/cancelled` and the client promise rejects with `MCPError(REQUEST_CANCELLED)`
#### Scenario: subscribeResource updates the cache on notifications/resources/updated
- **WHEN** the client has subscribed to a URI and the server emits `notifications/resources/updated`
- **THEN** the client refreshes the resource and emits the `resource:updated` event
#### Scenario: refreshes update the cached lists
- **WHEN** the client calls `refreshTools()`
- **THEN** `tools()` returns the freshly listed remote tools
#### Scenario: sampling provider is reachable via createMessage
- **WHEN** the server invokes `sampling/createMessage` and the client has a registered sampling provider
- **THEN** the provider is invoked with the request params and its return value is sent back as the response
#### Scenario: roots provider is reachable via roots/list
- **WHEN** the server invokes `roots/list` and the client has a registered roots provider
- **THEN** the provider is invoked and its return value is sent back as the response
**Verified by:** `src/client/mcp-client/mcp-client.test.ts::"connect performs the MCP initialize handshake"` AND `src/client/mcp-client/mcp-client.test.ts::"methods before connect raise NOT_INITIALIZED"` AND `src/client/mcp-client/mcp-client.test.ts::"connect twice raises ALREADY_CONNECTED"` AND `src/client/mcp-client/mcp-client.test.ts::"protocol version mismatch raises PROTOCOL_VERSION_MISMATCH"` AND `src/client/mcp-client/mcp-client.test.ts::"callTool round-trips with the server"` AND `src/client/mcp-client/mcp-client.test.ts::"forwards progress notifications to onProgress"` AND `src/client/mcp-client/mcp-client.test.ts::"AbortSignal cancels via notifications/cancelled"` AND `src/client/mcp-client/mcp-client.test.ts::"subscribeResource updates cache on notifications/resources/updated"` AND `src/client/mcp-client/mcp-client.test.ts::"refreshes update the cached lists"` AND `src/client/mcp-client/mcp-client.test.ts::"sampling provider responds to createMessage"` AND `src/client/mcp-client/mcp-client.test.ts::"roots provider responds to roots/list"` AND `src/client/mcp-client/mcp-client.test.ts::"reads resources and gets prompts"` AND `src/client/mcp-client/mcp-client.test.ts::"completes via completion/complete"` AND `src/client/mcp-client/mcp-client.test.ts::"setLogLevel emits logging/setLevel"`.
---
### Requirement: Telemetry meter records every request and notification
The package SHALL register an OTel meter at name `@suaveplan/mcp` exposing `mcp_request_total` counter (tagged `direction`, `method`, `status`), `mcp_notification_total` counter (tagged `direction`, `method`), and `mcp_request_duration_ms` histogram (tagged `direction`, `method`). Every dispatcher request MUST increment the counter and record the histogram. Every notification (sent or received) MUST increment the notification counter.
#### Scenario: request counter increments per request
- **WHEN** a request is issued and completes successfully
- **THEN** `mcp_request_total{direction="client",method="…",status="success"}` increases by 1
#### Scenario: error and cancelled outcomes are tagged
- **WHEN** a request fails or is cancelled
- **THEN** the counter increment carries `status="error"` or `status="cancelled"` accordingly
#### Scenario: notification counter increments per notification
- **WHEN** a notification is sent
- **THEN** `mcp_notification_total{direction="…",method="…"}` increases by 1
**Verified by:** `src/telemetry/telemetry.test.ts::"records mcp_request_total per request"` AND `src/telemetry/telemetry.test.ts::"tags error and cancelled outcomes"` AND `src/telemetry/telemetry.test.ts::"records mcp_notification_total per notification"` AND `src/telemetry/telemetry.test.ts::"records mcp_request_duration_ms histogram"`.
---
### Requirement: Lifecycle events are emitted via EventBus
The package SHALL expose `events: EventBus<MCPEventMap>` from both `MCPServer` and `MCPClient`, emitting `connection:opened` (payload `{ side, dispatcherId }`), `connection:closed` (payload `{ side, dispatcherId, reason? }`), `request:received` (payload `{ side, method, id }`), `request:completed` (payload `{ side, method, id, durationMs, outcome }`), and `notification:emitted` (payload `{ side, method, params }`).
#### Scenario: connect/disconnect emits open/close events
- **WHEN** the client connects then disconnects
- **THEN** subscribers observe `connection:opened` followed by `connection:closed`
#### Scenario: a request emits received and completed
- **WHEN** the server handles a request to completion
- **THEN** subscribers observe `request:received` followed by `request:completed` carrying the correct `durationMs` and `outcome`
**Verified by:** `src/events/events.test.ts::"emits connection:opened and connection:closed"` AND `src/events/events.test.ts::"emits request:received and request:completed"` AND `src/events/events.test.ts::"emits notification:emitted"`.
---
### Requirement: Full end-to-end handshake walks the entire MCP method surface
The package SHALL include an end-to-end test that pairs a real `MCPServer` and `MCPClient` over `InMemoryTransport.pair()` and exercises in order: `initialize`, `notifications/initialized`, `tools/list`, `tools/call` (with progress + abort variants), `resources/list`, `resources/templates/list`, `resources/read`, `resources/subscribe`, server-side `markResourceUpdated`, `resources/unsubscribe`, `prompts/list`, `prompts/get`, `completion/complete`, `sampling/createMessage`, `roots/list`, `logging/setLevel`, `ping`, and `disconnect`. Every method MUST round-trip successfully.
#### Scenario: every MCP method completes the round-trip
- **WHEN** the e2e test runs to completion
- **THEN** every assertion passes and no JSON-RPC error envelope appears
**Verified by:** `src/e2e/handshake.test.ts::"walks the full MCP method surface end-to-end"` AND `src/e2e/handshake.test.ts::"progress and cancellation propagate over the wire"` AND `src/e2e/handshake.test.ts::"resource subscriptions deliver updates"`.
---
### Requirement: Universal-tier purity
The package SHALL be universal-tier: zero browser-only DOM imports, zero Node-only built-in imports (no `node:fs`, `node:net`, `node:process`, `node:stream`, etc.), zero React. All runtime dependencies that touch I/O (streams, fetch, EventSource) MUST be passed in via the public API. The `package.json` MUST declare `"sideEffects": false` and MUST NOT declare an `engines.node` floor that contradicts universal targets.
#### Scenario: source tree contains no Node built-in imports
- **WHEN** the test harness greps the source tree
- **THEN** zero matches for `from "node:` or `require("node:`
#### Scenario: source tree contains no DOM globals
- **WHEN** the test harness greps the source tree
- **THEN** zero direct references to `document`, `window`, or `navigator`
**Verified by:** `src/e2e/handshake.test.ts::"source tree contains no Node built-in imports"` AND `src/e2e/handshake.test.ts::"source tree contains no DOM globals"`.
---
### Requirement: Edge-case branches reach 100% coverage at the package gate
The package SHALL hold 100% statement / branch / function / line coverage at the publish gate (`bunx turbo test:coverage --filter=@suaveplan/mcp`). Every defensive branch, error path, default-parameter, optional-field, and provider-fallback in `mcp-client.ts`, `mcp-server.ts`, `dispatcher.ts`, `transport/{stdio,in-memory,http-sse}.ts` MUST be reachable from a real call site exercised by the test suite — no `/* istanbul ignore */` or `// biome-ignore` suppressions, no dead defensive code left behind. Dead branches that no caller can reach MUST be deleted, not silenced.
#### Scenario: defensive transport-error and abort-race paths are reachable
- **WHEN** the dispatcher's send-side rejection, handler-throw-race-with-abort, malformed-cancellation/progress params, double-close, and non-Error handler throws are exercised
- **THEN** every corresponding branch is hit and the coverage gate stays at 100%
#### Scenario: client and server provider fall-backs and optional-field branches are reachable
- **WHEN** `MCPClient` / `MCPServer` are exercised with missing providers, omitted optional descriptions / mimeTypes / arguments, undefined `_meta`, root-level Zod errors, and no-arg call-tool / get-prompt invocations
- **THEN** every corresponding branch is hit and the coverage gate stays at 100%
**Verified by:** `src/e2e/dispatcher-edge-cases.test.ts::"transport send throwing maps to MCPTransportError"` AND `src/e2e/dispatcher-edge-cases.test.ts::"unregisterHandler removes a registered method"` AND `src/e2e/dispatcher-edge-cases.test.ts::"onNotification reuses an existing listener set"` AND `src/e2e/dispatcher-edge-cases.test.ts::"request send rejection cleans up pending entry"` AND `src/e2e/dispatcher-edge-cases.test.ts::"request send non-Error rejection still cleans up"` AND `src/e2e/dispatcher-edge-cases.test.ts::"handler abort race rejects without responding"` AND `src/e2e/dispatcher-edge-cases.test.ts::"error response codes round-trip through mapMcpCodeToJsonRpc"` AND `src/e2e/dispatcher-edge-cases.test.ts::"raw JSON-RPC wire codes map through mapJsonRpcCode"` AND `src/e2e/dispatcher-internals-edge-cases.test.ts::"dispatcher stringifies numeric ids distinctly from strings"` AND `src/e2e/dispatcher-internals-edge-cases.test.ts::"dispatcher close is idempotent over double-close"` AND `src/e2e/dispatcher-internals-edge-cases.test.ts::"dispatcher schema-validated request without params uses default {}"` AND `src/e2e/dispatcher-internals-edge-cases.test.ts::"dispatcher sendProgress omits total when undefined"` AND `src/e2e/dispatcher-internals-edge-cases.test.ts::"dispatcher silently drops unparseable wire messages"` AND `src/e2e/dispatcher-internals-edge-cases.test.ts::"CancelledNotificationParamsSchema parse failure is skipped silently"` AND `src/e2e/dispatcher-internals-edge-cases.test.ts::"setProgressToken with already-tagged pending entry skips it"` AND `src/e2e/dispatcher-internals-edge-cases.test.ts::"setProgressToken is no-op when method has no matching pending entry"` AND `src/e2e/dispatcher-internals-edge-cases.test.ts::"handler throwing a non-Error stringifies via String(err)"` AND `src/e2e/server-edge-cases.test.ts::"server without sampling/completion provider falls back"` AND `src/e2e/server-edge-cases.test.ts::"tool with paramsSchema validates over the dispatcher"` AND `src/e2e/server-edge-cases.test.ts::"server invokeTool with paramsSchema replaces params with parsed data"` AND `src/e2e/server-edge-cases.test.ts::"tool handler returning a ContentItem array is normalised"` AND `src/e2e/server-edge-cases.test.ts::"list helpers surface optional description/mimeType/arguments"` AND `src/e2e/server-edge-cases.test.ts::"markResourceUpdated before start is a silent no-op"` AND `src/e2e/server-edge-cases.test.ts::"server invokeTool wraps non-Error handler throws into isError"` AND `src/e2e/server-edge-cases.test.ts::"server start wires injected clock and idGenerator"` AND `src/e2e/server-wire-edge-cases.test.ts::"tools/call without arguments uses empty-object schema input"` AND `src/e2e/server-wire-edge-cases.test.ts::"tools/call without _meta has undefined progressToken"` AND `src/e2e/server-wire-edge-cases.test.ts::"createSamplingMessage and listClientRoots forward signal"` AND `src/e2e/server-wire-edge-cases.test.ts::"emitLog without logger argument omits the logger field"` AND `src/e2e/server-wire-edge-cases.test.ts::"authorize hook accepting the call passes the throw branch"` AND `src/e2e/server-wire-edge-cases.test.ts::"invokeTool handler progress and abort-listener fire"` AND `src/e2e/server-wire-edge-cases.test.ts::"Zod root-level rejection formats as \`(root)\` in error message"` AND `src/e2e/client-edge-cases.test.ts::"client malformed initialize result raises PROTOCOL_INVALID"` AND `src/e2e/client-edge-cases.test.ts::"client completion provider serves wire-side completion/complete"` AND `src/e2e/client-edge-cases.test.ts::"client completion fallback returns empty values without provider"` AND `src/e2e/client-edge-cases.test.ts::"client constructor wires injected clock and idGenerator"` AND `src/e2e/client-edge-cases.test.ts::"client disconnect with reason emits closed-with-reason event"` AND `src/e2e/client-edge-cases.test.ts::"client getPrompt without args uses default empty object"` AND `src/e2e/client-edge-cases.test.ts::"client request honours an explicit zero timeoutMs option"` AND `src/e2e/client-edge-cases.test.ts::"client list-changed catch handlers swallow refresh failures"` AND `src/e2e/client-edge-cases.test.ts::"client ping handler responds when peer requests it"` AND `src/e2e/client-edge-cases.test.ts::"callTool with no args uses the default empty object"`.

View File

@@ -0,0 +1,98 @@
# Tasks: enterprise-mcp
**Change ID**: enterprise-mcp
**Package**: `@suaveplan/mcp`
**Target path**: `packages/devtools/tooling/mcp`
**Prerequisites on Verdaccio**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events`, `@suaveplan/logger`, `@suaveplan/opentelemetry`.
## 0. Pre-flight (non-negotiable)
- [x] 0.1 Confirm clean working tree (`git status`) and no in-flight worktrees with uncommitted work.
- [x] 0.2 Account for every entry in `git worktree list`.
- [x] 0.3 §0a Sync — fast-forward worktree to expected base SHA.
- [x] 0.4 Read `CLAUDE.md`, `AGENTS.md`, `openspec/AGENTS.md`, `openspec/project.md`, `/mnt/s/web/suaveplan/enterprise/mcp.md`, and the main-branch source.
## 1. OpenSpec authoring
- [x] 1.1 `proposal.md` with `## Impact` → `### Affected dependents` per §14a.
- [x] 1.2 `design.md` covering architecture, transports, file layout, telemetry, trade-offs.
- [x] 1.3 `specs/mcp/spec.md` — every requirement carries a `**Verified by:**` line at draft time.
## 2. Scaffold the package
- [ ] 2.1 Create `packages/devtools/tooling/mcp/` with `package.json` (`0.1.0`, `sideEffects: false`, exports map), `tsconfig.lib.json`, `vite.config.ts`, `vitest.config.ts`, `README.md`.
- [ ] 2.2 Add `mcp` to the `tooling` plugin in `scripts/folder-reorg/assignment.yml`.
## 3. Error codes
- [ ] 3.1 `src/error-codes/error-codes.ts` — `MCP_CODES = defineCodes("MCP", […])` + `MCPError` / `MCPTransportError` / `MCPProtocolError` extending `SuaveplanError<MCPCode>`.
- [ ] 3.2 `error-codes.test.ts` — namespacing, leaves, reachability harness.
- [ ] 3.3 `error-codes.md`.
## 4. Wire-type schemas
- [ ] 4.1 `src/schemas/schemas.ts` — Zod schemas for every wire type (JSON-RPC envelopes + every MCP method's params & result).
- [ ] 4.2 `schemas.test.ts`.
- [ ] 4.3 `schemas.md`.
## 5. Telemetry + events
- [ ] 5.1 `src/telemetry/telemetry.ts` — meter `@suaveplan/mcp`, three instruments, getter/reset helpers for tests.
- [ ] 5.2 `src/events/events.ts` — `MCPEventMap` declaration.
- [ ] 5.3 Tests + docs.
## 6. JSON-RPC dispatcher
- [ ] 6.1 `src/jsonrpc/dispatcher/dispatcher.ts` — id allocator, pending-request map, request/notify/handler registry, cancellation, timeout.
- [ ] 6.2 Tests covering correlation, abort, cancel-on-notify, timeout, transport-close rejection, method-not-found, invalid-params.
- [ ] 6.3 `dispatcher.md`.
## 7. Transports
- [ ] 7.1 `src/transport/transport/` — interface + base.
- [ ] 7.2 `src/transport/in-memory/in-memory.ts` — `InMemoryTransport.pair()`.
- [ ] 7.3 `src/transport/stdio/stdio.ts` — newline-delimited JSON over injectable `AsyncIterable<Uint8Array>` + writable handle.
- [ ] 7.4 `src/transport/http-sse/http-sse.ts` — injectable `fetchImpl` + `eventSourceImpl`.
- [ ] 7.5 Tests + docs for each.
## 8. MCPServer
- [ ] 8.1 `src/server/mcp-server/mcp-server.ts` — registration, lifecycle, authorize, sampling/completion providers, abort, progress, resource subscriptions, fan-out.
- [ ] 8.2 `mcp-server.test.ts` (≤500 lines) covering every scenario in the spec delta.
- [ ] 8.3 `mcp-server.md`.
## 9. MCPClient
- [ ] 9.1 `src/client/mcp-client/mcp-client.ts` — connect (initialize handshake), refreshes, callTool / readResource / getPrompt / complete / subscribeResource, sampling + roots providers, progress forwarding, abort.
- [ ] 9.2 `mcp-client.test.ts` (≤500 lines).
- [ ] 9.3 `mcp-client.md`.
## 10. End-to-end test
- [ ] 10.1 `src/e2e/handshake.test.ts` — full round-trip across every MCP method via `InMemoryTransport.pair()`.
## 11. Package barrel + docs
- [ ] 11.1 `src/index.ts` — explicit named re-exports only (§6a).
- [ ] 11.2 Root `README.md` covering quick-start, transport comparison, API table, error codes.
## 12. Verification
- [ ] 12.1 `bunx turbo lint --filter=@suaveplan/mcp` — green.
- [ ] 12.2 `bunx turbo typecheck --filter=@suaveplan/mcp` — green.
- [ ] 12.3 `bunx turbo test --filter=@suaveplan/mcp` — green.
- [ ] 12.4 `bunx turbo build --filter=@suaveplan/mcp` — green.
- [ ] 12.5 `bunx turbo test:coverage --filter=@suaveplan/mcp` — 100 % statements/branches/functions/lines.
- [ ] 12.6 `grep -rnE "from ['\"](vitest|bun:test)['\"]" packages/devtools/tooling/mcp/src/` — zero output.
- [ ] 12.7 `grep -rnE "from \"node:" packages/devtools/tooling/mcp/src/` — zero output.
- [ ] 12.8 `bun scripts/folder-reorg/generate-manifest.ts` — `MANIFEST.md` regenerated.
## 13. Archival
- [ ] 13.1 Flip `@suaveplan/mcp` row in `STATUS.md` from `–` to `✅` with the README one-liner.
- [ ] 13.2 Fold `openspec/changes/enterprise-mcp/specs/mcp/spec.md` into `openspec/specs/mcp/spec.md`.
- [ ] 13.3 Move `openspec/changes/enterprise-mcp/` to `openspec/archive/2026-05-25-enterprise-mcp/`.
## 14. Commit
- [ ] 14.1 Branch `feat/migrate-mcp` already created; commit per `feat(mcp): migrate …` + Claude co-author footer. Do NOT push.