8.2 KiB
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)
// 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
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
- 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.
- 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.) andz.infers the TypeScript type. - Upstream declared a
stdio | sse | websockettransport union but shipped no transport. Genesis ships aTransportinterface 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 injectablefetch+EventSourceconstructor). - WebSocket transport is intentionally deferred to a sibling change once
@suaveplan/network-websocketslands. TheTransportinterface is shaped so a WebSocket adapter is drop-in. - The package gains a stable
MCP_CODESregistry built viadefineCodes("MCP", […])and aMCPError/MCPProtocolError/MCPTransportErrorhierarchy. - Every public method on
MCPServer/MCPClientaccepts anAbortSignal; cancellation surfaces over the wire asnotifications/cancelled. - Progress notifications:
MCPServer.invokeToolinvokes the handler with aprogress(token, value, total?)callback that forwardsnotifications/progressto the client. - Authorize hook:
MCPServer.setAuthorizer(fn)gates every incoming method; rejection yields a JSON-RPC-32001error mapped toMCP.UNAUTHORIZED. - Resource subscriptions: server pushes
notifications/resources/updatedto 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 separatereact-mcppackage underpackages/ui/react/. suaveplan-mcpCLI binary — belongs in atooling/mcp-clisibling.
Each deferral is justified above; this is not silent omission per AGENTS §2b.