Merge pull request #478 from modelcontextprotocol/localden/readme-ext

Lead README with agent skills for faster onboarding
This commit is contained in:
Den Delimarsky 2026-02-21 13:03:03 -08:00 • committed by GitHub
commit 221e372a28
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 1023 additions and 409 deletions

510
README.md
View file

@ -28,49 +28,91 @@
</p>
<p align="center">
<img src="media/claude-colorpicker-apps.gif" alt="MCP Apps demo" width="600">
<br><em>Color picker built with MCP Apps, running in Claude</em>
<img src="media/excalidraw.gif" alt="MCP Apps demo" width="600">
<br><em>Excalidraw built with MCP Apps, running in Claude</em>
</p>
## 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
<div align="center">
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"_ |
</div>
### Install the Skills
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.
**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
<p align="center">
<a href="https://claude.ai"><img src="https://img.shields.io/badge/Claude-web-d97706?logo=claude&logoColor=white" alt="Claude"></a>
<a href="https://claude.ai/download"><img src="https://img.shields.io/badge/Claude_Desktop-app-d97706?logo=claude&logoColor=white" alt="Claude Desktop"></a>
<a href="https://code.visualstudio.com/insiders"><img src="https://img.shields.io/badge/VS_Code-Insiders-007ACC?logo=visualstudiocode&logoColor=white" alt="VS Code Insiders"></a>
<a href="https://block.github.io/goose/"><img src="https://img.shields.io/badge/Goose-Block-000000?logo=goose&logoColor=white" alt="Goose"></a>
<a href="https://postman.com"><img src="https://img.shields.io/badge/Postman-client-FF6C37?logo=postman&logoColor=white" alt="Postman"></a>
<a href="https://www.mcpjam.com/"><img src="https://img.shields.io/badge/MCPJam-client-8B5CF6" alt="MCPJam"></a>
</p>
> [!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.
<!-- prettier-ignore-start -->
| | | |
@ -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:
<details>
<summary>MCP client configuration for all examples (using stdio)</summary>
Every Node.js example is published as `@modelcontextprotocol/server-<name>`. To
add one to an MCP client that supports stdio (Claude Desktop, VS Code, etc.),
use this pattern:
```json
{
"mcpServers": {
"basic-react": {
"<name>": {
"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-<name>", "--stdio"]
}
}
}
```
</details>
> [!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:
<details>
<summary>MCP client configuration for local development (all examples)</summary>
To test local modifications with an MCP client, clone the repo, install, then
point your client at a local build:
```json
{
"mcpServers": {
"basic-react": {
"<name>": {
"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/<name>-server && npm run build >&2 && node dist/index.js --stdio"
]
}
}
}
```
</details>
## Specification
This configuration rebuilds each server on launch, ensuring your local changes are picked up.
<div align="center">
| 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) |
</div>
## 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.

View file

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

BIN
media/excalidraw.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 MiB

View file

@ -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
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MCP App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./src/mcp-app.ts"></script>
</body>
</html>
```
### 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

View file

@ -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
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MCP App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./src/main.ts"></script>
</body>
</html>
```
### 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<Record<string, string>> {
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<Record<string, string>>((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<string> {
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<any> {
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<Record<string, any>> {
const app = new App({ name: "My App", version: "1.0.0" });
// Register ALL handlers BEFORE connect()
const params = await new Promise<Record<string, any>>((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<Record<string, any>> {
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