--- title: Building MCP Apps description: Create interactive UI applications that render inside goose Desktop --- import { PanelLeft } from 'lucide-react'; # Building MCP Apps for goose MCP Apps let MCP servers return interactive UIs that render directly inside the goose chat interface, rather than responding with text alone. This allows users to express intent through interaction, which is useful for workflows that require input, iteration, or visual feedback. :::warning Experimental MCP Apps support in goose is experimental and based on a draft specification. The implementation is minimal and may change, and does not yet support advanced capabilities or persistent app windows. ::: In this tutorial, you will build an MCP App using JavaScript and Node.js. The app includes an interactive counter, stays in sync with the host theme, and sends messages back to the chat, showing how user intent flows from UI to agent. :::info Prerequisites - Node.js 18+ installed - goose Desktop 1.19.1+ installed ::: --- ## Step 1: Initialize Your Project Create a new directory and initialize a Node.js project: ```bash mkdir mcp-app-demo cd mcp-app-demo npm init -y ``` Install the MCP SDK: ```bash npm install @modelcontextprotocol/sdk ``` Update your `package.json` to use ES modules by adding `"type": "module"`: ```json5 { "name": "mcp-app-demo", "version": "1.0.0", // highlight-next-line "type": "module", "main": "server.js", "scripts": { "start": "node server.js" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.0.0" } } ``` --- ## Step 2: Create the MCP Server Create `server.js` - this is the MCP server that loads and serves your HTML:
server.js ```javascript #!/usr/bin/env node import { readFileSync } from "fs"; import { fileURLToPath } from "url"; import { dirname, join } from "path"; import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; // Load HTML from file const __dirname = dirname(fileURLToPath(import.meta.url)); const APP_HTML = readFileSync(join(__dirname, "index.html"), "utf-8"); // Create the MCP server const server = new Server( { name: "mcp-app-demo", version: "1.0.0", }, { capabilities: { tools: {}, resources: {}, }, } ); // List available tools server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "show_demo_app", description: "Shows an interactive demo MCP App UI in the chat", inputSchema: { type: "object", properties: {}, required: [], }, }, ], }; }); // Handle tool calls server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name } = request.params; if (name === "show_demo_app") { return { content: [ { type: "text", text: "The demo app is now displayed!", }, ], // This metadata tells goose to render the MCP App _meta: { ui: { resourceUri: "ui://mcp-app-demo/main", }, }, }; } throw new Error(`Unknown tool: ${name}`); }); // List available resources server.setRequestHandler(ListResourcesRequestSchema, async () => { return { resources: [ { uri: "ui://mcp-app-demo/main", name: "MCP App Demo", description: "An interactive demo", mimeType: "text/html;profile=mcp-app", }, ], }; }); // Read resource content - returns the HTML server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const { uri } = request.params; if (uri === "ui://mcp-app-demo/main") { return { contents: [ { uri: "ui://mcp-app-demo/main", mimeType: "text/html;profile=mcp-app", text: APP_HTML, _meta: { ui: { csp: { connectDomains: [], resourceDomains: [], frameDomains: [], baseUriDomains: [], }, prefersBorder: true, }, }, }, ], }; } throw new Error(`Resource not found: ${uri}`); }); // Start the server async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("MCP App Demo server running on stdio"); } main().catch(console.error); ```
--- ## Step 3: Create the App HTML Create `index.html` - this is your interactive UI:
index.html ```html MCP App Demo

๐ŸŽฎ MCP App Demo

An interactive UI running inside goose

0
Counter Value

๐Ÿ’ฌ Send a message to goose

How this works:

This UI is served as an MCP resource with the ui:// scheme. It communicates with goose via JSON-RPC messages through the sandbox bridge.

โ€ข Counter uses local state
โ€ข "Send" calls ui/message to append text to chat
โ€ข Theme syncs with goose's theme setting
```
--- ## Step 4: Add to goose Desktop 1. Click the button in the top-left to open the sidebar 2. Click `Extensions` 3. Click `Add custom extension` 4. Fill in the details: - **Type**: `Standard IO` - **ID**: `mcp-app-demo` - **Name**: `MCP App Demo` - **Command**: `node /full/path/to/mcp-app-demo/server.js` 5. Click `Add` For more options, see [Adding Extensions](/docs/getting-started/using-extensions#adding-extensions). --- ## Step 5: Test Your App 1. Restart goose to load the new extension 2. Prompt goose: "Show me the demo app" 3. goose will call the `show_demo_app` tool 4. Your interactive app will render in the chat! Try: - Clicking the counter buttons - Typing a message and clicking "Send" - Switching goose between light/dark mode --- ## How It Works ``` โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Your MCP App โ”‚ HTML/JS in sandboxed iframe โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ postMessage โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ goose Desktop โ”‚ Renders UI, routes messages โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ MCP Protocol โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Your MCP Server โ”‚ Serves HTML via resources โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` Your server returns a `ui://` resource URI, goose fetches the HTML and renders it in an iframe. The app communicates back via `postMessage`โ€”requesting theme info, sending messages to chat, or resizing itself. MCP Apps run in a sandboxed iframe with strict Content Security Policy restrictions. ### Content Security Policy Configuration By default, apps can only load resources from their own origin. If your app needs to interact with external domainsโ€”such as loading resources from a CDN, making API calls, or embedding mapsโ€”you can configure which domains are allowed through the `csp` object in the resource's `_meta.ui` section. ```javascript _meta: { ui: { csp: { connectDomains: [], // Domains for fetch/XHR requests resourceDomains: [], // Domains for scripts, styles, images, fonts, media frameDomains: [], // Origins allowed for nested iframes baseUriDomains: [], // Additional allowed base URIs }, }, } ``` | Option | CSP Directive | Purpose | Default | |--------|---------------|---------|---------| | `connectDomains` | `connect-src` | Domains your app can make network requests to | Same-origin only | | `resourceDomains` | `script-src`, `style-src`, `img-src`, `font-src`, `media-src` | Domains for loading external resources | Same-origin only | | `frameDomains` | `frame-src` | Origins allowed for nested `