Initial Commit
This commit is contained in:
164
openspec/archive/2026-05-25-enterprise-mcp/design.md
Normal file
164
openspec/archive/2026-05-25-enterprise-mcp/design.md
Normal 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`.
|
||||
175
openspec/archive/2026-05-25-enterprise-mcp/proposal.md
Normal file
175
openspec/archive/2026-05-25-enterprise-mcp/proposal.md
Normal 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.
|
||||
326
openspec/archive/2026-05-25-enterprise-mcp/specs/mcp/spec.md
Normal file
326
openspec/archive/2026-05-25-enterprise-mcp/specs/mcp/spec.md
Normal 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"`.
|
||||
98
openspec/archive/2026-05-25-enterprise-mcp/tasks.md
Normal file
98
openspec/archive/2026-05-25-enterprise-mcp/tasks.md
Normal 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.
|
||||
Reference in New Issue
Block a user