diff --git a/README.md b/README.md
index acfc6617b..ab4004d66 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
Why
·
- Quickstart
+ Quickstart
·
API Docs
·
@@ -151,7 +151,7 @@ in a 1.x host and a 2.x host renders 1.x Views. See the
when upgrading from 1.x.
**New here?** Start with the
-[Quickstart Guide](https://apps.extensions.modelcontextprotocol.io/api/documents/Quickstart.html)
+[Quickstart Guide](https://apps.extensions.modelcontextprotocol.io/api/documents/quickstart.html)
to build your first MCP App.
## Using the SDK
@@ -272,7 +272,7 @@ point your client at a local build:
## Resources
-- [Quickstart Guide](https://apps.extensions.modelcontextprotocol.io/api/documents/Quickstart.html)
+- [Quickstart Guide](https://apps.extensions.modelcontextprotocol.io/api/documents/quickstart.html)
- [API Documentation](https://apps.extensions.modelcontextprotocol.io/api/)
- [Specification (2026-01-26)](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx)
([Draft](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx))
diff --git a/docs/migrate-to-2.md b/docs/migrate-to-2.md
index 515c36384..f9341d069 100644
--- a/docs/migrate-to-2.md
+++ b/docs/migrate-to-2.md
@@ -17,14 +17,22 @@ The `ui/*` messages exchanged over the iframe channel are byte-identical to
1.x. A 2.x View runs in a 1.x host and a 2.x host renders 1.x Views. The only
host-side deltas are in error responses (see below).
+## What stays the same
+
+`App` / `AppBridge` constructors, the `on*` setters, `addEventListener`,
+`callServerTool`, `readServerResource`, `registerAppResource`, the React hooks
+and the `_meta.ui.resourceUri` handling are unchanged. Published `.d.ts` files
+now resolve under `moduleResolution: NodeNext` / `Node16` without workarounds
+(#705).
+
## Peer dependencies by role
-| Role | Install |
-| ------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
-| View author | `@modelcontextprotocol/ext-apps`, `@modelcontextprotocol/client@^2.0.0`, `zod@^4.2.0` (+ `react`/`react-dom` for `./react`) |
-| Host author | same as View author |
-| MCP server author | View author packages + `@modelcontextprotocol/server@^2.0.0`; `@modelcontextprotocol/node` and `express` for HTTP transports |
-| CDN / `*-with-deps` | nothing extra: `./app-with-deps` and `./react-with-deps` bundle client, core and zod (about 25% larger than the 1.x bundles) |
+| Role | Install |
+| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| View author | `@modelcontextprotocol/ext-apps`, `@modelcontextprotocol/client@^2.0.0`, `zod@^4.2.0` (+ `react`/`react-dom` for `./react`) |
+| Host author | same as View author |
+| MCP server author | View author packages + `@modelcontextprotocol/server@^2.0.0`; for HTTP transports also `@modelcontextprotocol/node@^2.0.0` (`NodeStreamableHTTPServerTransport`) and `@modelcontextprotocol/express@^2.0.0` (`createMcpExpressApp`, which peers on `express`) |
+| CDN / `*-with-deps` | nothing extra: `./app-with-deps` and `./react-with-deps` bundle client, core and zod (about 25% larger than the 1.x bundles) |
`@modelcontextprotocol/client` is a required peer (`App` and `AppBridge` extend
its `Protocol` class); `@modelcontextprotocol/core` is a required peer that `client` already depends on, so npm installs it without you listing it;
@@ -35,18 +43,23 @@ its `Protocol` class); `@modelcontextprotocol/core` is a required peer that `cli
- **Peer packages.** `@modelcontextprotocol/sdk@^1` is replaced by the split
2.x packages above, all at `^2.0.0`. Remove the 1.x package from your
- project; the two SDKs do not interoperate.
+ project: the two SDKs share no classes or types (a 1.x `McpError` or
+ `Client` cannot be passed to 2.x code), even though they speak the same
+ wire protocol.
- **zod 3 dropped.** The peer range is `zod@^4.2.0`. Tool schemas must
- implement Standard JSON Schema (`~standard.jsonSchema`): zod v4, ArkType,
- Valibot. Raw zod shapes (`{ q: z.string() }`) still work with
+ implement Standard JSON Schema (`~standard.jsonSchema`): zod 4.2+, ArkType,
+ Valibot. zod 4.0 and 4.1 do not expose `~standard.jsonSchema` and are
+ rejected too. Raw zod shapes (`{ q: z.string() }`) still work with
`registerAppTool` but are deprecated; wrap them with `z.object({...})`.
- **`App` / `AppBridge` extend `Protocol` from `@modelcontextprotocol/client`.**
`ProtocolWithEvents` is gone; use the SDK's `Protocol`. The `AppRequest`,
`AppNotification` and `AppResult` unions are kept as deprecated type
aliases (nothing in 2.x consumes them) and will be removed in 3.0.
-- **Handler context.** Custom handlers receive the SDK 2.x context:
- `extra.signal` → `extra.mcpReq.signal`, `extra.requestId` →
- `extra.mcpReq.id`.
+- **Handler context.** Custom request handlers, `registerAppTool` callbacks
+ (server) and `app.registerTool` callbacks (View) receive the SDK 2.x
+ context: `extra.signal` → `extra.mcpReq.signal`, `extra.requestId` →
+ `extra.mcpReq.id`; `extra.sessionId` is unchanged and `extra.authInfo`
+ moved to `extra.http?.authInfo`.
- **`setRequestHandler` / `setNotificationHandler` take method names.**
`app.setRequestHandler(SomeRequestSchema, handler)` becomes
`app.setRequestHandler("some/method", { params: SomeParamsSchema }, (params, ctx) => …)`
@@ -55,14 +68,23 @@ its `Protocol` class); `@modelcontextprotocol/core` is a required peer that `cli
method names. The 1.x `(Schema, handler)` form still works on `App` and
`AppBridge` as a deprecated overload: it logs a one-time warning, hands the
handler the whole `{ method, params }` message as before, and gives request
- handlers a 1.x-shaped `extra` (`signal`, `requestId`, `sessionId`, `_meta`).
- It will be removed in 3.0.
+ handlers a 1.x-shaped `extra` (`signal`, `requestId`, `sessionId`, `_meta`,
+ `sendRequest`, `sendNotification`, `authInfo`; exported as
+ `LegacyRequestHandlerExtra`). It will be removed in 3.0.
+- **Double-registration guard covers the SDK's own handlers.** Registering a
+ handler for `notifications/progress` or `notifications/cancelled` (which
+ the base `Protocol` already owns) now throws `already registered`, as
+ `ping` and the `on*`-owned methods did in 1.x. Use `addEventListener` or
+ the SDK's progress callbacks instead of replacing these.
- **Errors.** Remote JSON-RPC errors are `ProtocolError` (numeric `code`);
local conditions are `SdkError` with a string `code`: request timeout →
`"REQUEST_TIMEOUT"`, connection closed → `"CONNECTION_CLOSED"`. Cancelling a
request with an `AbortSignal` also rejects with `"REQUEST_TIMEOUT"` (the
message is the abort reason). Messages no longer carry the `MCP error N:`
prefix.
+ A resource-not-found reply that carries `data.uri` arrives as
+ `ResourceNotFoundError` (a `ProtocolError` subclass, `code` `-32602`, with a
+ `.uri` getter).
## Host-side wire deltas
@@ -98,8 +120,9 @@ The published `./schema.json` export is regenerated from the 2.x core schemas:
2. Replace `sdk/...` imports with the split packages (`sdk/server/mcp.js` →
`@modelcontextprotocol/server`, `sdk/server/streamableHttp.js` →
`NodeStreamableHTTPServerTransport` from `@modelcontextprotocol/node`,
- `sdk/server/stdio.js` → `@modelcontextprotocol/server/stdio`, `sdk/types.js`
- → `@modelcontextprotocol/client` or `server` for the types,
+ `sdk/server/stdio.js` → `@modelcontextprotocol/server/stdio`, hand-written
+ Express routing → `createMcpExpressApp` from `@modelcontextprotocol/express`,
+ `sdk/types.js` → `@modelcontextprotocol/client` or `server` for the types,
`@modelcontextprotocol/core` for the zod schemas).
3. Wrap raw zod shapes with `z.object({...})`.
4. Update custom handlers to the `extra.mcpReq.*` context and method-keyed
diff --git a/src/app-bridge.ts b/src/app-bridge.ts
index c61dfa4df..1ac4db485 100644
--- a/src/app-bridge.ts
+++ b/src/app-bridge.ts
@@ -37,9 +37,8 @@ import {
import { EventDispatcher, MethodRegistry } from "./events.js";
import {
toModernArgs,
- type LegacyMethodSchema,
- type LegacyNotificationHandler,
- type LegacyRequestHandler,
+ type LegacyNotificationHandlerSetter,
+ type LegacyRequestHandlerSetter,
} from "./legacy-handlers.js";
import type { ZodLiteral, ZodObject, ZodType } from "zod/v4";
@@ -330,12 +329,9 @@ export class AppBridge extends Protocol {
*
* @throws {Error} if a handler for this method is already registered.
*/
- override setRequestHandler: Protocol["setRequestHandler"] &
- ((
- /** @deprecated Pass the method name and `{ params }` instead. */
- schema: S,
- handler: LegacyRequestHandler,
- ) => void) = (...args: unknown[]) => {
+ override setRequestHandler: LegacyRequestHandlerSetter<
+ Protocol["setRequestHandler"]
+ > = (...args: unknown[]) => {
const [method, ...rest] = toModernArgs("request", args) ?? args;
this._methods.claim(method as string, "setRequestHandler");
(super.setRequestHandler as unknown as UntypedHandlerSetter).call(
@@ -352,12 +348,9 @@ export class AppBridge extends Protocol {
*
* @throws {Error} if a handler for this method is already registered.
*/
- override setNotificationHandler: Protocol["setNotificationHandler"] &
- ((
- /** @deprecated Pass the method name and `{ params }` instead. */
- schema: S,
- handler: LegacyNotificationHandler,
- ) => void) = (...args: unknown[]) => {
+ override setNotificationHandler: LegacyNotificationHandlerSetter<
+ Protocol["setNotificationHandler"]
+ > = (...args: unknown[]) => {
const [method, ...rest] = toModernArgs("notification", args) ?? args;
this._methods.claim(method as string, "setNotificationHandler");
(super.setNotificationHandler as unknown as UntypedHandlerSetter).call(
diff --git a/src/app.ts b/src/app.ts
index 0ab51accd..89c6c35e7 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -26,15 +26,16 @@ export { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "./constants.js";
import { EventDispatcher, MethodRegistry } from "./events.js";
import {
toModernArgs,
- type LegacyMethodSchema,
- type LegacyNotificationHandler,
- type LegacyRequestHandler,
+ type LegacyNotificationHandlerSetter,
+ type LegacyRequestHandlerSetter,
} from "./legacy-handlers.js";
export type {
LegacyMethodSchema,
LegacyNotificationHandler,
+ LegacyNotificationHandlerSetter,
LegacyRequestHandler,
LegacyRequestHandlerExtra,
+ LegacyRequestHandlerSetter,
} from "./legacy-handlers.js";
export { EventDispatcher } from "./events.js";
@@ -328,12 +329,9 @@ export class App extends Protocol {
*
* @throws {Error} if a handler for this method is already registered.
*/
- override setRequestHandler: Protocol["setRequestHandler"] &
- ((
- /** @deprecated Pass the method name and `{ params }` instead. */
- schema: S,
- handler: LegacyRequestHandler,
- ) => void) = (...args: unknown[]) => {
+ override setRequestHandler: LegacyRequestHandlerSetter<
+ Protocol["setRequestHandler"]
+ > = (...args: unknown[]) => {
const [method, ...rest] = toModernArgs("request", args) ?? args;
this._methods.claim(method as string, "setRequestHandler");
(super.setRequestHandler as unknown as UntypedHandlerSetter).call(
@@ -350,12 +348,9 @@ export class App extends Protocol {
*
* @throws {Error} if a handler for this method is already registered.
*/
- override setNotificationHandler: Protocol["setNotificationHandler"] &
- ((
- /** @deprecated Pass the method name and `{ params }` instead. */
- schema: S,
- handler: LegacyNotificationHandler,
- ) => void) = (...args: unknown[]) => {
+ override setNotificationHandler: LegacyNotificationHandlerSetter<
+ Protocol["setNotificationHandler"]
+ > = (...args: unknown[]) => {
const [method, ...rest] = toModernArgs("notification", args) ?? args;
this._methods.claim(method as string, "setNotificationHandler");
(super.setNotificationHandler as unknown as UntypedHandlerSetter).call(
diff --git a/src/legacy-handlers.ts b/src/legacy-handlers.ts
index b4d115754..237b8eba9 100644
--- a/src/legacy-handlers.ts
+++ b/src/legacy-handlers.ts
@@ -36,6 +36,33 @@ export type LegacyNotificationHandler = (
notification: S["_output"],
) => void | Promise;
+/**
+ * `setRequestHandler` with the 1.x `(Schema, handler)` form kept as a
+ * deprecated overload next to the 2.x forms in `Modern`.
+ */
+export type LegacyRequestHandlerSetter = Modern & {
+ /**
+ * @deprecated 1.x form. Pass the method name and `{ params }` instead;
+ * removed in 3.0.
+ */
+ (
+ schema: S,
+ handler: LegacyRequestHandler,
+ ): void;
+};
+
+/** `setNotificationHandler` counterpart of {@link LegacyRequestHandlerSetter}. */
+export type LegacyNotificationHandlerSetter = Modern & {
+ /**
+ * @deprecated 1.x form. Pass the method name and `{ params }` instead;
+ * removed in 3.0.
+ */
+ (
+ schema: S,
+ handler: LegacyNotificationHandler,
+ ): void;
+};
+
/**
* Arguments for the 2.x three-argument `setRequestHandler` /
* `setNotificationHandler` form.