@@ -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
4254991. **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