diff --git a/README.md b/README.md
index de0c4ade7..52f21796b 100644
--- a/README.md
+++ b/README.md
@@ -28,49 +28,91 @@
-
-
Color picker built with MCP Apps, running in Claude
+
+
Excalidraw built with MCP Apps, running in Claude
## Table of Contents
-- [Specification](#specification)
+- [Build with Agent Skills](#build-with-agent-skills)
+- [Supported Clients](#supported-clients)
- [Why MCP Apps?](#why-mcp-apps)
- [How It Works](#how-it-works)
- [Getting Started](#getting-started)
- [Using the SDK](#using-the-sdk)
-- [Agent Skills](#agent-skills)
- [Examples](#examples)
+- [Specification](#specification)
- [Resources](#resources)
- [Contributing](#contributing)
-## Specification
+## Build with Agent Skills
-
+The fastest way to build an MCP App is to let your AI coding agent do it. This
+repo ships four [Agent Skills](https://agentskills.io/) — install them once,
+then just ask:
-| Version | Status | Link |
-| -------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------- |
-| **2026-01-26** | Stable | [specification/2026-01-26/apps.mdx](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx) |
-| **draft** | Development | [specification/draft/apps.mdx](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx) |
+| Skill | What it does | Try it |
+| --------------------------------------------------------------------------- | ----------------------------------------------------------- | ------------------------------------- |
+| [`create-mcp-app`](./plugins/mcp-apps/skills/create-mcp-app/SKILL.md) | Scaffolds a new MCP App with an interactive UI from scratch | _"Create an MCP App"_ |
+| [`migrate-oai-app`](./plugins/mcp-apps/skills/migrate-oai-app/SKILL.md) | Converts an existing OpenAI App to use MCP Apps | _"Migrate from OpenAI Apps SDK"_ |
+| [`add-app-to-server`](./plugins/mcp-apps/skills/add-app-to-server/SKILL.md) | Adds interactive UI to an existing MCP server's tools | _"Add UI to my MCP server"_ |
+| [`convert-web-app`](./plugins/mcp-apps/skills/convert-web-app/SKILL.md) | Turns an existing web app into a hybrid web + MCP App | _"Add MCP App support to my web app"_ |
-
+### Install the Skills
+
+**Claude Code** — install via the plugin marketplace:
+
+```
+/plugin marketplace add modelcontextprotocol/ext-apps
+/plugin install mcp-apps@modelcontextprotocol-ext-apps
+```
+
+**Other agents** — any AI coding agent that supports
+[Agent Skills](https://agentskills.io/) can use these skills. See the
+[agent skills guide](./docs/agent-skills.md) for manual installation
+instructions.
+
+Once installed, verify by asking your agent _"What skills do you have?"_ — you
+should see `create-mcp-app`, `migrate-oai-app`, `add-app-to-server`, and
+`convert-web-app` in the list. Then just ask it to create or migrate an app and
+it will guide you through the rest.
+
+## Supported Clients
-MCP Apps are a proposed standard inspired by [MCP-UI](https://mcpui.dev/) and [OpenAI's Apps SDK](https://developers.openai.com/apps-sdk/) to allow MCP Servers to display interactive UI elements in conversational MCP clients / chatbots.
+
+
+
+
+
+
+
+
+
+> [!NOTE] MCP Apps is an extension to the
+> [core MCP specification](https://modelcontextprotocol.io/specification). Host
+> support varies — see the
+> [clients page](https://modelcontextprotocol.io/clients) for the full list.
## Why MCP Apps?
-MCP tools return text and structured data. That works for many cases, but not when you need an interactive UI, like a chart, form, or video player.
+MCP tools return text and structured data. That works for many cases, but not
+when you need an interactive UI, like a chart, form, or video player.
-MCP Apps provide a standardized way to deliver interactive UIs from MCP servers. Your UI renders inline in the conversation, in context, in any compliant host.
+MCP Apps provide a standardized way to deliver interactive UIs from MCP servers.
+Your UI renders inline in the conversation, in context, in any compliant host.
## How It Works
-MCP Apps extend the Model Context Protocol by letting tools declare UI resources:
+MCP Apps extend the Model Context Protocol by letting tools declare UI
+resources:
-1. **Tool definition** — Your tool declares a `ui://` resource containing its HTML interface
+1. **Tool definition** — Your tool declares a `ui://` resource containing its
+ HTML interface
2. **Tool call** — The LLM calls the tool on your server
-3. **Host renders** — The host fetches the resource and displays it in a sandboxed iframe
-4. **Bidirectional communication** — The host passes tool data to the UI via notifications, and the UI can call other tools through the host
+3. **Host renders** — The host fetches the resource and displays it in a
+ sandboxed iframe
+4. **Bidirectional communication** — The host passes tool data to the UI via
+ notifications, and the UI can call other tools through the host
## Getting Started
@@ -78,11 +120,15 @@ MCP Apps extend the Model Context Protocol by letting tools declare UI resources
npm install -S @modelcontextprotocol/ext-apps
```
-**New here?** Start with the [Quickstart Guide](https://modelcontextprotocol.github.io/ext-apps/api/documents/Quickstart.html) to build your first MCP App.
+**New here?** Start with the
+[Quickstart Guide](https://modelcontextprotocol.github.io/ext-apps/api/documents/Quickstart.html)
+to build your first MCP App.
## Using the SDK
-The SDK serves three roles: app developers building interactive Views, host developers embedding those Views, and MCP server authors registering tools with UI metadata.
+The SDK serves three roles: app developers building interactive Views, host
+developers embedding those Views, and MCP server authors registering tools with
+UI metadata.
| Package | Purpose | Docs |
| ------------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
@@ -91,24 +137,19 @@ The SDK serves three roles: app developers building interactive Views, host deve
| `@modelcontextprotocol/ext-apps/app-bridge` | Embed and communicate with Views in your chat client | [API Docs →](https://modelcontextprotocol.github.io/ext-apps/api/modules/app-bridge.html) |
| `@modelcontextprotocol/ext-apps/server` | Register tools and resources on your MCP server | [API Docs →](https://modelcontextprotocol.github.io/ext-apps/api/modules/server.html) |
-There's no _supported_ host implementation in this repo (beyond the [examples/basic-host](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host) example).
+There's no _supported_ host implementation in this repo (beyond the
+[examples/basic-host](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host)
+example).
-The [MCP-UI](https://github.com/idosal/mcp-ui) client SDK offers a fully-featured MCP Apps framework used by a few hosts. Clients may choose to use it or roll their own implementation.
-
-## Agent Skills
-
-This repository provides two [Agent Skills](https://agentskills.io/) for building MCP Apps. You can install the skills as a Claude Code plugin:
-
-```
-/plugin marketplace add modelcontextprotocol/ext-apps
-/plugin install mcp-apps@modelcontextprotocol-ext-apps
-```
-
-For more information, including instructions for installing the skills in your favorite AI coding agent, see the [agent skills guide](./docs/agent-skills.md).
+The [MCP-UI](https://github.com/idosal/mcp-ui) client SDK offers a
+fully-featured MCP Apps framework used by a few hosts. Clients may choose to use
+it or roll their own implementation.
## Examples
-The [`examples/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples) directory contains demo apps showcasing real-world use cases.
+The
+[`examples/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples)
+directory contains demo apps showcasing real-world use cases.
| | | |
@@ -135,7 +176,9 @@ The [`examples/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/exa
#### With basic-host
-To run all examples locally using [basic-host](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host) (the reference host implementation included in this repo):
+To run all examples locally using
+[basic-host](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host)
+(the reference host implementation included in this repo):
```bash
git clone https://github.com/modelcontextprotocol/ext-apps.git
@@ -148,411 +191,64 @@ Then open http://localhost:8080/.
#### With MCP Clients
-To use these examples with MCP clients that support the stdio transport (such as Claude Desktop or VS Code), add this MCP server configuration to your client's settings:
-
-
-MCP client configuration for all examples (using stdio)
+Every Node.js example is published as `@modelcontextprotocol/server-`. To
+add one to an MCP client that supports stdio (Claude Desktop, VS Code, etc.),
+use this pattern:
```json
{
"mcpServers": {
- "basic-react": {
- "command": "npx",
- "args": [
- "-y",
- "--silent",
- "--registry=https://registry.npmjs.org/",
- "@modelcontextprotocol/server-basic-react",
- "--stdio"
- ]
- },
- "basic-vanillajs": {
- "command": "npx",
- "args": [
- "-y",
- "--silent",
- "--registry=https://registry.npmjs.org/",
- "@modelcontextprotocol/server-basic-vanillajs",
- "--stdio"
- ]
- },
- "basic-vue": {
- "command": "npx",
- "args": [
- "-y",
- "--silent",
- "--registry=https://registry.npmjs.org/",
- "@modelcontextprotocol/server-basic-vue",
- "--stdio"
- ]
- },
- "basic-svelte": {
- "command": "npx",
- "args": [
- "-y",
- "--silent",
- "--registry=https://registry.npmjs.org/",
- "@modelcontextprotocol/server-basic-svelte",
- "--stdio"
- ]
- },
- "basic-preact": {
- "command": "npx",
- "args": [
- "-y",
- "--silent",
- "--registry=https://registry.npmjs.org/",
- "@modelcontextprotocol/server-basic-preact",
- "--stdio"
- ]
- },
- "basic-solid": {
- "command": "npx",
- "args": [
- "-y",
- "--silent",
- "--registry=https://registry.npmjs.org/",
- "@modelcontextprotocol/server-basic-solid",
- "--stdio"
- ]
- },
- "budget-allocator": {
- "command": "npx",
- "args": [
- "-y",
- "--silent",
- "--registry=https://registry.npmjs.org/",
- "@modelcontextprotocol/server-budget-allocator",
- "--stdio"
- ]
- },
- "cohort-heatmap": {
- "command": "npx",
- "args": [
- "-y",
- "--silent",
- "--registry=https://registry.npmjs.org/",
- "@modelcontextprotocol/server-cohort-heatmap",
- "--stdio"
- ]
- },
- "customer-segmentation": {
- "command": "npx",
- "args": [
- "-y",
- "--silent",
- "--registry=https://registry.npmjs.org/",
- "@modelcontextprotocol/server-customer-segmentation",
- "--stdio"
- ]
- },
- "map": {
- "command": "npx",
- "args": [
- "-y",
- "--silent",
- "--registry=https://registry.npmjs.org/",
- "@modelcontextprotocol/server-map",
- "--stdio"
- ]
- },
- "pdf": {
- "command": "npx",
- "args": [
- "-y",
- "--silent",
- "--registry=https://registry.npmjs.org/",
- "@modelcontextprotocol/server-pdf",
- "--stdio"
- ]
- },
- "scenario-modeler": {
- "command": "npx",
- "args": [
- "-y",
- "--silent",
- "--registry=https://registry.npmjs.org/",
- "@modelcontextprotocol/server-scenario-modeler",
- "--stdio"
- ]
- },
- "shadertoy": {
- "command": "npx",
- "args": [
- "-y",
- "--silent",
- "--registry=https://registry.npmjs.org/",
- "@modelcontextprotocol/server-shadertoy",
- "--stdio"
- ]
- },
- "sheet-music": {
+ "": {
"command": "npx",
- "args": [
- "-y",
- "--silent",
- "--registry=https://registry.npmjs.org/",
- "@modelcontextprotocol/server-sheet-music",
- "--stdio"
- ]
- },
- "system-monitor": {
- "command": "npx",
- "args": [
- "-y",
- "--silent",
- "--registry=https://registry.npmjs.org/",
- "@modelcontextprotocol/server-system-monitor",
- "--stdio"
- ]
- },
- "threejs": {
- "command": "npx",
- "args": [
- "-y",
- "--silent",
- "--registry=https://registry.npmjs.org/",
- "@modelcontextprotocol/server-threejs",
- "--stdio"
- ]
- },
- "transcript": {
- "command": "npx",
- "args": [
- "-y",
- "--silent",
- "--registry=https://registry.npmjs.org/",
- "@modelcontextprotocol/server-transcript",
- "--stdio"
- ]
- },
- "video-resource": {
- "command": "npx",
- "args": [
- "-y",
- "--silent",
- "--registry=https://registry.npmjs.org/",
- "@modelcontextprotocol/server-video-resource",
- "--stdio"
- ]
- },
- "wiki-explorer": {
- "command": "npx",
- "args": [
- "-y",
- "--silent",
- "--registry=https://registry.npmjs.org/",
- "@modelcontextprotocol/server-wiki-explorer",
- "--stdio"
- ]
- },
- "qr": {
- "command": "uv",
- "args": [
- "run",
- "/path/to/ext-apps/examples/qr-server/server.py",
- "--stdio"
- ]
- },
- "say": {
- "command": "uv",
- "args": [
- "run",
- "--default-index",
- "https://pypi.org/simple",
- "https://raw.githubusercontent.com/modelcontextprotocol/ext-apps/refs/heads/main/examples/say-server/server.py",
- "--stdio"
- ]
+ "args": ["-y", "@modelcontextprotocol/server-", "--stdio"]
}
}
}
```
-
-
-> [!NOTE]
-> The `qr` server requires cloning the repository first. See [qr-server README](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/qr-server) for details.
+For example, to add the map server: `@modelcontextprotocol/server-map`. The
+Python examples (`qr-server`, `say-server`) use `uv run` instead — see their
+READMEs for details.
#### Local Development
-To test local modifications with MCP clients, first clone and install the repository:
-
-```bash
-git clone https://github.com/modelcontextprotocol/ext-apps.git
-cd ext-apps
-npm install
-```
-
-Then configure your MCP client to build and run the local server. Replace `~/code/ext-apps` with your actual clone path:
-
-
-MCP client configuration for local development (all examples)
+To test local modifications with an MCP client, clone the repo, install, then
+point your client at a local build:
```json
{
"mcpServers": {
- "basic-react": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/code/ext-apps/examples/basic-server-react && npm run build >&2 && node dist/index.js --stdio"
- ]
- },
- "basic-vanillajs": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/code/ext-apps/examples/basic-server-vanillajs && npm run build >&2 && node dist/index.js --stdio"
- ]
- },
- "basic-vue": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/code/ext-apps/examples/basic-server-vue && npm run build >&2 && node dist/index.js --stdio"
- ]
- },
- "basic-svelte": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/code/ext-apps/examples/basic-server-svelte && npm run build >&2 && node dist/index.js --stdio"
- ]
- },
- "basic-preact": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/code/ext-apps/examples/basic-server-preact && npm run build >&2 && node dist/index.js --stdio"
- ]
- },
- "basic-solid": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/code/ext-apps/examples/basic-server-solid && npm run build >&2 && node dist/index.js --stdio"
- ]
- },
- "budget-allocator": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/code/ext-apps/examples/budget-allocator-server && npm run build >&2 && node dist/index.js --stdio"
- ]
- },
- "cohort-heatmap": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/code/ext-apps/examples/cohort-heatmap-server && npm run build >&2 && node dist/index.js --stdio"
- ]
- },
- "customer-segmentation": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/code/ext-apps/examples/customer-segmentation-server && npm run build >&2 && node dist/index.js --stdio"
- ]
- },
- "map": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/code/ext-apps/examples/map-server && npm run build >&2 && node dist/index.js --stdio"
- ]
- },
- "pdf": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/code/ext-apps/examples/pdf-server && npm run build >&2 && node dist/index.js --stdio"
- ]
- },
- "scenario-modeler": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/code/ext-apps/examples/scenario-modeler-server && npm run build >&2 && node dist/index.js --stdio"
- ]
- },
- "shadertoy": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/code/ext-apps/examples/shadertoy-server && npm run build >&2 && node dist/index.js --stdio"
- ]
- },
- "sheet-music": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/code/ext-apps/examples/sheet-music-server && npm run build >&2 && node dist/index.js --stdio"
- ]
- },
- "system-monitor": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/code/ext-apps/examples/system-monitor-server && npm run build >&2 && node dist/index.js --stdio"
- ]
- },
- "threejs": {
+ "": {
"command": "bash",
"args": [
"-c",
- "cd ~/code/ext-apps/examples/threejs-server && npm run build >&2 && node dist/index.js --stdio"
- ]
- },
- "transcript": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/code/ext-apps/examples/transcript-server && npm run build >&2 && node dist/index.js --stdio"
- ]
- },
- "video-resource": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/code/ext-apps/examples/video-resource-server && npm run build >&2 && node dist/index.js --stdio"
- ]
- },
- "wiki-explorer": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/code/ext-apps/examples/wiki-explorer-server && npm run build >&2 && node dist/index.js --stdio"
- ]
- },
- "qr": {
- "command": "bash",
- "args": [
- "-c",
- "uv run ~/code/ext-apps/examples/qr-server/server.py --stdio"
- ]
- },
- "say": {
- "command": "bash",
- "args": [
- "-c",
- "uv run --index https://pypi.org/simple ~/code/ext-apps/examples/say-server/server.py --stdio"
+ "cd ~/code/ext-apps/examples/-server && npm run build >&2 && node dist/index.js --stdio"
]
}
}
}
```
-
+## Specification
+
+
-This configuration rebuilds each server on launch, ensuring your local changes are picked up.
+| Version | Status | Link |
+| -------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------- |
+| **2026-01-26** | Stable | [specification/2026-01-26/apps.mdx](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx) |
+| **draft** | Development | [specification/draft/apps.mdx](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx) |
+
+
## Resources
- [Quickstart Guide](https://modelcontextprotocol.github.io/ext-apps/api/documents/Quickstart.html)
- [API Documentation](https://modelcontextprotocol.github.io/ext-apps/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))
+- [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))
- [SEP-1865 Discussion](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1865)
## Contributing
-Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on how to get started, submit pull requests, and report issues.
+Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for
+guidelines on how to get started, submit pull requests, and report issues.
diff --git a/docs/agent-skills.md b/docs/agent-skills.md
index 4d957a2c6..36e1f989e 100644
--- a/docs/agent-skills.md
+++ b/docs/agent-skills.md
@@ -6,10 +6,12 @@ title: Agent Skills
[Agent Skills](https://agentskills.io/) are instruction sets that guide AI coding agents through tasks. When you invoke a skill, the agent takes the lead — it asks clarifying questions, makes decisions based on your codebase, and executes the work.
-This repository provides two skills:
+This repository provides four skills:
- [**create-mcp-app**](https://github.com/modelcontextprotocol/ext-apps/blob/main/plugins/mcp-apps/skills/create-mcp-app/SKILL.md) — scaffolds a new MCP App with an interactive UI
- [**migrate-oai-app**](https://github.com/modelcontextprotocol/ext-apps/blob/main/plugins/mcp-apps/skills/migrate-oai-app/SKILL.md) — migrates an existing OpenAI App to the MCP Apps SDK
+- [**add-app-to-server**](https://github.com/modelcontextprotocol/ext-apps/blob/main/plugins/mcp-apps/skills/add-app-to-server/SKILL.md) — adds interactive UI to an existing MCP server's tools
+- [**convert-web-app**](https://github.com/modelcontextprotocol/ext-apps/blob/main/plugins/mcp-apps/skills/convert-web-app/SKILL.md) — converts an existing web application into an MCP App
## Install the Skills
@@ -51,7 +53,7 @@ Then copy the skills from `plugins/mcp-apps/skills/` to your agent's skills dire
## Verify Installation
-Ask your agent "What skills do you have?" — you should see `create-mcp-app` and `migrate-oai-app` among the available skills.
+Ask your agent "What skills do you have?" — you should see `create-mcp-app`, `migrate-oai-app`, `add-app-to-server`, and `convert-web-app` among the available skills.
## Invoke a Skill
@@ -59,6 +61,8 @@ Try invoking the skills by asking your agent:
- "Create an MCP App" — scaffolds a new MCP App with an interactive UI
- "Migrate from OpenAI Apps SDK" — converts an existing OpenAI App to use the MCP Apps SDK
+- "Add UI to my MCP server" — adds interactive UI to an existing MCP server's tools
+- "Convert my web app to an MCP App" — converts an existing web application into an MCP App
The agent will guide you through the process, asking clarifying questions as needed.
diff --git a/media/excalidraw.gif b/media/excalidraw.gif
new file mode 100644
index 000000000..848e1c29a
Binary files /dev/null and b/media/excalidraw.gif differ
diff --git a/plugins/mcp-apps/skills/add-app-to-server/SKILL.md b/plugins/mcp-apps/skills/add-app-to-server/SKILL.md
new file mode 100644
index 000000000..02d09a4e2
--- /dev/null
+++ b/plugins/mcp-apps/skills/add-app-to-server/SKILL.md
@@ -0,0 +1,389 @@
+---
+name: add-app-to-server
+description: This skill should be used when the user asks to "add an app to my MCP server", "add UI to my MCP server", "add a view to my MCP tool", "enrich MCP tools with UI", "add interactive UI to existing server", "add MCP Apps to my server", or needs to add interactive UI capabilities to an existing MCP server that already has tools. Provides guidance for analyzing existing tools and adding MCP Apps UI resources.
+---
+
+# Add UI to MCP Server
+
+Enrich an existing MCP server's tools with interactive UIs using the MCP Apps SDK (`@modelcontextprotocol/ext-apps`).
+
+## How It Works
+
+Existing tools get paired with HTML resources that render inline in the host's conversation. The tool continues to work for text-only clients — UI is an enhancement, not a replacement. Each tool that benefits from UI gets linked to a resource via `_meta.ui.resourceUri`, and the host renders that resource in a sandboxed iframe when the tool is called.
+
+## Getting Reference Code
+
+Clone the SDK repository for working examples and API documentation:
+
+```bash
+git clone --branch "v$(npm view @modelcontextprotocol/ext-apps version)" --depth 1 https://github.com/modelcontextprotocol/ext-apps.git /tmp/mcp-ext-apps
+```
+
+### API Reference (Source Files)
+
+Read JSDoc documentation directly from `/tmp/mcp-ext-apps/src/`:
+
+| File | Contents |
+|------|----------|
+| `src/app.ts` | `App` class, handlers (`ontoolinput`, `ontoolresult`, `onhostcontextchanged`, `onteardown`), lifecycle |
+| `src/server/index.ts` | `registerAppTool`, `registerAppResource`, `getUiCapability`, tool visibility options |
+| `src/spec.types.ts` | All type definitions: `McpUiHostContext`, CSS variable keys, display modes |
+| `src/styles.ts` | `applyDocumentTheme`, `applyHostStyleVariables`, `applyHostFonts` |
+| `src/react/useApp.tsx` | `useApp` hook for React apps |
+| `src/react/useHostStyles.ts` | `useHostStyles`, `useHostStyleVariables`, `useHostFonts` hooks |
+
+### Key Examples (Mixed Tool Patterns)
+
+These examples demonstrate servers with both App-enhanced and plain tools — the exact pattern you're adding:
+
+| Example | Pattern |
+|---------|---------|
+| `examples/map-server/` | `show-map` (App tool) + `geocode` (plain tool) |
+| `examples/pdf-server/` | `display_pdf` (App tool) + `list_pdfs` (plain tool) + `read_pdf_bytes` (app-only tool) |
+| `examples/system-monitor-server/` | `get-system-info` (App tool) + `poll-system-stats` (app-only polling tool) |
+
+### Framework Templates
+
+Learn and adapt from `/tmp/mcp-ext-apps/examples/basic-server-{framework}/`:
+
+| Template | Key Files |
+|----------|-----------|
+| `basic-server-vanillajs/` | `server.ts`, `src/mcp-app.ts`, `mcp-app.html` |
+| `basic-server-react/` | `server.ts`, `src/mcp-app.tsx` (uses `useApp` hook) |
+| `basic-server-vue/` | `server.ts`, `src/App.vue` |
+| `basic-server-svelte/` | `server.ts`, `src/App.svelte` |
+| `basic-server-preact/` | `server.ts`, `src/mcp-app.tsx` |
+| `basic-server-solid/` | `server.ts`, `src/mcp-app.tsx` |
+
+## Step 1: Analyze Existing Tools
+
+Before writing any code, analyze the server's existing tools and determine which ones benefit from UI.
+
+1. Read the server source and list all registered tools
+2. For each tool, assess whether it would benefit from UI (returns data that could be visualized, involves user interaction, etc.) vs. is fine as text-only (simple lookups, utility functions)
+3. Identify tools that could become **app-only helpers** (data the UI needs to poll/fetch but the model doesn't need to call directly)
+4. Present the analysis to the user and confirm which tools to enhance
+
+### Decision Framework
+
+| Tool output type | UI benefit | Example |
+|---|---|---|
+| Structured data / lists / tables | High — interactive table, search, filtering | List of items, search results |
+| Metrics / numbers over time | High — charts, gauges, dashboards | System stats, analytics |
+| Media / rich content | High — viewer, player, renderer | Maps, PDFs, images, video |
+| Simple text / confirmations | Low — text is fine | "File created", "Setting updated" |
+| Data for other tools | Consider app-only | Polling endpoints, chunk loaders |
+
+## Step 2: Add Dependencies
+
+```bash
+npm install @modelcontextprotocol/ext-apps
+npm install -D vite vite-plugin-singlefile
+```
+
+Plus framework-specific dependencies if needed (e.g., `react`, `react-dom`, `@vitejs/plugin-react` for React).
+
+Use `npm install` to add dependencies rather than manually writing version numbers. This lets npm resolve the latest compatible versions. Never specify version numbers from memory.
+
+## Step 3: Set Up the Build Pipeline
+
+### Vite Configuration
+
+Create `vite.config.ts` with `vite-plugin-singlefile` to bundle the UI into a single HTML file:
+
+```typescript
+import { defineConfig } from "vite";
+import { viteSingleFile } from "vite-plugin-singlefile";
+
+export default defineConfig({
+ plugins: [viteSingleFile()],
+ build: {
+ outDir: "dist",
+ rollupOptions: {
+ input: "mcp-app.html", // one per UI, or one shared entry
+ },
+ },
+});
+```
+
+### HTML Entry Point
+
+Create `mcp-app.html` (or one per distinct UI if tools need different views):
+
+```html
+
+
+
+
+
+ MCP App
+
+
+
+
+
+
+```
+
+### Build Scripts
+
+Add build scripts to `package.json`. The UI must be built before the server code bundles it:
+
+```json
+{
+ "scripts": {
+ "build:ui": "vite build",
+ "build:server": "tsc",
+ "build": "npm run build:ui && npm run build:server",
+ "serve": "tsx server.ts"
+ }
+}
+```
+
+## Step 4: Convert Tools to App Tools
+
+Transform plain MCP tools into App tools with UI.
+
+**Before** (plain MCP tool):
+```typescript
+server.tool("my-tool", { param: z.string() }, async (args) => {
+ const data = await fetchData(args.param);
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
+});
+```
+
+**After** (App tool with UI):
+```typescript
+import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
+
+const resourceUri = "ui://my-tool/mcp-app.html";
+
+registerAppTool(server, "my-tool", {
+ description: "Shows data with an interactive UI",
+ inputSchema: { param: z.string() },
+ _meta: { ui: { resourceUri } },
+}, async (args) => {
+ const data = await fetchData(args.param);
+ return {
+ content: [{ type: "text", text: JSON.stringify(data) }], // text fallback for non-UI hosts
+ structuredContent: { data }, // structured data for the UI
+ };
+});
+```
+
+Key guidance:
+- **Always keep the `content` array** with a text fallback for text-only clients
+- Add `structuredContent` for data the UI needs to render
+- Link the tool to its resource via `_meta.ui.resourceUri`
+- Leave tools that don't benefit from UI unchanged — they stay as plain tools
+
+## Step 5: Register Resources
+
+Register the HTML resource so the host can fetch it:
+
+```typescript
+import fs from "node:fs/promises";
+import path from "node:path";
+
+const resourceUri = "ui://my-tool/mcp-app.html";
+
+registerAppResource(server, {
+ uri: resourceUri,
+ name: "My Tool UI",
+ mimeType: RESOURCE_MIME_TYPE,
+}, async () => {
+ const html = await fs.readFile(
+ path.resolve(import.meta.dirname, "dist", "mcp-app.html"),
+ "utf-8",
+ );
+ return { contents: [{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }] };
+});
+```
+
+If multiple tools share the same UI, they can reference the same `resourceUri` and the same resource registration.
+
+## Step 6: Build the UI
+
+### Handler Registration
+
+Register ALL handlers BEFORE calling `app.connect()`:
+
+```typescript
+import { App, PostMessageTransport, applyDocumentTheme, applyHostStyleVariables, applyHostFonts } from "@modelcontextprotocol/ext-apps";
+
+const app = new App({ name: "My App", version: "1.0.0" });
+
+app.ontoolinput = (params) => {
+ // Render the UI using params.arguments and/or params.structuredContent
+};
+
+app.ontoolresult = (result) => {
+ // Update UI with final tool result
+};
+
+app.onhostcontextchanged = (ctx) => {
+ if (ctx.theme) applyDocumentTheme(ctx.theme);
+ if (ctx.styles?.variables) applyHostStyleVariables(ctx.styles.variables);
+ if (ctx.styles?.css?.fonts) applyHostFonts(ctx.styles.css.fonts);
+ if (ctx.safeAreaInsets) {
+ const { top, right, bottom, left } = ctx.safeAreaInsets;
+ document.body.style.padding = `${top}px ${right}px ${bottom}px ${left}px`;
+ }
+};
+
+app.onteardown = async () => {
+ return {};
+};
+
+await app.connect(new PostMessageTransport());
+```
+
+### Host Styling
+
+Use host CSS variables for theme integration:
+
+```css
+.container {
+ background: var(--color-background-secondary);
+ color: var(--color-text-primary);
+ font-family: var(--font-sans);
+ border-radius: var(--border-radius-md);
+}
+```
+
+Key variable groups: `--color-background-*`, `--color-text-*`, `--color-border-*`, `--font-sans`, `--font-mono`, `--font-text-*-size`, `--font-heading-*-size`, `--border-radius-*`. See `src/spec.types.ts` for the full list.
+
+For React apps, use the `useApp` and `useHostStyles` hooks instead — see `basic-server-react/` for the pattern.
+
+## Optional Enhancements
+
+### App-Only Helper Tools
+
+Tools the UI calls but the model doesn't need to invoke directly (polling, pagination, chunk loading):
+
+```typescript
+registerAppTool(server, "poll-data", {
+ description: "Polls latest data for the UI",
+ _meta: { ui: { resourceUri, visibility: ["app"] } },
+}, async () => {
+ const data = await getLatestData();
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
+});
+```
+
+The UI calls these via `app.callServerTool("poll-data", {})`.
+
+### CSP Configuration
+
+If the UI needs to load external resources (fonts, APIs, CDNs), declare the domains:
+
+```typescript
+registerAppResource(server, {
+ uri: resourceUri,
+ name: "My Tool UI",
+ mimeType: RESOURCE_MIME_TYPE,
+ _meta: {
+ ui: {
+ connectDomains: ["api.example.com"], // fetch/XHR targets
+ resourceDomains: ["cdn.example.com"], // scripts, styles, images
+ frameDomains: ["embed.example.com"], // nested iframes
+ },
+ },
+}, async () => { /* ... */ });
+```
+
+### Streaming Partial Input
+
+For large tool inputs, show progress during LLM generation:
+
+```typescript
+app.ontoolinputpartial = (params) => {
+ const args = params.arguments; // Healed partial JSON - always valid
+ // Render preview with partial data
+};
+
+app.ontoolinput = (params) => {
+ // Final complete input - switch to full render
+};
+```
+
+### Graceful Degradation with `getUiCapability()`
+
+Conditionally register App tools only when the client supports UI, falling back to text-only tools:
+
+```typescript
+import { getUiCapability, registerAppTool, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
+
+server.server.oninitialized = () => {
+ const clientCapabilities = server.server.getClientCapabilities();
+ const uiCap = getUiCapability(clientCapabilities);
+
+ if (uiCap?.mimeTypes?.includes(RESOURCE_MIME_TYPE)) {
+ // Client supports UI — register App tool
+ registerAppTool(server, "my-tool", {
+ description: "Shows data with interactive UI",
+ _meta: { ui: { resourceUri } },
+ }, appToolHandler);
+ } else {
+ // Text-only client — register plain tool
+ server.tool("my-tool", "Shows data", { param: z.string() }, plainToolHandler);
+ }
+};
+```
+
+### Fullscreen Mode
+
+Allow the UI to expand to fullscreen:
+
+```typescript
+app.onhostcontextchanged = (ctx) => {
+ if (ctx.availableDisplayModes?.includes("fullscreen")) {
+ fullscreenBtn.style.display = "block";
+ }
+ if (ctx.displayMode) {
+ container.classList.toggle("fullscreen", ctx.displayMode === "fullscreen");
+ }
+};
+
+async function toggleFullscreen() {
+ const newMode = currentMode === "fullscreen" ? "inline" : "fullscreen";
+ const result = await app.requestDisplayMode({ mode: newMode });
+ currentMode = result.mode;
+}
+```
+
+## Common Mistakes to Avoid
+
+1. **Forgetting text `content` fallback** — Always include `content` array with text for non-UI hosts
+2. **Registering handlers after `connect()`** — Register ALL handlers BEFORE calling `app.connect()`
+3. **Missing `vite-plugin-singlefile`** — Without it, assets won't load in the sandboxed iframe
+4. **Forgetting resource registration** — The tool references a `resourceUri` that must have a matching resource
+5. **Hardcoding styles** — Use host CSS variables (`var(--color-*)`) for theme integration
+6. **Not handling safe area insets** — Always apply `ctx.safeAreaInsets` in `onhostcontextchanged`
+
+## Testing
+
+### Using basic-host
+
+Test the enhanced server with the basic-host example:
+
+```bash
+# Terminal 1: Build and run your server
+npm run build && npm run serve
+
+# Terminal 2: Run basic-host (from cloned repo)
+cd /tmp/mcp-ext-apps/examples/basic-host
+npm install
+SERVERS='["http://localhost:3001/mcp"]' npm run start
+# Open http://localhost:8080
+```
+
+Configure `SERVERS` with a JSON array of your server URLs (default: `http://localhost:3001/mcp`).
+
+### Verify
+
+1. Plain tools still work and return text output
+2. App tools render their UI in the iframe
+3. `ontoolinput` handler fires with tool arguments
+4. `ontoolresult` handler fires with tool result
+5. Host styling (theme, fonts, colors) applies correctly
diff --git a/plugins/mcp-apps/skills/convert-web-app/SKILL.md b/plugins/mcp-apps/skills/convert-web-app/SKILL.md
new file mode 100644
index 000000000..5db2d946c
--- /dev/null
+++ b/plugins/mcp-apps/skills/convert-web-app/SKILL.md
@@ -0,0 +1,525 @@
+---
+name: convert-web-app
+description: This skill should be used when the user asks to "add MCP App support to my web app", "turn my web app into a hybrid MCP App", "make my web page work as an MCP App too", "wrap my existing UI as an MCP App", "convert iframe embed to MCP App", "turn my SPA into an MCP App", or needs to add MCP App support to an existing web application while keeping it working standalone. Provides guidance for analyzing existing web apps and creating a hybrid web + MCP App with server-side tool and resource registration.
+---
+
+# Add MCP App Support to a Web App
+
+Add MCP App support to an existing web application so it works both as a standalone web app **and** as an MCP App that renders inline in MCP-enabled hosts like Claude Desktop — from a single codebase.
+
+## How It Works
+
+The existing web app stays intact. A thin initialization layer detects whether the app is running inside an MCP host or as a regular web page, and fetches parameters from the appropriate source. A new MCP server wraps the app's bundled HTML as a resource and registers a tool to display it.
+
+```
+Standalone: Browser loads page → App reads URL params / APIs → renders
+MCP App: Host calls tool → Server returns result → Host renders app in iframe → App reads MCP lifecycle → renders
+```
+
+The app's rendering logic is shared — only the data source changes.
+
+## Getting Reference Code
+
+Clone the SDK repository for working examples and API documentation:
+
+```bash
+git clone --branch "v$(npm view @modelcontextprotocol/ext-apps version)" --depth 1 https://github.com/modelcontextprotocol/ext-apps.git /tmp/mcp-ext-apps
+```
+
+### API Reference (Source Files)
+
+Read JSDoc documentation directly from `/tmp/mcp-ext-apps/src/`:
+
+| File | Contents |
+|------|----------|
+| `src/app.ts` | `App` class, handlers (`ontoolinput`, `ontoolresult`, `onhostcontextchanged`, `onteardown`), lifecycle |
+| `src/server/index.ts` | `registerAppTool`, `registerAppResource`, tool visibility options |
+| `src/spec.types.ts` | All type definitions: `McpUiHostContext`, CSS variable keys, display modes |
+| `src/styles.ts` | `applyDocumentTheme`, `applyHostStyleVariables`, `applyHostFonts` |
+| `src/react/useApp.tsx` | `useApp` hook for React apps |
+| `src/react/useHostStyles.ts` | `useHostStyles`, `useHostStyleVariables`, `useHostFonts` hooks |
+
+### Framework Templates
+
+Learn and adapt from `/tmp/mcp-ext-apps/examples/basic-server-{framework}/`:
+
+| Template | Key Files |
+|----------|-----------|
+| `basic-server-vanillajs/` | `server.ts`, `src/mcp-app.ts`, `mcp-app.html` |
+| `basic-server-react/` | `server.ts`, `src/mcp-app.tsx` (uses `useApp` hook) |
+| `basic-server-vue/` | `server.ts`, `src/App.vue` |
+| `basic-server-svelte/` | `server.ts`, `src/App.svelte` |
+| `basic-server-preact/` | `server.ts`, `src/mcp-app.tsx` |
+| `basic-server-solid/` | `server.ts`, `src/mcp-app.tsx` |
+
+### Reference Examples
+
+| Example | Relevant Pattern |
+|---------|-----------------|
+| `examples/map-server/` | External API integration + CSP (`connectDomains`, `resourceDomains`) |
+| `examples/sheet-music-server/` | Library that loads external assets (soundfonts) |
+| `examples/pdf-server/` | Binary content handling + app-only helper tools |
+
+## Step 1: Analyze the Existing Web App
+
+Before writing any code, examine the existing web app to plan what needs to change.
+
+### What to Investigate
+
+1. **Data sources** — How does the app get its data? (URL params, API calls, props, hardcoded, localStorage)
+2. **External dependencies** — CDN scripts, fonts, API endpoints, iframe embeds, WebSocket connections
+3. **Build system** — Current bundler (Webpack, Vite, Rollup, none), framework (React, Vue, vanilla), entry points
+4. **User interactions** — Does the app have inputs/forms that should map to tool parameters?
+5. **Runtime detection** — How to tell if the app is running inside an MCP host (e.g., check the current origin, a query param, or whether `window.parent !== window`)
+
+Present findings to the user and confirm the approach.
+
+### Data Source Mapping
+
+In hybrid mode, the app keeps its existing data sources for standalone use and adds MCP equivalents:
+
+| Standalone data source | MCP App equivalent |
+|---|---|
+| URL query parameters | `ontoolinput` / `ontoolresult` `arguments` or `structuredContent` |
+| REST API calls | `app.callServerTool()` to server-side tools, or keep direct API calls with CSP `connectDomains` |
+| Props / component inputs | `ontoolinput` `arguments` |
+| localStorage / sessionStorage | Not available in sandboxed iframe — pass via `structuredContent` or server-side state |
+| WebSocket connections | Keep with CSP `connectDomains`, or convert to polling via app-only tools |
+| Hardcoded data | Move to tool `structuredContent` to make it dynamic |
+
+## Step 2: Investigate CSP Requirements
+
+MCP Apps HTML runs in a sandboxed iframe with no same-origin server. **Every** external origin must be declared in CSP — missing origins fail silently.
+
+**Before writing any code**, build the app and investigate all origins it references:
+
+1. Build the app using the existing build command
+2. Search the resulting HTML, CSS, and JS for **every** origin (not just "external" origins — every network request will need CSP approval)
+3. For each origin found, trace back to source:
+ - If it comes from a constant → universal (same in dev and prod)
+ - If it comes from an env var or conditional → note the mechanism and identify both dev and prod values
+4. Check for third-party libraries that may make their own requests (analytics, error tracking, etc.)
+
+**Document your findings** as three lists, and note for each origin whether it's universal, dev-only, or prod-only:
+
+- **resourceDomains**: origins serving images, fonts, styles, scripts
+- **connectDomains**: origins for API/fetch requests
+- **frameDomains**: origins for nested iframes
+
+If no origins are found, the app may not need custom CSP domains.
+
+## Step 3: Set Up the MCP Server
+
+Create a new MCP server with tool and resource registration. This wraps the existing web app for MCP hosts.
+
+### Dependencies
+
+```bash
+npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk zod
+npm install -D tsx vite vite-plugin-singlefile
+```
+
+Use `npm install` to add dependencies rather than manually writing version numbers. This lets npm resolve the latest compatible versions. Never specify version numbers from memory.
+
+### Server Code
+
+Create `server.ts`:
+
+```typescript
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
+import fs from "node:fs/promises";
+import path from "node:path";
+import { z } from "zod";
+
+const server = new McpServer({ name: "my-app", version: "1.0.0" });
+
+const resourceUri = "ui://my-app/mcp-app.html";
+
+// Register the tool — inputSchema maps to the app's data sources
+registerAppTool(server, "show-app", {
+ description: "Displays the app with the given parameters",
+ inputSchema: { query: z.string().describe("The search query") },
+ _meta: { ui: { resourceUri } },
+}, async (args) => {
+ // Process args server-side if needed
+ return {
+ content: [{ type: "text", text: `Showing app for: ${args.query}` }],
+ structuredContent: { query: args.query },
+ };
+});
+
+// Register the HTML resource
+registerAppResource(server, {
+ uri: resourceUri,
+ name: "My App UI",
+ mimeType: RESOURCE_MIME_TYPE,
+ // Add CSP domains from Step 2 if needed:
+ // _meta: { ui: { connectDomains: ["api.example.com"], resourceDomains: ["cdn.example.com"] } },
+}, async () => {
+ const html = await fs.readFile(
+ path.resolve(import.meta.dirname, "dist", "mcp-app.html"),
+ "utf-8",
+ );
+ return { contents: [{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }] };
+});
+
+// Start the server
+const transport = new StdioServerTransport();
+await server.connect(transport);
+```
+
+### Package Scripts
+
+Add to `package.json`:
+
+```json
+{
+ "scripts": {
+ "build:ui": "vite build",
+ "build:server": "tsc",
+ "build": "npm run build:ui && npm run build:server",
+ "serve": "tsx server.ts"
+ }
+}
+```
+
+## Step 4: Adapt the Build Pipeline
+
+The MCP App build must produce a single HTML file using `vite-plugin-singlefile`. The standalone web app build stays unchanged.
+
+### Vite Configuration
+
+Create or update `vite.config.ts`. If the app already uses Vite, add `vite-plugin-singlefile` and a separate entry point for the MCP App build. If it uses another bundler, add a Vite config alongside for the MCP App build only.
+
+```typescript
+import { defineConfig } from "vite";
+import { viteSingleFile } from "vite-plugin-singlefile";
+
+export default defineConfig({
+ plugins: [viteSingleFile()],
+ build: {
+ outDir: "dist",
+ rollupOptions: {
+ input: "mcp-app.html",
+ },
+ },
+});
+```
+
+Add framework-specific Vite plugins as needed (e.g., `@vitejs/plugin-react` for React, `@vitejs/plugin-vue` for Vue).
+
+### HTML Entry Point
+
+Create `mcp-app.html` as a separate entry point for the MCP App build. This can point to the same app code — the runtime detection handles the rest:
+
+```html
+
+
+
+
+
+ MCP App
+
+
+
+
+
+
+```
+
+### Two-Phase Build
+
+1. Vite bundles the UI → `dist/mcp-app.html` (single file with all assets inlined)
+2. Server is compiled separately (TypeScript → JavaScript)
+
+The standalone web app continues to build and deploy as before.
+
+## Step 5: Add MCP App Initialization Alongside Existing Logic
+
+This is the core step. Instead of replacing the app's data sources, add an alternative initialization path for MCP mode. The app detects its environment at startup and reads parameters from the right source.
+
+### The Hybrid Pattern
+
+```typescript
+import { App, PostMessageTransport } from "@modelcontextprotocol/ext-apps";
+
+// Detect whether we're running inside an MCP host.
+// Choose a detection method that fits the app:
+// - Origin check: window.location.origin !== 'https://myhost.com'
+// - Null origin (sandboxed iframe): window.location.origin === 'null'
+// - Query param: new URL(location.href).searchParams.has('mcp')
+const isMcpApp = window.location.origin === "null";
+
+async function getParameters(): Promise> {
+ if (isMcpApp) {
+ // Running as MCP App — get params from tool lifecycle
+ const app = new App({ name: "My App", version: "1.0.0" });
+
+ // Register handlers BEFORE connect()
+ const params = await new Promise>((resolve) => {
+ app.ontoolresult = (result) => resolve(result.structuredContent ?? {});
+ });
+
+ await app.connect(new PostMessageTransport());
+ return params;
+ } else {
+ // Running as standalone web app — get params from URL
+ return Object.fromEntries(new URL(location.href).searchParams);
+ }
+}
+
+async function main() {
+ const params = await getParameters();
+ renderApp(params); // Same rendering logic for both modes
+}
+
+main().catch(console.error);
+```
+
+### URL Parameters (Hybrid)
+
+```typescript
+// Before (standalone only):
+const query = new URL(location.href).searchParams.get("q");
+renderApp(query);
+
+// After (hybrid):
+async function getQuery(): Promise {
+ if (isMcpApp) {
+ const app = new App({ name: "My App", version: "1.0.0" });
+ return new Promise((resolve) => {
+ app.ontoolinput = (params) => resolve(params.arguments?.q ?? "");
+ app.connect(new PostMessageTransport());
+ });
+ }
+ return new URL(location.href).searchParams.get("q") ?? "";
+}
+
+const query = await getQuery();
+renderApp(query); // Unchanged rendering logic
+```
+
+### API Calls (Hybrid)
+
+```typescript
+// Before (standalone only):
+const data = await fetch("/api/data").then(r => r.json());
+
+// After (hybrid):
+async function fetchData(): Promise {
+ if (isMcpApp) {
+ const result = await app.callServerTool("fetch-data", {});
+ return result.structuredContent;
+ }
+ return fetch("/api/data").then(r => r.json());
+}
+```
+
+Or keep direct API calls in both modes with CSP `connectDomains`:
+
+```typescript
+// API calls can stay unchanged if the API is external and the CSP declares the domain
+// Declare connectDomains: ["api.example.com"] in the resource registration
+```
+
+### localStorage / sessionStorage (Hybrid)
+
+```typescript
+// Before (standalone only):
+const saved = localStorage.getItem("settings");
+
+// After (hybrid) — localStorage isn't available in sandboxed iframes:
+function getSettings(): any {
+ if (isMcpApp) {
+ // Will be provided via tool result
+ return null; // or a default
+ }
+ return JSON.parse(localStorage.getItem("settings") ?? "null");
+}
+```
+
+### Complete Hybrid Example
+
+```typescript
+import { App, PostMessageTransport, applyDocumentTheme, applyHostStyleVariables, applyHostFonts } from "@modelcontextprotocol/ext-apps";
+
+const isMcpApp = window.location.origin === "null";
+
+async function initMcpApp(): Promise> {
+ const app = new App({ name: "My App", version: "1.0.0" });
+
+ // Register ALL handlers BEFORE connect()
+ const params = await new Promise>((resolve) => {
+ app.ontoolinput = (input) => resolve(input.arguments ?? {});
+ });
+
+ app.onhostcontextchanged = (ctx) => {
+ if (ctx.theme) applyDocumentTheme(ctx.theme);
+ if (ctx.styles?.variables) applyHostStyleVariables(ctx.styles.variables);
+ if (ctx.styles?.css?.fonts) applyHostFonts(ctx.styles.css.fonts);
+ if (ctx.safeAreaInsets) {
+ const { top, right, bottom, left } = ctx.safeAreaInsets;
+ document.body.style.padding = `${top}px ${right}px ${bottom}px ${left}px`;
+ }
+ };
+
+ app.onteardown = async () => {
+ return {};
+ };
+
+ await app.connect(new PostMessageTransport());
+ return params;
+}
+
+async function initStandaloneApp(): Promise> {
+ return Object.fromEntries(new URL(location.href).searchParams);
+}
+
+async function main() {
+ const params = isMcpApp ? await initMcpApp() : await initStandaloneApp();
+ renderApp(params); // Same rendering logic — no fork needed
+}
+
+main().catch(console.error);
+```
+
+## Step 6: Add Host Styling Integration (MCP Mode Only)
+
+When running as an MCP App, integrate with host styling for theme consistency. Use CSS variable fallbacks so the app looks correct in both modes.
+
+**Vanilla JS** — use helper functions:
+```typescript
+import { applyDocumentTheme, applyHostStyleVariables, applyHostFonts } from "@modelcontextprotocol/ext-apps";
+
+app.onhostcontextchanged = (ctx) => {
+ if (ctx.theme) applyDocumentTheme(ctx.theme);
+ if (ctx.styles?.variables) applyHostStyleVariables(ctx.styles.variables);
+ if (ctx.styles?.css?.fonts) applyHostFonts(ctx.styles.css.fonts);
+};
+```
+
+**React** — use hooks:
+```typescript
+import { useApp, useHostStyles } from "@modelcontextprotocol/ext-apps/react";
+
+const { app } = useApp({ appInfo, capabilities, onAppCreated });
+useHostStyles(app);
+```
+
+**Using variables in CSS** — use `var()` with fallbacks so standalone mode still looks right:
+
+```css
+.container {
+ background: var(--color-background-secondary, #f5f5f5);
+ color: var(--color-text-primary, #333);
+ font-family: var(--font-sans, system-ui);
+ border-radius: var(--border-radius-md, 8px);
+}
+```
+
+Key variable groups: `--color-background-*`, `--color-text-*`, `--color-border-*`, `--font-sans`, `--font-mono`, `--font-text-*-size`, `--font-heading-*-size`, `--border-radius-*`. See `src/spec.types.ts` for the full list.
+
+## Optional Enhancements
+
+### App-Only Helper Tools
+
+For data the UI needs to poll or fetch that the model doesn't need to call directly:
+
+```typescript
+registerAppTool(server, "refresh-data", {
+ description: "Fetches latest data for the UI",
+ _meta: { ui: { resourceUri, visibility: ["app"] } },
+}, async () => {
+ const data = await getLatestData();
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
+});
+```
+
+The UI calls these via `app.callServerTool("refresh-data", {})`.
+
+### Streaming Partial Input
+
+For large tool inputs, use `ontoolinputpartial` to show progress during LLM generation:
+
+```typescript
+app.ontoolinputpartial = (params) => {
+ const args = params.arguments; // Healed partial JSON - always valid
+ renderPreview(args);
+};
+
+app.ontoolinput = (params) => {
+ renderFull(params.arguments);
+};
+```
+
+### Fullscreen Mode
+
+```typescript
+app.onhostcontextchanged = (ctx) => {
+ if (ctx.availableDisplayModes?.includes("fullscreen")) {
+ fullscreenBtn.style.display = "block";
+ }
+ if (ctx.displayMode) {
+ container.classList.toggle("fullscreen", ctx.displayMode === "fullscreen");
+ }
+};
+
+async function toggleFullscreen() {
+ const newMode = currentMode === "fullscreen" ? "inline" : "fullscreen";
+ const result = await app.requestDisplayMode({ mode: newMode });
+ currentMode = result.mode;
+}
+```
+
+### Text Fallback
+
+Always provide a `content` array for non-UI hosts:
+
+```typescript
+return {
+ content: [{ type: "text", text: "Fallback description of the result" }],
+ structuredContent: { /* data for the UI */ },
+};
+```
+
+## Common Mistakes to Avoid
+
+1. **Forgetting CSP declarations for external origins** — fails silently in the sandboxed iframe
+2. **Using `localStorage` / `sessionStorage` in MCP mode** — not available in sandboxed iframe; use fallbacks or pass via `structuredContent`
+3. **Missing `vite-plugin-singlefile`** — external assets won't load in the iframe
+4. **Registering handlers after `connect()`** — register ALL handlers BEFORE calling `app.connect()`
+5. **Hardcoding styles without fallbacks** — use host CSS variables with `var(..., fallback)` so both modes look correct
+6. **Not handling safe area insets** — always apply `ctx.safeAreaInsets` in `onhostcontextchanged`
+7. **Forgetting text `content` fallback** — always provide `content` array for non-UI hosts
+8. **Forgetting resource registration** — the tool references a `resourceUri` that must have a matching resource
+9. **Replacing standalone logic instead of branching** — keep the original data sources intact; add the MCP path alongside them
+
+## Testing
+
+### Using basic-host
+
+Test the MCP App mode with the basic-host example:
+
+```bash
+# Terminal 1: Build and run your server
+npm run build && npm run serve
+
+# Terminal 2: Run basic-host (from cloned repo)
+cd /tmp/mcp-ext-apps/examples/basic-host
+npm install
+SERVERS='["http://localhost:3001/mcp"]' npm run start
+# Open http://localhost:8080
+```
+
+Configure `SERVERS` with a JSON array of your server URLs (default: `http://localhost:3001/mcp`).
+
+### Verify
+
+1. **MCP mode**: App loads in basic-host without console errors
+2. `ontoolinput` handler fires with tool arguments
+3. `ontoolresult` handler fires with tool result
+4. Host styling (theme, fonts, colors) applies correctly
+5. External resources load (if CSP domains are configured)
+6. **Standalone mode**: App still works when opened directly in a browser