Skip to content

Commit 8a0444e

Browse files
committed
docs: add SDK divorce plan for removing @modelcontextprotocol/sdk dependency
Detailed plan to vendor the minimal Protocol shim (~300 lines), copy MCP types from the spec repo (2025-11-25 version), abstract the Client dependency via interface, and move SDK to devDependencies. Key findings: - MCP spec schema.ts is self-contained (zero imports, pure TS types) - ext-apps only uses a narrow Protocol surface (6 methods) - Full vendor would be ~10K lines; minimal shim is ~300 lines - Client can be abstracted via structural typing interface
1 parent 80d2f44 commit 8a0444e

1 file changed

Lines changed: 214 additions & 0 deletions

File tree

docs/sdk-divorce-plan.md

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
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

Comments
 (0)