Update README hero gif to Excalidraw, add new agent skills
This commit is contained in:
parent
8543d8d5dc
commit
2af02baf46
5 changed files with 881 additions and 383 deletions
398
README.md
398
README.md
|
|
@ -28,8 +28,8 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="media/claude-colorpicker-apps.gif" alt="MCP Apps demo" width="600">
|
<img src="media/excalidraw.gif" alt="MCP Apps demo" width="600">
|
||||||
<br><em>Color picker built with MCP Apps, running in Claude</em>
|
<br><em>Excalidraw built with MCP Apps, running in Claude</em>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
@ -46,10 +46,14 @@
|
||||||
|
|
||||||
## Build with Agent Skills
|
## Build with Agent Skills
|
||||||
|
|
||||||
The fastest way to build an MCP App is to let your AI coding agent do it. This repo ships two [Agent Skills](https://agentskills.io/) — install them once, then just ask:
|
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:
|
||||||
|
|
||||||
- **"Create an MCP App"** — scaffolds a new MCP App with an interactive UI from scratch
|
| Skill | What it does | Try it |
|
||||||
- **"Migrate from OpenAI Apps SDK"** — converts an existing OpenAI App to use MCP Apps
|
| ----- | ------------ | ------ |
|
||||||
|
| [`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) | Converts an existing web app into an MCP App | _"Convert my web app to an MCP App"_ |
|
||||||
|
|
||||||
### Install the Skills
|
### Install the Skills
|
||||||
|
|
||||||
|
|
@ -62,7 +66,7 @@ The fastest way to build an MCP App is to let your AI coding agent do it. This r
|
||||||
|
|
||||||
**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.
|
**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` and `migrate-oai-app` in the list. Then just ask it to create or migrate an app and it will guide you through the rest.
|
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.
|
||||||
|
|
||||||
## Why MCP Apps?
|
## Why MCP Apps?
|
||||||
|
|
||||||
|
|
@ -144,404 +148,36 @@ Then open http://localhost:8080/.
|
||||||
|
|
||||||
#### With MCP Clients
|
#### 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:
|
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:
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>MCP client configuration for all examples (using stdio)</summary>
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"basic-react": {
|
"<name>": {
|
||||||
"command": "npx",
|
"command": "npx",
|
||||||
"args": [
|
"args": ["-y", "@modelcontextprotocol/server-<name>", "--stdio"]
|
||||||
"-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"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
</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.
|
||||||
|
|
||||||
> [!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.
|
|
||||||
|
|
||||||
#### Local Development
|
#### Local Development
|
||||||
|
|
||||||
To test local modifications with MCP clients, first clone and install the repository:
|
To test local modifications with an MCP client, clone the repo, install, then point your client at a local build:
|
||||||
|
|
||||||
```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>
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"basic-react": {
|
"<name>": {
|
||||||
"command": "bash",
|
"command": "bash",
|
||||||
"args": [
|
"args": ["-c", "cd ~/code/ext-apps/examples/<name>-server && npm run build >&2 && node dist/index.js --stdio"]
|
||||||
"-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"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
This configuration rebuilds each server on launch, ensuring your local changes are picked up.
|
|
||||||
|
|
||||||
## Specification
|
## Specification
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
|
||||||
|
|
@ -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.
|
[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
|
- [**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
|
- [**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
|
## Install the Skills
|
||||||
|
|
||||||
|
|
@ -51,7 +53,7 @@ Then copy the skills from `plugins/mcp-apps/skills/` to your agent's skills dire
|
||||||
|
|
||||||
## Verify Installation
|
## 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
|
## 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
|
- "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
|
- "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.
|
The agent will guide you through the process, asking clarifying questions as needed.
|
||||||
|
|
||||||
|
|
|
||||||
BIN
media/excalidraw.gif
Normal file
BIN
media/excalidraw.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.9 MiB |
389
plugins/mcp-apps/skills/add-app-to-server/SKILL.md
Normal file
389
plugins/mcp-apps/skills/add-app-to-server/SKILL.md
Normal 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
|
||||||
469
plugins/mcp-apps/skills/convert-web-app/SKILL.md
Normal file
469
plugins/mcp-apps/skills/convert-web-app/SKILL.md
Normal file
|
|
@ -0,0 +1,469 @@
|
||||||
|
---
|
||||||
|
name: convert-web-app
|
||||||
|
description: This skill should be used when the user asks to "convert my web app to an MCP App", "turn my website into an MCP App", "make my web page an MCP tool", "wrap my existing UI as an MCP App", "convert iframe embed to MCP App", "turn my SPA into an MCP App", or needs to convert an existing web application into an MCP App with server-side tool and resource registration. Provides guidance for analyzing existing web apps and creating MCP server + App wrappers.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Convert Web App to MCP App
|
||||||
|
|
||||||
|
Turn an existing web application into an MCP App that renders inline in MCP-enabled hosts like Claude Desktop.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
The existing web app becomes the View (HTML resource) in an MCP App. A new MCP server is created to:
|
||||||
|
|
||||||
|
1. Register a tool that the LLM calls to display the app
|
||||||
|
2. Register the bundled HTML as a resource
|
||||||
|
3. Pass data to the app via `structuredContent` instead of URL params / API calls
|
||||||
|
|
||||||
|
```
|
||||||
|
Host calls tool → Server returns result → Host renders your web app → App receives data via MCP lifecycle
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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` |
|
||||||
|
|
||||||
|
### Conversion 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 the conversion.
|
||||||
|
|
||||||
|
### 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?
|
||||||
|
|
||||||
|
Present findings to the user and confirm the conversion approach.
|
||||||
|
|
||||||
|
### Data Source Mapping
|
||||||
|
|
||||||
|
| Data source in web app | 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 — move to `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 conversion 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. The web app has no existing server, so this is built from scratch.
|
||||||
|
|
||||||
|
### 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 UI must be bundled into a single HTML file using `vite-plugin-singlefile`. Without it, external assets won't load in the sandboxed iframe.
|
||||||
|
|
||||||
|
### Vite Configuration
|
||||||
|
|
||||||
|
Create or update `vite.config.ts`:
|
||||||
|
|
||||||
|
```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 the entry point for the MCP App build:
|
||||||
|
|
||||||
|
```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>
|
||||||
|
```
|
||||||
|
|
||||||
|
If the web app already uses Vite, add `vite-plugin-singlefile` and create a separate entry point. If it uses another bundler, add a Vite config alongside for the MCP App build.
|
||||||
|
|
||||||
|
### 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)
|
||||||
|
|
||||||
|
## Step 5: Replace Data Sources with MCP App Lifecycle
|
||||||
|
|
||||||
|
This is the core conversion step. Replace the web app's data sources with MCP App lifecycle handlers.
|
||||||
|
|
||||||
|
### URL Parameters → `ontoolinput`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Before:
|
||||||
|
const query = new URL(location.href).searchParams.get("q");
|
||||||
|
renderApp(query);
|
||||||
|
|
||||||
|
// After:
|
||||||
|
app.ontoolinput = (params) => {
|
||||||
|
const query = params.arguments?.q;
|
||||||
|
renderApp(query);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Fetch → `app.callServerTool()`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Before:
|
||||||
|
const data = await fetch("/api/data").then(r => r.json());
|
||||||
|
|
||||||
|
// After:
|
||||||
|
const result = await app.callServerTool("fetch-data", {});
|
||||||
|
const data = result.structuredContent;
|
||||||
|
```
|
||||||
|
|
||||||
|
Or keep direct API calls with CSP `connectDomains`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// API calls can stay if the API is external and the CSP declares the domain
|
||||||
|
// Declare connectDomains: ["api.example.com"] in the resource registration
|
||||||
|
```
|
||||||
|
|
||||||
|
### Props / Component Inputs → `ontoolinput`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Before (e.g., passed via parent frame or global):
|
||||||
|
const config = window.__APP_CONFIG__;
|
||||||
|
|
||||||
|
// After:
|
||||||
|
app.ontoolinput = (params) => {
|
||||||
|
const config = params.arguments;
|
||||||
|
initApp(config);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### localStorage / sessionStorage → `structuredContent`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Before:
|
||||||
|
const saved = localStorage.getItem("settings");
|
||||||
|
|
||||||
|
// After — pass via tool result:
|
||||||
|
app.ontoolresult = (result) => {
|
||||||
|
const settings = result.structuredContent?.settings;
|
||||||
|
applySettings(settings);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Complete Conversion Example
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { App, PostMessageTransport, applyDocumentTheme, applyHostStyleVariables, applyHostFonts } from "@modelcontextprotocol/ext-apps";
|
||||||
|
|
||||||
|
const app = new App({ name: "My App", version: "1.0.0" });
|
||||||
|
|
||||||
|
// Register ALL handlers BEFORE connect()
|
||||||
|
app.ontoolinput = (params) => {
|
||||||
|
// Replace URL params / props with tool arguments
|
||||||
|
renderApp(params.arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
app.ontoolresult = (result) => {
|
||||||
|
// Replace API responses with structured content
|
||||||
|
updateApp(result.structuredContent);
|
||||||
|
};
|
||||||
|
|
||||||
|
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());
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 6: Add Host Styling Integration
|
||||||
|
|
||||||
|
Replace hardcoded styles with host CSS variables for theme consistency.
|
||||||
|
|
||||||
|
**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** — after applying, use `var()` with fallbacks for standalone testing:
|
||||||
|
|
||||||
|
```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`** — not available in sandboxed iframe; move to `structuredContent` or server-side state
|
||||||
|
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** — use host CSS variables for theme integration
|
||||||
|
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
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Using basic-host
|
||||||
|
|
||||||
|
Test the converted app 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. App loads 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)
|
||||||
Loading…
Reference in a new issue