Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ rm -fR package-lock.json node_modules && \

### Key Source Files

- `src/app.ts` - `App` class extends MCP Protocol, handles guest initialization, tool calls, messaging
- `src/app-bridge.ts` - `AppBridge` class for hosts, proxies MCP requests, sends tool input/results to guests
- `src/app.ts` - `App` subclasses the base MCP SDK `Protocol`, handles View initialization, tool calls, and messaging
- `src/app-bridge.ts` - `AppBridge` subclasses the base MCP SDK `Protocol` for the iframe channel and proxies through a separate outer `Client`
- `src/server/index.ts` - Helpers for MCP servers to register tools/resources with UI metadata
- `src/types.ts` - Protocol types re-exported from `spec.types.ts` and Zod schemas from `generated/schema.ts` (auto-generated during build)
- `src/message-transport.ts` - `PostMessageTransport` for iframe communication
Expand All @@ -70,12 +70,12 @@ rm -fR package-lock.json node_modules && \
### Protocol Flow

```
View (App) <--PostMessageTransport--> Host (AppBridge) <--MCP Client--> MCP Server
View (App/Protocol) <--PostMessageTransport--> Host (AppBridge/Protocol) <--separate MCP Client--> MCP Server
```

1. Host creates iframe with view HTML
2. View creates `App` instance and calls `connect()` with `PostMessageTransport`
3. View sends `ui/initialize` request, receives host capabilities and context
3. View sends `ui/initialize` (the only handshake on the iframe channel) and receives authoritative Apps capabilities and context
4. Host sends `sendToolInput()` with tool arguments after initialization
5. View can call server tools via `app.callServerTool()` or send messages via `app.sendMessage()`
6. Host sends `sendToolResult()` when tool execution completes
Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,17 @@ resources:
## Getting Started

```bash
npm install -S @modelcontextprotocol/ext-apps
npm install -S @modelcontextprotocol/ext-apps \
@modelcontextprotocol/client@2.0.0-beta.4 \
@modelcontextprotocol/server@2.0.0-beta.4 \
@modelcontextprotocol/core@2.0.0-beta.4 \
zod@^4.2.0
```

This release uses the split base MCP SDK v2 packages. Install all three at the
exact published beta.4 version so the Apps SDK and your MCP client/server share
one compatible protocol implementation.

**New here?** Start with the
[Quickstart Guide](https://apps.extensions.modelcontextprotocol.io/api/documents/Quickstart.html)
to build your first MCP App.
Expand Down
12 changes: 9 additions & 3 deletions build.bun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,15 @@ function buildJs(
});
}

// zod is a peerDependency — keep it external so consumers share a single
// zod instance (instanceof ZodError / schema.extend() break with duplicate copies).
const PEER_EXTERNALS = ["@modelcontextprotocol/sdk", "zod"];
// Peer dependencies stay external in the standard entry points so consumers
// share one base MCP SDK and Zod instance. The *-with-deps entry points keep
// bundling these dependencies for standalone browser use.
const PEER_EXTERNALS = [
"@modelcontextprotocol/client",
"@modelcontextprotocol/core",
"@modelcontextprotocol/server",
"zod",
];

await Promise.all([
buildJs("src/app.ts", {
Expand Down
2 changes: 1 addition & 1 deletion docs/authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ registerAppTool(
"get_account_balance",
{
description: "Get account balance",
inputSchema: { accountId: z.string() },
inputSchema: z.object({ accountId: z.string() }),
},
async ({ accountId }) => {
if (!authInfo) {
Expand Down
8 changes: 4 additions & 4 deletions docs/migrate_from_openai_apps.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ The server-side changes involve updating metadata structure and using helper fun
#### Before (OpenAI)

```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { McpServer } from "@modelcontextprotocol/server";
import { z } from "zod";

function createServer() {
Expand All @@ -78,7 +78,7 @@ function createServer() {
{
title: "Shopping Cart",
description: "Display the user's shopping cart",
inputSchema: { userId: z.string() },
inputSchema: z.object({ userId: z.string() }),
annotations: { readOnlyHint: true },
_meta: {
"openai/outputTemplate": "ui://view/cart.html",
Expand Down Expand Up @@ -126,7 +126,7 @@ function createServer() {
#### After (MCP Apps)

```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { McpServer } from "@modelcontextprotocol/server";
import {
registerAppTool,
registerAppResource,
Expand All @@ -144,7 +144,7 @@ function createServer() {
{
title: "Shopping Cart",
description: "Display the user's shopping cart",
inputSchema: { userId: z.string() },
inputSchema: z.object({ userId: z.string() }),
annotations: { readOnlyHint: true },
_meta: { ui: { resourceUri: "ui://view/cart.html" } },
},
Expand Down
8 changes: 6 additions & 2 deletions docs/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,11 @@ flowchart LR
- **Host** — The chat client (e.g., Claude Desktop) that connects to servers, embeds Views in iframes, and proxies communication between them.
- **View** — The UI running inside a sandboxed iframe. It receives tool data from the Host and can call server tools or send messages back to the chat.

The View acts as an MCP client, the Host acts as a proxy, and the Server is a standard MCP server.
The View's `App` subclasses the base MCP SDK `Client`. The Host's `AppBridge`
subclasses the base MCP SDK `Server` for the inner iframe channel, while a
separate outer `Client` connects the Host to the actual MCP Server. Keeping
those two connections separate prevents iframe negotiation and capabilities
from leaking into the server connection.

## Lifecycle

Expand Down Expand Up @@ -93,7 +97,7 @@ sequenceDiagram
```

1. **Discovery** — The Host learns about tools and their UI resources when connecting to the server.
2. **Initialization** — When a UI tool is called, the Host renders the iframe. The View sends `ui/initialize` and receives host context (theme, capabilities, container dimensions). This handshake ensures the View is ready before receiving data.
2. **Initialization** — When a UI tool is called, the Host renders the iframe. The View sends `ui/initialize` (the only handshake on the iframe channel) and receives host context (theme, capabilities, container dimensions). `ui/initialize` is authoritative for Apps capabilities and identity, and `ui/notifications/initialized` is the public View-ready signal.
3. **Data delivery** — The Host sends tool arguments and, once available, tool results to the View. Results include both `content` (text for the model's context) and optionally `structuredContent` (data optimized for UI rendering). This separation lets servers provide rich data to the UI without bloating the model's context.
4. **Interactive phase** — The user interacts with the View. The View can call tools, send messages, or update context.
5. **Teardown** — Before unmounting, the Host notifies the View so it can save state or release resources.
Expand Down
11 changes: 4 additions & 7 deletions docs/patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ registerAppTool(
"update-quantity",
{
description: "Update item quantity in cart",
inputSchema: { itemId: z.string(), quantity: z.number() },
inputSchema: z.object({ itemId: z.string(), quantity: z.number() }),
_meta: {
ui: {
resourceUri: "ui://shop/cart.html",
Expand Down Expand Up @@ -127,14 +127,14 @@ registerAppTool(
{
title: "Read Data Bytes",
description: "Load binary data in chunks",
inputSchema: {
inputSchema: z.object({
id: z.string().describe("Resource identifier"),
offset: z.number().min(0).default(0).describe("Byte offset"),
byteCount: z
.number()
.default(MAX_CHUNK_BYTES)
.describe("Bytes to read"),
},
}),
outputSchema: DataChunkSchema,
// Hidden from model - only callable by the App
_meta: { ui: { visibility: ["app"] } },
Expand Down Expand Up @@ -281,10 +281,7 @@ server.registerResource(

<!-- prettier-ignore -->
```ts source="./patterns.tsx#binaryBlobResourceClient"
const result = await app.request(
{ method: "resources/read", params: { uri: `video://${videoId}` } },
ReadResourceResultSchema,
);
const result = await app.readServerResource({ uri: `video://${videoId}` });

const content = result.contents[0];
if (!content || !("blob" in content)) {
Expand Down
17 changes: 5 additions & 12 deletions docs/patterns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,12 @@ import { randomUUID } from "node:crypto";
import type {
CallToolResult,
ReadResourceResult,
} from "@modelcontextprotocol/sdk/types.js";
import { ReadResourceResultSchema } from "@modelcontextprotocol/sdk/types.js";
} from "@modelcontextprotocol/server";
import type { McpUiHostContext } from "../src/types.js";
import { useEffect, useState } from "react";
import { useApp } from "../src/react/index.js";
import { registerAppTool } from "../src/server/index.js";
import {
McpServer,
ResourceTemplate,
} from "@modelcontextprotocol/sdk/server/mcp.js";
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/server";
import { z } from "zod";

/**
Expand Down Expand Up @@ -117,14 +113,14 @@ function chunkedDataServer(server: McpServer) {
{
title: "Read Data Bytes",
description: "Load binary data in chunks",
inputSchema: {
inputSchema: z.object({
id: z.string().describe("Resource identifier"),
offset: z.number().min(0).default(0).describe("Byte offset"),
byteCount: z
.number()
.default(MAX_CHUNK_BYTES)
.describe("Bytes to read"),
},
}),
outputSchema: DataChunkSchema,
// Hidden from model - only callable by the App
_meta: { ui: { visibility: ["app"] } },
Expand Down Expand Up @@ -252,10 +248,7 @@ function binaryBlobResourceServer(
*/
async function binaryBlobResourceClient(app: App, videoId: string) {
//#region binaryBlobResourceClient
const result = await app.request(
{ method: "resources/read", params: { uri: `video://${videoId}` } },
ReadResourceResultSchema,
);
const result = await app.readServerResource({ uri: `video://${videoId}` });

const content = result.contents[0];
if (!content || !("blob" in content)) {
Expand Down
19 changes: 10 additions & 9 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ This tutorial assumes you've built an MCP server before and are comfortable with

We'll use the [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) to build the server.

You'll also need Node.js 18+.
You'll also need Node.js 20+.

## 1. Set up the project

Expand All @@ -37,7 +37,7 @@ Install the dependencies you'll need:

```bash
npm init -y
npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk express cors
npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@2.0.0-beta.4 @modelcontextprotocol/core@2.0.0-beta.4 @modelcontextprotocol/server@2.0.0-beta.4 @modelcontextprotocol/node@2.0.0-beta.4 @modelcontextprotocol/express@2.0.0-beta.4 zod@^4.2.0 express cors
npm install -D typescript vite vite-plugin-singlefile @types/express @types/cors @types/node tsx concurrently cross-env
```

Expand Down Expand Up @@ -178,9 +178,10 @@ import {
registerAppTool,
RESOURCE_MIME_TYPE,
} from "@modelcontextprotocol/ext-apps/server";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { McpServer } from "@modelcontextprotocol/server";
import fs from "node:fs/promises";
import path from "node:path";
import { z } from "zod";

const DIST_DIR = path.join(import.meta.dirname, "dist");

Expand All @@ -205,7 +206,7 @@ export function createServer(): McpServer {
{
title: "Get Time",
description: "Returns the current server time.",
inputSchema: {},
inputSchema: z.object({}),
_meta: { ui: { resourceUri } }, // Links this tool to its UI resource
},
async () => {
Expand Down Expand Up @@ -240,10 +241,10 @@ export function createServer(): McpServer {

<!-- prettier-ignore -->
```ts source="../examples/quickstart/main.ts"
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { createMcpExpressApp } from "@modelcontextprotocol/express";
import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
import type { McpServer } from "@modelcontextprotocol/server";
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
Expand All @@ -263,7 +264,7 @@ export async function startStreamableHTTPServer(

app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
const transport = new StreamableHTTPServerTransport({
const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});

Expand Down
4 changes: 4 additions & 0 deletions docs/testing-mcp-apps.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ The [`basic-host`](https://github.com/modelcontextprotocol/ext-apps/tree/main/ex
cd examples/basic-host
```

The root install runs the package build, including type-checking the
documentation snippets against the exact base MCP SDK beta.4 packages. Do
not bypass a failed build before testing the host.

2. Start basic-host, pointing it to your MCP server:

```bash
Expand Down
2 changes: 1 addition & 1 deletion examples/basic-host/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
},
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.7.0",
"@modelcontextprotocol/sdk": "^1.29.0",
"@modelcontextprotocol/client": "2.0.0-beta.4",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"zod": "^4.1.13"
Expand Down
12 changes: 8 additions & 4 deletions examples/basic-host/src/implementation.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { RESOURCE_MIME_TYPE, getToolUiResourceUri, type McpUiSandboxProxyReadyNotification, AppBridge, PostMessageTransport, type McpUiResourceCsp, type McpUiResourcePermissions, buildAllowAttribute, type McpUiUpdateModelContextRequest, type McpUiMessageRequest } from "@modelcontextprotocol/ext-apps/app-bridge";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import type { CallToolResult, Resource, Tool } from "@modelcontextprotocol/sdk/types.js";
import {
Client,
SSEClientTransport,
StreamableHTTPClientTransport,
type CallToolResult,
type Resource,
type Tool,
} from "@modelcontextprotocol/client";
import { getTheme, onThemeChange } from "./theme";
import { HOST_STYLE_VARIABLES } from "./host-styles";

Expand Down
2 changes: 1 addition & 1 deletion examples/basic-host/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getToolUiResourceUri, McpUiToolMetaSchema } from "@modelcontextprotocol/ext-apps/app-bridge";
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
import type { Tool } from "@modelcontextprotocol/client";
import { Component, type ErrorInfo, type ReactNode, StrictMode, Suspense, use, useEffect, useMemo, useRef, useState } from "react";
import { createRoot } from "react-dom/client";
import { callTool, connectToServer, hasAppHtml, initializeApp, loadSandboxProxy, log, newAppBridge, type ServerInfo, type ToolCallInfo, type ModelContext, type AppMessage } from "./implementation";
Expand Down
10 changes: 5 additions & 5 deletions examples/basic-server-preact/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/

import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { createMcpExpressApp } from "@modelcontextprotocol/express";
import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
import type { McpServer } from "@modelcontextprotocol/server";
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
Expand All @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(

app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
const transport = new StreamableHTTPServerTransport({
const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});

Expand Down
5 changes: 4 additions & 1 deletion examples/basic-server-preact/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,11 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
"@modelcontextprotocol/client": "2.0.0-beta.4",
"@modelcontextprotocol/ext-apps": "^1.7.0",
"@modelcontextprotocol/sdk": "^1.29.0",
"@modelcontextprotocol/express": "2.0.0-beta.4",
"@modelcontextprotocol/node": "2.0.0-beta.4",
"@modelcontextprotocol/server": "2.0.0-beta.4",
"cors": "^2.8.5",
"express": "^5.1.0",
"preact": "^10.0.0",
Expand Down
10 changes: 7 additions & 3 deletions examples/basic-server-preact/server.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
import {
McpServer,
type CallToolResult,
type ReadResourceResult,
} from "@modelcontextprotocol/server";
import fs from "node:fs/promises";
import path from "node:path";
import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
import { z } from "zod";
// Works both from source (server.ts) and compiled (dist/server.js)
const DIST_DIR = import.meta.filename.endsWith(".ts")
? path.join(import.meta.dirname, "dist")
Expand All @@ -28,7 +32,7 @@ export function createServer(): McpServer {
{
title: "Get Time",
description: "Returns the current server time as an ISO 8601 string.",
inputSchema: {},
inputSchema: z.object({}),
_meta: { ui: { resourceUri } }, // Links this tool to its UI resource
},
async (): Promise<CallToolResult> => {
Expand Down
2 changes: 1 addition & 1 deletion examples/basic-server-preact/src/mcp-app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
applyHostStyleVariables,
type McpUiHostContext,
} from "@modelcontextprotocol/ext-apps";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import type { CallToolResult } from "@modelcontextprotocol/client";
import { useCallback, useEffect, useState } from "preact/hooks";
import { render } from "preact";
import styles from "./mcp-app.module.css";
Expand Down
Loading
Loading