AgentSkillsCN

mcp-apps

构建具备交互式界面的 MCP 应用,这些界面可在支持 MCP 的主机中进行渲染(如 Claude、ChatGPT、VS Code、Goose)。当用户提出“创建 MCP 应用”“为 MCP 工具添加界面”“构建交互式 MCP 视图”“搭建 MCP 应用脚手架”,或在开发需要丰富界面的工具时(例如仪表板、表单、可视化图表或多步骤工作流),均可使用此技能。本技能涵盖 @modelcontextprotocol/ext-apps SDK、工具与资源模式、主机样式定制、流式输入以及框架集成(React、Vanilla JS、Vue、Svelte)。

SKILL.md
--- frontmatter
name: mcp-apps
description: Build MCP Apps with interactive UIs that render inside MCP-enabled hosts (Claude, ChatGPT, VS Code, Goose). Use when asked to "create an MCP App", "add UI to an MCP tool", "build interactive MCP view", "scaffold MCP App", or when implementing tools that need rich interfaces like dashboards, forms, visualizations, or multi-step workflows. Covers the @modelcontextprotocol/ext-apps SDK, tool+resource pattern, host styling, streaming input, and framework integration (React, Vanilla JS, Vue, Svelte).

MCP Apps

Build interactive UIs that run inside MCP-enabled hosts. An MCP App combines an MCP tool with an HTML resource.

Core Concept

Every MCP App requires two linked parts:

code
Host calls tool → Server returns result → Host renders resource UI → UI receives result
  1. Tool - Called by LLM/host, returns data
  2. Resource - Serves bundled HTML UI (via ui:// scheme)
  3. Link - Tool's _meta.ui.resourceUri references the resource

Get Reference Code

Clone the SDK for working examples:

bash
git clone --branch "v$(npm view @modelcontextprotocol/ext-apps version)" --depth 1 https://github.com/modelcontextprotocol/ext-apps.git /tmp/mcp-ext-apps

Templates (/tmp/mcp-ext-apps/examples/)

TemplateUse For
basic-server-react/React apps with useApp hook
basic-server-vanillajs/Simple apps, no framework
basic-server-vue/Vue apps
basic-server-svelte/Svelte apps

API Reference (/tmp/mcp-ext-apps/src/)

FileContents
app.tsApp class, handlers, lifecycle
server/index.tsregisterAppTool, registerAppResource
react/useApp.tsxReact hook
styles.tsHost styling helpers

Implementation Checklist

Server Side

  1. Install dependencies:

    bash
    npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk zod
    npm install -D tsx vite vite-plugin-singlefile
    
  2. Register tool with UI metadata:

    typescript
    import { registerAppTool, registerAppResource } from "@modelcontextprotocol/ext-apps/server";
    
    registerAppTool(server, {
      name: "my_tool",
      description: "Tool description",
      inputSchema: zodToJsonSchema(MyInputSchema),
      _meta: {
        ui: {
          resourceUri: "ui://my-app/main",
          visibility: ["model", "app"]  // or ["app"] for UI-only
        }
      },
      handler: async (args) => ({
        content: [{ type: "text", text: "Fallback for non-UI hosts" }],
        data: args  // Passed to UI via ontoolresult
      })
    });
    
    registerAppResource(server, {
      uri: "ui://my-app/main",
      name: "My App UI",
      mimeType: "text/html",
      async handler() {
        return fs.readFileSync("./dist/mcp-app.html", "utf-8");
      }
    });
    

Client Side (UI)

Register handlers BEFORE connect():

typescript
import { App } from "@modelcontextprotocol/ext-apps";

const app = new App({ name: "My App", version: "1.0.0" });

// Tool input (args before execution)
app.ontoolinput = (params) => {
  console.log("Input:", params.arguments);
};

// Tool result (after execution)
app.ontoolresult = (result) => {
  renderUI(result.data);
};

// Host context changes (theme, safe area)
app.onhostcontextchanged = (ctx) => {
  if (ctx.safeAreaInsets) {
    const { top, right, bottom, left } = ctx.safeAreaInsets;
    document.body.style.padding = `${top}px ${right}px ${bottom}px ${left}px`;
  }
};

// Cleanup
app.onteardown = async () => ({ state: {} });

await app.connect();

Build Config

Use vite-plugin-singlefile to bundle into one HTML file:

typescript
// vite.config.ts
import { defineConfig } from "vite";
import { viteSingleFile } from "vite-plugin-singlefile";

export default defineConfig({
  plugins: [viteSingleFile()],
  build: {
    outDir: "dist",
    rollupOptions: {
      input: "mcp-app.html"
    }
  }
});

Key Patterns

Host Styling

Use CSS variables from host context:

typescript
import { applyDocumentTheme, applyHostStyleVariables } from "@modelcontextprotocol/ext-apps";

app.onhostcontextchanged = (ctx) => {
  if (ctx.theme) applyDocumentTheme(ctx.theme);
  if (ctx.styles?.variables) applyHostStyleVariables(ctx.styles.variables);
};
css
.container {
  background: var(--color-background-secondary);
  color: var(--color-text-primary);
  font-family: var(--font-sans);
}

Streaming Partial Input

Show progress during LLM generation:

typescript
app.ontoolinputpartial = (params) => {
  // Healed partial JSON - always valid
  preview.textContent = JSON.stringify(params.arguments, null, 2);
};

app.ontoolinput = (params) => {
  // Final complete input
  render(params.arguments);
};

Call Server Tools from UI

typescript
const response = await app.callServerTool({
  name: "fetch_details",
  arguments: { id: "123" }
});

Update Model Context

Keep the model informed of UI state:

typescript
await app.updateModelContext({
  content: [{ type: "text", text: "User selected option B" }]
});

Tool Visibility

typescript
// Both model and app can call (default)
visibility: ["model", "app"]

// UI-only (hidden from model) - for refresh buttons, form submissions
visibility: ["app"]

// Model-only
visibility: ["model"]

Testing

Use the basic-host example:

bash
# Terminal 1: Your server
npm run build && npm run serve

# Terminal 2: Test host
cd /tmp/mcp-ext-apps/examples/basic-host
npm install
SERVERS='["http://localhost:3001/mcp"]' npm run start
# Open http://localhost:8080

Common Mistakes

  1. Handlers after connect() - Register ALL handlers BEFORE app.connect()
  2. Missing vite-plugin-singlefile - Required for bundling
  3. No resourceUri link - Tool must have _meta.ui.resourceUri
  4. Ignoring safe area insets - Always handle ctx.safeAreaInsets
  5. Hardcoded styles - Use host CSS variables