Files
suaveplan/openspec/archive/2026-05-25-enterprise-mcp/proposal.md
2026-05-27 23:29:53 +01:00

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

  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.infers 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.