|
| 1 | +# ext-apps SDK Divorce Plan |
| 2 | + |
| 3 | +This document outlines how `@modelcontextprotocol/ext-apps` can remove its dependency on |
| 4 | +`@modelcontextprotocol/sdk` (v1) without adopting the breaking V2 SDK, by vendoring only |
| 5 | +what it actually needs. |
| 6 | + |
| 7 | +## Current State |
| 8 | + |
| 9 | +ext-apps depends on `@modelcontextprotocol/sdk@^1.29.0` for: |
| 10 | + |
| 11 | +| Category | Count | Examples | |
| 12 | +|----------|-------|----------| |
| 13 | +| Pure TypeScript types | ~46 | `CallToolRequest`, `Tool`, `Implementation`, `JSONRPCMessage` | |
| 14 | +| Zod schemas (handler routing) | 11 | `CallToolRequestSchema` u2192 `setRequestHandler(schema, handler)` | |
| 15 | +| Zod schemas (result validation) | 7 | `CallToolResultSchema` u2192 `request(req, schema)` | |
| 16 | +| Zod schemas (composition) | 7 | `ToolSchema`, `ContentBlockSchema` u2192 `generated/schema.ts` | |
| 17 | +| Zod schema (wire validation) | 1 | `JSONRPCMessageSchema.safeParse()` in PostMessageTransport | |
| 18 | +| Runtime classes | 3 | `Protocol` (extended), `Client` (instantiated), `Transport` (interface) | |
| 19 | +| Server helpers (examples+docs only) | 5 | `McpServer`, `StdioServerTransport`, etc. | |
| 20 | + |
| 21 | +--- |
| 22 | + |
| 23 | +## Sources of Truth |
| 24 | + |
| 25 | +### For types: MCP Specification repo |
| 26 | + |
| 27 | +The `modelcontextprotocol/modelcontextprotocol` repo has versioned schemas at |
| 28 | +`schema/{version}/schema.ts` u2014 pure TypeScript, zero imports, zero Zod. |
| 29 | + |
| 30 | +Available versions: |
| 31 | +- `2024-11-05` u2014 baseline (31 KB) |
| 32 | +- `2025-03-26` u2014 (34 KB) |
| 33 | +- `2025-06-18` u2014 (42 KB) |
| 34 | +- `2025-11-25` u2014 (67 KB) u2014 **recommended target** (stable release, has all types we need) |
| 35 | +- `draft` u2014 (93 KB, `DRAFT-2026-v1`) u2014 unstable, has tasks/elicitation |
| 36 | + |
| 37 | +This repo is `private: true` / not on npm. We'd copy a specific version's `schema.ts` |
| 38 | +into our tree (it's self-contained). |
| 39 | + |
| 40 | +**Every pure type ext-apps imports is defined here**: `CallToolRequest`, `CallToolResult`, |
| 41 | +`Tool`, `Implementation`, `ContentBlock`, `EmbeddedResource`, `ResourceLink`, |
| 42 | +`JSONRPCMessage`, `PingRequest`, `EmptyResult`, etc. |
| 43 | + |
| 44 | +**Not defined here**: `Transport` interface, `ProtocolOptions`, `RequestOptions`, |
| 45 | +`RequestHandlerExtra` u2014 these are SDK concepts, not protocol concepts. |
| 46 | + |
| 47 | +### For Protocol: Minimal shim (~300 lines) |
| 48 | + |
| 49 | +The full V2 Protocol is 1,081 lines with ~10,700 lines of transitive deps. ext-apps |
| 50 | +uses a narrow surface: |
| 51 | + |
| 52 | +``` |
| 53 | +connect(transport) u2014 wire up transport callbacks |
| 54 | +close() u2014 tear down |
| 55 | +setRequestHandler(schema, fn) u2014 register handler keyed by schema.shape.method.value |
| 56 | +setNotificationHandler(schema, fn) u2014 same pattern |
| 57 | +request(req, resultSchema, options) u2014 send request, correlate response, validate result |
| 58 | +notification(notif, options) u2014 send one-way message |
| 59 | +onclose? / onerror? u2014 callbacks |
| 60 | +fallbackNotificationHandler? u2014 catch-all |
| 61 | +``` |
| 62 | + |
| 63 | +A minimal shim reproducing this surface is ~300 lines: |
| 64 | +- JSON-RPC message routing (request/response correlation by ID) |
| 65 | +- Handler map keyed by method string (extracted from schema at registration time) |
| 66 | +- Timeout + AbortSignal support |
| 67 | +- Zod schema validation on incoming requests and outgoing results |
| 68 | +- No task management, no capability negotiation, no auth |
| 69 | + |
| 70 | +### For Zod schemas: Vendor from SDK or regenerate |
| 71 | + |
| 72 | +**Handler routing schemas** (11): These are only needed to extract the method string |
| 73 | +and validate incoming params. With the vendored Protocol shim, we keep the V1 API |
| 74 | +(`setRequestHandler(schema, handler)`) so these still work. We can define minimal |
| 75 | +Zod schemas ourselves: |
| 76 | + |
| 77 | +```typescript |
| 78 | +import { z } from 'zod/v4'; |
| 79 | +export const CallToolRequestSchema = z.object({ |
| 80 | + method: z.literal('tools/call'), |
| 81 | + params: z.object({ name: z.string(), arguments: z.record(z.unknown()).optional() }).passthrough() |
| 82 | +}).passthrough(); |
| 83 | +``` |
| 84 | + |
| 85 | +**Result validation schemas** (7): Same approach u2014 define minimal Zod schemas matching |
| 86 | +the spec types. Or skip validation entirely (the SDK's V2 approach). |
| 87 | + |
| 88 | +**Composition schemas** (7 in `generated/schema.ts`): These are the trickiest. |
| 89 | +`ContentBlockSchema`, `ToolSchema`, etc. are used to compose ext-apps' own schemas. |
| 90 | +Options: |
| 91 | +1. Vendor the specific Zod schemas from SDK 1.29.0 source |
| 92 | +2. Regenerate from the spec schema.ts using `ts-to-zod` (ext-apps already uses this) |
| 93 | +3. Hand-write minimal versions |
| 94 | + |
| 95 | +**Wire validation** (1): `JSONRPCMessageSchema` u2014 vendor or redefine. It's a simple |
| 96 | +discriminated union. |
| 97 | + |
| 98 | +--- |
| 99 | + |
| 100 | +## Architecture After Divorce |
| 101 | + |
| 102 | +``` |
| 103 | +ext-apps/ |
| 104 | +u251cu2500u2500 src/ |
| 105 | +u2502 u251cu2500u2500 vendor/ |
| 106 | +u2502 u2502 u251cu2500u2500 protocol.ts # Minimal Protocol shim (~300 lines) |
| 107 | +u2502 u2502 u251cu2500u2500 transport.ts # Transport interface (~30 lines) |
| 108 | +u2502 u2502 u251cu2500u2500 jsonrpc.ts # JSONRPCMessage types + validation schema |
| 109 | +u2502 u2502 u2514u2500u2500 mcp-types.ts # Copy of spec schema.ts (chosen version) |
| 110 | +u2502 u251cu2500u2500 generated/ |
| 111 | +u2502 u2502 u2514u2500u2500 schema.ts # Updated: import from vendor/ instead of SDK |
| 112 | +u2502 u251cu2500u2500 mcp-schemas.ts # Minimal Zod schemas for MCP request/result types |
| 113 | +u2502 u251cu2500u2500 events.ts # ProtocolWithEvents u2014 import from vendor/protocol.ts |
| 114 | +u2502 u251cu2500u2500 app.ts # Import types from vendor/mcp-types.ts |
| 115 | +u2502 u251cu2500u2500 app-bridge.ts # Import types from vendor/mcp-types.ts |
| 116 | +u2502 u2514u2500u2500 ... |
| 117 | +u251cu2500u2500 examples/ # Still depend on SDK for McpServer, transports |
| 118 | +u2514u2500u2500 docs/ # Same |
| 119 | +``` |
| 120 | + |
| 121 | +### What stays as SDK dependency |
| 122 | + |
| 123 | +- **`examples/`** and **`docs/`**: These use `McpServer`, `StdioServerTransport`, |
| 124 | + `StreamableHTTPServerTransport`, `createMcpExpressApp`, `ResourceTemplate` u2014 all |
| 125 | + server-side SDK classes. They stay as SDK dependencies (moved to devDependencies). |
| 126 | + |
| 127 | +- **`src/server/index.ts`**: Uses `McpServer` types (`RegisteredTool`, `ToolCallback`, |
| 128 | + etc.) as `import type`. Can be made into an optional peer dependency, or the types |
| 129 | + can be copied. |
| 130 | + |
| 131 | +- **`Client` class** (`src/app-bridge.ts`, `src/react/useApp.tsx`): Used as a runtime |
| 132 | + dependency for the host-side `AppBridge`. Options: |
| 133 | + 1. Keep `@modelcontextprotocol/client` (V2) as a peer dependency for hosts |
| 134 | + 2. Vendor the Client class too (much larger surface u2014 not recommended) |
| 135 | + 3. Accept `Client` via dependency injection (pass a client-like interface) |
| 136 | + |
| 137 | +### Recommended approach for Client |
| 138 | + |
| 139 | +Define a minimal `McpClient` interface that `AppBridge` needs: |
| 140 | + |
| 141 | +```typescript |
| 142 | +export interface McpClient { |
| 143 | + connect(transport: Transport): Promise<void>; |
| 144 | + close(): Promise<void>; |
| 145 | + getServerCapabilities(): ServerCapabilities | undefined; |
| 146 | + request<T>(request: { method: string; params?: Record<string, unknown> }, schema?: unknown): Promise<T>; |
| 147 | + setNotificationHandler(schema: unknown, handler: (notification: unknown) => void): void; |
| 148 | +} |
| 149 | +``` |
| 150 | + |
| 151 | +AppBridge's constructor takes `McpClient` instead of `Client`. Hosts using the MCP SDK |
| 152 | +pass their `Client` instance (it satisfies this interface). ext-apps doesn't import Client. |
| 153 | + |
| 154 | +--- |
| 155 | + |
| 156 | +## Spec Version Selection |
| 157 | + |
| 158 | +| Option | Pros | Cons | |
| 159 | +|--------|------|------| |
| 160 | +| `2025-11-25` (latest stable) | Stable, has `ContentBlock`, `ResourceLink`, `ToolAnnotations` | Missing tasks/elicitation (ext-apps doesn't use them) | |
| 161 | +| `draft` (DRAFT-2026-v1) | Most complete | Unstable, 93 KB, includes unused types | |
| 162 | +| Cherry-pick from `2025-11-25` | Only the types ext-apps uses | Manual maintenance | |
| 163 | + |
| 164 | +**Recommendation**: Use `2025-11-25` as the base. It has everything ext-apps needs. |
| 165 | +If we later need draft-only types, cherry-pick them. |
| 166 | + |
| 167 | +--- |
| 168 | + |
| 169 | +## Migration Steps |
| 170 | + |
| 171 | +### Phase 1: Vendor Protocol + Transport (no SDK changes yet) |
| 172 | + |
| 173 | +1. Create `src/vendor/protocol.ts` u2014 minimal Protocol shim with V1 generic signature |
| 174 | +2. Create `src/vendor/transport.ts` u2014 Transport interface |
| 175 | +3. Update `src/events.ts` to import from `./vendor/protocol.ts` |
| 176 | +4. Verify all tests pass with vendored Protocol |
| 177 | + |
| 178 | +### Phase 2: Vendor MCP Types |
| 179 | + |
| 180 | +1. Copy `schema/2025-11-25/schema.ts` into `src/vendor/mcp-types.ts` |
| 181 | +2. Update all `import type { ... } from '@modelcontextprotocol/sdk/types.js'` to |
| 182 | + `import type { ... } from './vendor/mcp-types.js'` |
| 183 | +3. Create `src/mcp-schemas.ts` with minimal Zod schemas for the MCP request/result |
| 184 | + types that ext-apps uses at runtime |
| 185 | +4. Update `src/generated/schema.ts` to import Zod schemas from `src/mcp-schemas.ts` |
| 186 | + instead of from the SDK |
| 187 | + |
| 188 | +### Phase 3: Abstract Client dependency |
| 189 | + |
| 190 | +1. Define `McpClient` interface in `src/vendor/client-interface.ts` |
| 191 | +2. Update `AppBridge` to accept `McpClient` instead of `Client` |
| 192 | +3. Move `@modelcontextprotocol/sdk` to `peerDependencies` (optional) for hosts |
| 193 | + that want to pass an SDK `Client` |
| 194 | + |
| 195 | +### Phase 4: Clean up |
| 196 | + |
| 197 | +1. Remove `@modelcontextprotocol/sdk` from `dependencies` |
| 198 | +2. Keep in `devDependencies` for examples/ only |
| 199 | +3. Update `src/server/index.ts` to use `import type` from peer dep or vendored types |
| 200 | +4. Update all examples/ to import from V2 SDK packages |
| 201 | + |
| 202 | +--- |
| 203 | + |
| 204 | +## Estimated Effort |
| 205 | + |
| 206 | +| Phase | Files changed | New code | Risk | |
| 207 | +|-------|--------------|----------|------| |
| 208 | +| 1. Vendor Protocol | 3-4 | ~350 lines | Medium u2014 must match V1 behavior exactly | |
| 209 | +| 2. Vendor MCP Types | 8-10 | ~100 lines (schemas) + copy spec | Low u2014 type-only changes except schemas | |
| 210 | +| 3. Abstract Client | 3-4 | ~30 lines (interface) | Low u2014 structural subtyping | |
| 211 | +| 4. Clean up | 5-10 | 0 | Low u2014 import path changes | |
| 212 | + |
| 213 | +Total new code: ~480 lines (Protocol shim + schemas + interface). |
| 214 | +Spec types file: ~67 KB copied from spec repo (2025-11-25 version). |
0 commit comments