docs: add MCP Apps tutorial and documentation updates (#6384)

This commit is contained in:
Rizel Scarlett
2026-01-07 18:06:31 -05:00
committed by GitHub
parent 42271650eb
commit e49079edc0
6 changed files with 577 additions and 40 deletions
@@ -668,4 +668,8 @@ goose session --with-streamable-http-extension "https://example.com/streamable"
goose extensions are implemented with MCP, a standard protocol that allows AI models and agents to securely connect with local or remote resources. Learn how to build your own [extension as an MCP server](https://modelcontextprotocol.io/quickstart/server).
**Tutorials:**
- [Building Custom Extensions](/docs/tutorials/custom-extensions) - Create a Python-based MCP extension
- [Building MCP Apps](/docs/tutorials/building-mcp-apps) - Create interactive UI apps
[extensions-directory]: /extensions
@@ -1,5 +1,5 @@
{
"label": "Rich Interactive Chat",
"label": "MCP Apps and MCP-UI",
"position": 55,
"link": {
"type": "doc",
@@ -1,14 +1,14 @@
---
title: Rich Interactive Chat
title: Rich Interactive Chat with MCP Apps and MCP-UI
hide_title: true
description: Transform text-based responses into graphical components and interactive elements
description: Build interactive UI applications that render inside goose Desktop using MCP Apps and MCP-UI
---
import Card from '@site/src/components/Card';
import styles from '@site/src/components/Card/styles.module.css';
import VideoCarousel from '@site/src/components/VideoCarousel';
<h1 className={styles.pageTitle}>Rich Interactive Chat with MCP-UI</h1>
<h1 className={styles.pageTitle}>Rich Interactive Chat with MCP Apps and MCP-UI</h1>
<p className={styles.pageDescription}>
goose Desktop supports extensions that transform text-only responses into graphical, interactive experiences. Instead of reading through lists and descriptions, you can click, explore, and interact with UI components directly in your conversations.
</p>
@@ -27,6 +27,11 @@ import VideoCarousel from '@site/src/components/VideoCarousel';
<div className={styles.categorySection}>
<h2 className={styles.categoryTitle}>📚 Documentation & Guides</h2>
<div className={styles.cardGrid}>
<Card
title="Building MCP Apps"
description="Step-by-step tutorial to create interactive UI applications that render inside goose Desktop."
link="/docs/tutorials/building-mcp-apps"
/>
<Card
title="MCP-UI Extensions"
description="goose transforms text-based responses into engaging graphical and interactive user experiences."
@@ -43,6 +48,11 @@ import VideoCarousel from '@site/src/components/VideoCarousel';
<div className={styles.categorySection}>
<h2 className={styles.categoryTitle}>📝 Featured Blog Posts</h2>
<div className={styles.cardGrid}>
<Card
title="goose Lands MCP Apps"
description="goose ships early support for the draft MCP Apps specification, aligning with the emerging standard for interactive UIs."
link="/blog/2026/01/06/mcp-apps"
/>
<Card
title="MCP UI: Bringing the Browser into the Agent"
description="MCP-UI servers return content that goose Desktop renders as rich, embeddable UI."
@@ -93,4 +103,4 @@ import VideoCarousel from '@site/src/components/VideoCarousel';
}
]}
/>
</div>
</div>
@@ -1,8 +1,8 @@
---
sidebar_position: 1
title: MCP-UI Extensions
sidebar_label: MCP-UI Extensions
description: Learn how goose can render graphical and interactive UI components from MCP-UI-enabled extensions
title: Using MCP Apps and MCP-UI
sidebar_label: Using MCP Apps and MCP-UI
description: Learn how goose renders interactive UI components from MCP Apps and MCP-UI extensions
---
import Tabs from '@theme/Tabs';
@@ -11,7 +11,13 @@ import GooseDesktopInstaller from '@site/src/components/GooseDesktopInstaller';
import CLIExtensionInstructions from '@site/src/components/CLIExtensionInstructions';
import { PanelLeft } from 'lucide-react';
Extensions built on MCP-UI allow goose Desktop to provide interactive and engaging user experiences. Imagine using a graphical, clickable UI instead of reading text responses and typing all your prompts:
# Using MCP Apps and MCP-UI
Extensions built with MCP Apps or MCP-UI allow goose Desktop to provide interactive and engaging user experiences. Imagine using a graphical, clickable UI instead of reading text responses and typing all your prompts. These extensions return content that goose can render as embedded UI elements for rich, dynamic, and streamlined interactions.
:::info MCP Apps is the official specification
[MCP Apps](/docs/tutorials/building-mcp-apps) is now the official MCP specification for interactive UIs. MCP-UI extensions still work in goose, but MCP Apps is the recommended path for new extensions.
:::
<div style={{ width: '100%', maxWidth: '800px', margin: '0 auto' }}>
<video
@@ -28,23 +34,16 @@ Extensions built on MCP-UI allow goose Desktop to provide interactive and engagi
</video>
</div>
<br/>
MCP-UI-enabled extensions return content that goose can render as embedded UI elements for rich, dynamic, and streamlined interactions.
## Try It Out
See how interactive responses work in goose.
### Add Enabled Extension
For this exercise, we'll add an MCP-UI-enabled extension that connects to [MCP-UI Demos](https://mcp-aharvard.netlify.app/) provided by Andrew Harvard.
See how interactive responses work in goose. For this exercise, we'll add an extension that connects to [MCP-UI Demos](https://mcp-aharvard.netlify.app/) provided by Andrew Harvard.
<Tabs groupId="interface">
<TabItem value="ui" label="goose Desktop" default>
<GooseDesktopInstaller
extensionId="richdemo"
extensionName="Rich Demo"
description="Demo MCP-UI-enabled extension"
description="Demo interactive extension"
type="http"
url="https://mcp-aharvard.netlify.app/mcp"
/>
@@ -52,7 +51,7 @@ For this exercise, we'll add an MCP-UI-enabled extension that connects to [MCP-U
<TabItem value="cli" label="goose CLI">
<CLIExtensionInstructions
name="rich_demo"
description="Demo MCP-UI-enabled extension"
description="Demo interactive extension"
type="http"
url="https://mcp-aharvard.netlify.app/mcp"
timeout={300}
@@ -60,8 +59,6 @@ For this exercise, we'll add an MCP-UI-enabled extension that connects to [MCP-U
</TabItem>
</Tabs>
### Interact in Chat
In goose Desktop, ask:
- `Help me select seats for my flight`
@@ -71,15 +68,14 @@ Instead of just text, you'll see an interactive response with:
- Real-time, clickable selection capabilities
- A booking confirmation with flight details
Ask questions to try out other demos:
Try out other demos:
- `Plan my next trip based on my mood`
- `What's the weather in Philadelphia?`
Stay tuned as more extensions build with MCP-UI!
## For Extension Developers
Want to add interactivity to your own extensions? MCP-UI extends the Model Context Protocol to allow MCP servers to return content that agents can render as UI components instead of text-only responses. Learn more:
- [MCP-UI: Bringing the Browser into the Agent](/blog/2025/08/11/mcp-ui-post-browser-world)
- [MCP-UI Documentation](https://mcpui.dev/guide/introduction)
Add interactivity to your own extensions:
- [Building MCP Apps](/docs/tutorials/building-mcp-apps) - Step-by-step tutorial (recommended)
- [MCP-UI Documentation](https://mcpui.dev/guide/introduction) - MCP-UI specification
@@ -0,0 +1,530 @@
---
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 application
:::
---
## 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:
```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: [],
},
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:
<details>
<summary>index.html</summary>
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MCP App Demo</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
padding: 20px;
min-height: 100vh;
transition: background-color 0.3s, color 0.3s;
}
body.light { background: #f5f5f7; color: #1d1d1f; }
body.dark { background: #1d1d1f; color: #f5f5f7; }
.container {
max-width: 500px;
margin: 0 auto;
padding: 24px;
border-radius: 16px;
}
body.light .container { background: #ffffff; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
body.dark .container { background: #2d2d2f; box-shadow: 0 1px 3px rgba(0,0,0,0.3); }
h1 { font-size: 24px; margin-bottom: 8px; }
.subtitle { opacity: 0.7; margin-bottom: 20px; font-size: 14px; }
.counter-section {
text-align: center;
padding: 24px;
border-radius: 12px;
margin-bottom: 20px;
}
body.light .counter-section { background: #f5f5f7; }
body.dark .counter-section { background: #1d1d1f; }
.counter-value { font-size: 64px; font-weight: bold; color: #0071e3; }
.counter-label { font-size: 14px; opacity: 0.6; margin-top: 4px; }
.button-row { display: flex; gap: 12px; justify-content: center; margin-top: 16px; }
button {
padding: 12px 24px;
font-size: 18px;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
color: white;
transition: opacity 0.2s;
}
button:hover { opacity: 0.85; }
button:active { opacity: 0.7; }
.btn-increment { background: #0071e3; }
.btn-decrement { background: #ff3b30; }
.btn-reset { background: #86868b; }
.btn-send { background: #34c759; }
.message-section { margin-top: 20px; }
.message-section h3 { font-size: 16px; margin-bottom: 12px; }
.message-input { display: flex; gap: 8px; }
input[type="text"] {
flex: 1;
padding: 12px 16px;
border-radius: 8px;
border: 2px solid transparent;
font-size: 14px;
transition: border-color 0.2s;
}
body.light input { background: #f5f5f7; color: #1d1d1f; }
body.dark input { background: #1d1d1f; color: #f5f5f7; }
input:focus { outline: none; border-color: #0071e3; }
.status {
margin-top: 16px;
padding: 12px;
border-radius: 8px;
font-size: 13px;
display: none;
}
.status.show { display: block; }
.status.success { background: rgba(52, 199, 89, 0.15); color: #34c759; }
.status.error { background: rgba(255, 59, 48, 0.15); color: #ff3b30; }
.info-section {
margin-top: 20px;
padding: 16px;
border-radius: 8px;
font-size: 12px;
opacity: 0.8;
}
body.light .info-section { background: #f5f5f7; }
body.dark .info-section { background: #1d1d1f; }
.info-section code {
background: rgba(0, 113, 227, 0.1);
padding: 2px 6px;
border-radius: 4px;
font-family: 'SF Mono', Monaco, monospace;
}
</style>
</head>
<body class="light">
<div class="container">
<h1>🎮 MCP App Demo</h1>
<p class="subtitle">An interactive UI running inside goose</p>
<div class="counter-section">
<div class="counter-value" id="counter">0</div>
<div class="counter-label">Counter Value</div>
<div class="button-row">
<button class="btn-decrement" onclick="updateCounter(-1)"></button>
<button class="btn-reset" onclick="resetCounter()">Reset</button>
<button class="btn-increment" onclick="updateCounter(1)">+</button>
</div>
</div>
<div class="message-section">
<h3>💬 Send a message to goose</h3>
<div class="message-input">
<input type="text" id="messageInput" placeholder="Type a message..." />
<button class="btn-send" onclick="sendMessage()">Send</button>
</div>
<div class="status" id="status"></div>
</div>
<div class="info-section">
<strong>How this works:</strong><br><br>
This UI is served as an MCP resource with the <code>ui://</code> scheme.
It communicates with goose via JSON-RPC messages through the sandbox bridge.
<br><br>
• Counter uses local state<br>
• "Send" calls <code>ui/message</code> to append text to chat<br>
• Theme syncs with goose's theme setting
</div>
</div>
<script>
class McpAppClient {
constructor() {
this.pendingRequests = new Map();
this.requestId = 0;
this.initialized = false;
this.hostContext = null;
window.addEventListener('message', (e) => this.handleMessage(e));
this.initialize();
}
async initialize() {
try {
const result = await this.request('ui/initialize', {});
this.hostContext = result.hostContext;
this.initialized = true;
if (this.hostContext?.theme) {
this.applyTheme(this.hostContext.theme);
}
this.notify('ui/notifications/initialized', {});
this.reportSize();
} catch (error) {
console.error('Failed to initialize MCP App:', error);
}
}
handleMessage(event) {
const data = event.data;
if (!data || typeof data !== 'object') return;
if ('id' in data && this.pendingRequests.has(data.id)) {
const { resolve, reject } = this.pendingRequests.get(data.id);
this.pendingRequests.delete(data.id);
data.error ? reject(new Error(data.error.message)) : resolve(data.result);
return;
}
if (data.method === 'ui/notifications/host-context-changed') {
if (data.params?.theme) {
this.applyTheme(data.params.theme);
}
}
}
request(method, params) {
return new Promise((resolve, reject) => {
const id = ++this.requestId;
this.pendingRequests.set(id, { resolve, reject });
window.parent.postMessage({ jsonrpc: '2.0', id, method, params }, '*');
setTimeout(() => {
if (this.pendingRequests.has(id)) {
this.pendingRequests.delete(id);
reject(new Error('Request timed out'));
}
}, 30000);
});
}
notify(method, params) {
window.parent.postMessage({ jsonrpc: '2.0', method, params }, '*');
}
applyTheme(theme) {
document.body.className = theme;
}
reportSize() {
this.notify('ui/notifications/size-changed', { height: document.body.scrollHeight });
}
async sendMessageToChat(text) {
return this.request('ui/message', { content: { type: 'text', text } });
}
}
const mcpApp = new McpAppClient();
let counter = 0;
function updateCounter(delta) {
counter += delta;
document.getElementById('counter').textContent = counter;
mcpApp.reportSize();
}
function resetCounter() {
counter = 0;
document.getElementById('counter').textContent = counter;
mcpApp.reportSize();
}
async function sendMessage() {
const input = document.getElementById('messageInput');
const message = input.value.trim();
if (!message) {
showStatus('Please enter a message', 'error');
return;
}
try {
await mcpApp.sendMessageToChat(message);
showStatus('Message sent to chat!', 'success');
input.value = '';
} catch (error) {
showStatus('Failed to send: ' + error.message, 'error');
}
}
function showStatus(message, type) {
const status = document.getElementById('status');
status.textContent = message;
status.className = 'status show ' + type;
setTimeout(() => { status.className = 'status'; }, 3000);
}
document.getElementById('messageInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendMessage();
});
</script>
</body>
</html>
```
</details>
---
## Step 4: Add to goose Desktop
1. Click the <PanelLeft className="inline" size={16} /> 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 sandboxed with CSP restrictions. See the [MCP Apps Specification](https://github.com/modelcontextprotocol/ext-apps) for details on security and the full protocol.
@@ -308,25 +308,22 @@ goose supports advanced MCP features that can enhance your extensions.
**Learn More:** See the [MCP Specification](https://modelcontextprotocol.io/specification/draft/client/sampling) for technical details.
### MCP-UI: Interactive Extensions
### MCP Apps: Interactive Extensions
**[MCP-UI Extensions](/docs/guides/interactive-chat/mcp-ui)** enable rich, interactive user interfaces instead of text-only responses, transforming static MCP servers into dynamic, engaging experiences.
**[MCP Apps](/docs/tutorials/building-mcp-apps)** enable rich, interactive user interfaces instead of text-only responses.
**Key Benefits:**
- Your MCP server can return interactive UI components alongside or instead of text
- Components render securely in isolated environments within goose Desktop
- Real-time user interactions trigger callbacks to your MCP server
- Standardized protocol ensures consistent behavior across different clients
- Return interactive UI components from your MCP server tools
- Components render securely in isolated sandboxes within goose Desktop
- Real-time user interactions trigger callbacks to your server
**Getting Started:**
- Use MCP-UI SDKs in multiple programming languages to create `UIResource` objects in your MCP server
- Return UI components from tools or resources using the standardized specification
- goose Desktop automatically renders MCP-UI components when detected
- Components support multiple rendering approaches for flexible styling
**Use Cases:** Interactive forms, data visualizations, booking interfaces, configuration wizards
**Use Cases:** Interactive forms, seat selection maps, data visualization dashboards, booking interfaces, configuration wizards
**Learn More:** [Building MCP Apps Tutorial](/docs/tutorials/building-mcp-apps)
**Learn More:** See the [MCP-UI Specification](https://mcpui.dev/guide/introduction) for technical details and implementation examples.
:::note
goose also supports [MCP-UI](/docs/guides/interactive-chat/mcp-ui), but MCP Apps is the recommended path for new extensions.
:::
[mcp-docs]: https://modelcontextprotocol.io/
[mcp-python]: https://github.com/modelcontextprotocol/python-sdk