Skip to content

Commit 80d2f44

Browse files
committed
docs: add type safety regression analysis and package dependency model
Document how v1's protocol-agnostic schema-based handler registration provided equal type safety for both MCP and custom methods, while v2 splits into typed (spec methods) and weaker (custom methods) paths. Also document that core types are not duplicated across client/server packages - both re-export from a shared core dependency.
1 parent 5acf49c commit 80d2f44

1 file changed

Lines changed: 75 additions & 1 deletion

File tree

docs/v2-sdk-impact-analysis.md

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,81 @@ type AppSpec = {
420420
421421
---
422422
423-
## 7. Discussion Points for SDK Team
423+
## 7. Type Safety Regression for Custom Method Handlers
424+
425+
In v1, `setRequestHandler(schema, handler)` was **protocol-agnostic** — the Zod schema
426+
carried both the method discriminator and the full request type. Any Zod schema with
427+
`{ method: z.literal('...') }` worked identically, whether it was `CallToolRequestSchema`
428+
(MCP spec) or `McpUiInitializeRequestSchema` (ext-apps custom). The handler received
429+
`z.infer<typeof schema>` regardless of provenance.
430+
431+
V2 **splits this into two paths**:
432+
433+
1. **Spec methods** (`M extends RequestMethod`): fully typed via `RequestTypeMap[M]`.
434+
Handler receives the full request object. Type-safe.
435+
2. **Custom methods** (string fallback): 3-arg form `(method, paramsSchema, handler)`.
436+
Handler receives only validated `params` (not the full request envelope), and the
437+
return type is `Result` (untyped) unless a `ProtocolSpec` generic is supplied.
438+
439+
This means ext-apps' current pattern:
440+
```typescript
441+
// v1: one generic, works for any schema, fully typed
442+
this.replaceRequestHandler(McpUiOpenLinkRequestSchema, (request, extra) => {
443+
// request is McpUiOpenLinkRequest (full envelope)
444+
return this._onopenlink(request.params, extra);
445+
});
446+
```
447+
448+
Becomes either:
449+
```typescript
450+
// v2 untyped fallback: params is Record<string, unknown>
451+
this.setRequestHandler('ui/open-link', McpUiOpenLinkParamsSchema, (params, ctx) => {
452+
return this._onopenlink(params, ctx); // params typed from schema only
453+
});
454+
```
455+
456+
Or (with `ProtocolSpec`):
457+
```typescript
458+
// v2 + ProtocolSpec: fully typed from the spec
459+
this.setRequestHandler('ui/open-link', McpUiOpenLinkParamsSchema, (params, ctx) => {
460+
return this._onopenlink(params, ctx); // params typed from AppSpec
461+
});
462+
```
463+
464+
The **handler shape also changes**: v1 handlers receive the full JSON-RPC request object
465+
(`{ method, params }`), v2 custom handlers receive only the validated `params` (with `_meta`
466+
stripped). This affects ext-apps' `ProtocolWithEvents._assertMethodNotRegistered()` which
467+
currently accesses `schema.shape.method.value` to extract the method name — that Zod
468+
introspection no longer works when the first arg is a string.
469+
470+
**Recommendation**: The `ProtocolSpec` path restores full type safety but requires ext-apps
471+
to declare its method vocabulary up front. The v1 approach of "pass any schema, get types
472+
for free" was more ergonomic for extension protocols. Consider whether the SDK should
473+
preserve a schema-based overload alongside the string-based one.
474+
475+
---
476+
477+
## 8. Package Dependency Model
478+
479+
Both `@modelcontextprotocol/client` and `@modelcontextprotocol/server` depend on
480+
`@modelcontextprotocol/core` (`"workspace:^"`) and re-export its public types via
481+
`export * from '@modelcontextprotocol/core/public'`. The types are **not duplicated** u2014
482+
`core` is a single copy at install time. `CallToolRequest` imported from either `client`
483+
or `server` has the same type identity.
484+
485+
ext-apps already uses both `Client` (from `client`) and `McpServer` (from `server`),
486+
so it would depend on two packages instead of one u2014 but this is a cosmetic change, not
487+
a real cost. The types come along for free from either package.
488+
489+
`@modelcontextprotocol/core` is `private: true` and must not be depended on directly
490+
by consumers. However, ext-apps' `generated/schema.ts` composes SDK Zod schemas
491+
(`CallToolResultSchema`, `ToolSchema`, etc.) which are only in `core`'s internal barrel.
492+
This is the one case where ext-apps may need a blessed escape hatch or must vendor
493+
those schemas.
494+
495+
---
496+
497+
## 9. Discussion Points for SDK Team
424498
425499
1. **Should `Protocol` be part of the public API?** The protocol-concrete branch exports it.
426500
ext-apps is the primary consumer outside the SDK itself. If Protocol stays internal,

0 commit comments

Comments
 (0)