.memstack/skills/create-mcp-app/SKILL.md
This skill should be used when the user asks to "create an MCP App", "build an MCP server", "add a UI to an MCP tool", "develop an interactive dashboard", "register an MCP server", or needs guidance on building MCP servers in the MemStack sandbox with optional Canvas UI rendering. Covers the full lifecycle from development to registration to UI activation.
npx skillsauth add s1366560/agi-demos create-mcp-appInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Build MCP servers in the sandbox environment with optional interactive UI that renders in the Canvas panel. This guide covers the complete lifecycle: develop, register, and activate.
You (Agent) write code in /workspace
|
v
register_mcp_server(server_name, server_type="stdio", command="python", args=["server.py"])
|
v
Platform starts your server as a child process (stdio transport)
|
v
Platform discovers tools via tools/list JSON-RPC call
|
v
Tools with _meta.ui.resourceUri → Platform auto-detects as "MCP App"
|
v
When tool is called → Platform fetches HTML via resources/read → Renders in Canvas panel
Key insight: The platform handles ALL the wiring. You just write a correct MCP server, register it, and the UI appears automatically.
FastMCP is a modern, higher-level framework that simplifies MCP server development. It supports both tool-only servers and interactive UI apps.
Install FastMCP:
pip install "fastmcp[apps]"
Minimal FastMCP Server with UI:
"""MCP Server using FastMCP framework."""
from fastmcp import FastMCP
mcp = FastMCP("my-dashboard")
@mcp.tool()
def render_dashboard(query: str = "default") -> str:
"""Render the dashboard with current data."""
return "Dashboard rendered"
@mcp.resource("ui://my-dashboard/dashboard")
def dashboard_ui() -> str:
"""Serve the dashboard HTML."""
return """<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>My Dashboard</title>
<style>
body { font-family: system-ui, sans-serif; padding: 20px; }
</style>
</head>
<body>
<h1>My Dashboard</h1>
<div id="content">Dashboard content here</div>
</body>
</html>"""
if __name__ == "__main__":
mcp.run()
FastMCP with Tool-UI Binding:
from fastmcp import FastMCP
from fastmcp.server.apps import AppConfig
mcp = FastMCP("my-app")
@mcp.tool(app=AppConfig(resource_uri="ui://my-app/view.html"))
def show_data(data: str) -> str:
"""Show data with interactive UI."""
return f"Data: {data}"
@mcp.resource("ui://my-app/view.html")
def view_html() -> str:
return "<html>...</html>"
Use the official mcp package for full control. This is the approach documented in the main sections below.
Write a Python MCP server in /workspace/{server_name}/server.py using the mcp package.
Minimal example with UI:
"""MCP Server: my-dashboard - Interactive Dashboard."""
from mcp.server import Server
from mcp.server.stdio import stdio_server
import asyncio
server = Server("my-dashboard")
@server.list_tools()
async def list_tools():
"""List available tools."""
return [
{
"name": "render_dashboard",
"description": "Render the dashboard with current data",
"inputSchema": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Data query"}
}
},
"_meta": {
"ui": {
"resourceUri": "ui://my-dashboard/dashboard",
"title": "My Dashboard"
}
}
}
]
@server.call_tool()
async def call_tool(name: str, arguments: dict):
"""Handle tool calls."""
if name == "render_dashboard":
return {"content": [{"type": "text", "text": "Dashboard rendered"}]}
raise ValueError(f"Unknown tool: {name}")
@server.list_resources()
async def list_resources():
"""List available resources."""
return [
{
"uri": "ui://my-dashboard/dashboard",
"name": "Dashboard UI",
"mimeType": "text/html"
}
]
@server.read_resource()
async def read_resource(uri):
"""Read resource content - serves HTML for the Canvas panel."""
uri = str(uri)
if uri == "ui://my-dashboard/dashboard":
html = """<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>My Dashboard</title>
<style>
body { font-family: system-ui, sans-serif; padding: 20px; }
</style>
</head>
<body>
<h1>My Dashboard</h1>
<div id="content">Dashboard content here</div>
</body>
</html>"""
return {"contents": [{"uri": uri, "mimeType": "text/html", "text": html}]}
raise ValueError(f"Unknown resource: {uri}")
async def main():
async with stdio_server() as (read_stream, write_stream):
await server.run(read_stream, write_stream)
if __name__ == "__main__":
asyncio.run(main())
[project]
name = "my-dashboard"
version = "0.1.0"
dependencies = ["mcp"]
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
cd /workspace/my-dashboard && pip install -e .
Before registering, verify the server starts without errors:
cd /workspace/my-dashboard && timeout 5 python server.py 2>&1 || true
If the server starts and waits for input (no error output), it is working correctly. The timeout command prevents it from hanging.
Use the register_mcp_server tool:
register_mcp_server(
server_name="my-dashboard",
server_type="stdio",
command="python",
args=["/workspace/my-dashboard/server.py"]
)
After registration:
tools/list_meta.ui on tools → marks as MCP Appmcp__my_dashboard__render_dashboardCall the registered tool. If it has _meta.ui, the Canvas panel will automatically render the HTML from your read_resource handler.
read_resource Return Format (MUST be exact)The read_resource handler MUST return a dict with this exact structure:
# CORRECT - dict with "contents" list
return {"contents": [{"uri": uri, "mimeType": "text/html", "text": html_string}]}
Common mistakes:
# WRONG - returning a tuple
return (uri, "text/html", html_string)
# WRONG - returning just the HTML string
return html_string
# WRONG - returning without "contents" wrapper
return {"uri": uri, "mimeType": "text/html", "text": html_string}
# WRONG - using "content" instead of "contents"
return {"content": [{"uri": uri, "mimeType": "text/html", "text": html_string}]}
The platform's MCP manager calls result.get("contents", []) and iterates to find item.get("text"). Any other format will silently fail.
_meta.ui Format (MUST use ui:// scheme)"_meta": {
"ui": {
"resourceUri": "ui://server-name/resource-path",
"title": "Display Title"
}
}
resourceUri MUST use the ui:// schemelist_resources and read_resource handleThe sandbox runs MCP servers as child processes using stdio (stdin/stdout). This is:
Do NOT switch to HTTP/SSE/WebSocket unless you have a specific reason (e.g., the server must be shared across multiple clients). stdio is simpler and more reliable for sandbox use.
The async with stdio_server() pattern keeps the server running. Your server MUST:
asyncio.run(main()) as the entry pointimport sys; print("debug", file=sys.stderr)list_resources Must Declare All UI ResourcesEvery resourceUri referenced in _meta.ui must also appear in list_resources():
@server.list_resources()
async def list_resources():
return [
{
"uri": "ui://my-server/dashboard", # Must match _meta.ui.resourceUri
"name": "Dashboard UI",
"mimeType": "text/html"
}
]
When registering, always use absolute paths for the server script:
# CORRECT
register_mcp_server(server_name="my-app", server_type="stdio",
command="python", args=["/workspace/my-app/server.py"])
# WRONG - relative path may not resolve correctly
register_mcp_server(server_name="my-app", server_type="stdio",
command="python", args=["server.py"])
The platform provides 3 server templates you can reference:
| Template | Description | Key Pattern |
|----------|-------------|-------------|
| web-dashboard | Interactive dashboard with metrics | _meta.ui + read_resource HTML |
| api-wrapper | REST API integration | Tool-only, no UI |
| data-processor | Data transformation pipeline | Tool-only, no UI |
The web-dashboard template is the canonical example for MCP Apps with UI.
Prefab is a declarative UI framework for Python. You describe layouts, charts, tables, forms using Python DSL, and FastMCP compiles them to JSON for the Canvas renderer.
Install Prefab:
pip install "fastmcp[apps]" "prefab-ui"
Example: Interactive Chart:
from prefab_ui.components import Column, Heading, BarChart, ChartSeries
from prefab_ui.app import PrefabApp
from fastmcp import FastMCP
mcp = FastMCP("Dashboard")
@mcp.tool(app=True)
def sales_chart(year: int) -> PrefabApp:
"""Show sales data as an interactive chart."""
data = [
{"month": "Jan", "revenue": 10000},
{"month": "Feb", "revenue": 15000},
{"month": "Mar", "revenue": 12000},
]
with Column(gap=4, css_class="p-6") as view:
Heading(f"{year} Sales")
BarChart(
data=data,
series=[ChartSeries(data_key="revenue", label="Revenue")],
x_axis="month",
)
return PrefabApp(view=view)
Available Prefab Components:
Column, Row, StackHeading, Text, LabelButton, IconButton, LinkBarChart, LineChart, PieChart, ScatterChartTable, DataGrid, ListTextField, Select, Checkbox, Radio, Slider, DatePickerCard, Modal, Drawer, Tabs, AccordionExample: Interactive Form:
from prefab_ui.components import Column, TextField, Button, Heading
from prefab_ui.app import PrefabApp
from fastmcp import FastMCP
mcp = FastMCP("FormApp")
@mcp.tool(app=True)
def user_form() -> PrefabApp:
"""Show a user input form."""
with Column(gap=3, css_class="p-4") as view:
Heading("User Information")
TextField(label="Name", placeholder="Enter your name")
TextField(label="Email", placeholder="Enter your email")
Button(label="Submit", on_click="submit_form")
return PrefabApp(view=view)
pip list | grep mcpcd /workspace/my-server && python server.pypython -c "import server" (from the server directory)_meta.ui.resourceUri uses ui:// schemelist_resources() includes the same URIread_resource() handles the URI and returns the correct dict formatlist_tools() returns a proper list of tool dictsname, description, and inputSchematools/listAfter registration, tools are namespaced as mcp__{server_name}__{tool_name} (with hyphens replaced by underscores). Use this full name when calling.
Multiple tools can reference the same resourceUri. The Canvas will render the same HTML resource regardless of which tool triggered it:
@server.list_tools()
async def list_tools():
return [
{
"name": "show_chart",
"description": "Show chart view",
"inputSchema": {"type": "object", "properties": {}},
"_meta": {"ui": {"resourceUri": "ui://my-app/view", "title": "Chart"}}
},
{
"name": "show_table",
"description": "Show table view",
"inputSchema": {"type": "object", "properties": {}},
"_meta": {"ui": {"resourceUri": "ui://my-app/view", "title": "Table"}}
}
]
Not every tool needs a UI. Tools without _meta.ui work normally as text-only tools:
{
"name": "fetch_data",
"description": "Fetch raw data",
"inputSchema": {"type": "object", "properties": {"query": {"type": "string"}}}
# No _meta.ui - this is a plain tool
}
The read_resource handler is called each time the UI needs to render. You can generate dynamic HTML based on server state:
import json
# Server-level state
_current_data = {}
@server.call_tool()
async def call_tool(name: str, arguments: dict):
global _current_data
if name == "update_data":
_current_data = arguments.get("data", {})
return {"content": [{"type": "text", "text": "Data updated"}]}
@server.read_resource()
async def read_resource(uri):
uri = str(uri)
if uri == "ui://my-app/view":
html_content = json.dumps(_current_data, indent=2)
html = f"""<!DOCTYPE html>
<html><body>
<pre>{html_content}</pre>
</body></html>"""
return {"contents": [{"uri": uri, "mimeType": "text/html", "text": html}]}
MCP Apps can send messages FROM the Canvas UI BACK to the agent conversation. This enables interactive workflows where the user makes choices in the UI and the agent reacts.
Architecture:
Guest iframe (your HTML)
| window.parent.postMessage(jsonRpcMsg, '*')
v
sandbox_proxy.html (relay)
| window.parent.postMessage(jsonRpcMsg, '*')
v
Host @mcp-ui/client Transport
| Parses JSON-RPC, validates ui/message schema
v
StandardMCPAppRenderer.handleMessage
| Extracts text from content array
v
CanvasPanel.onSendPrompt(text)
| Sends as user message to conversation
v
Agent receives and processes the message
CRITICAL: Use the exact JSON-RPC format below. The host @mcp-ui/client Transport validates incoming messages against the JSON-RPC schema. Any deviation will be silently dropped.
// In your HTML served by read_resource:
let msgIdCounter = 1;
function sendToAgent(text) {
window.parent.postMessage({
jsonrpc: "2.0",
id: msgIdCounter++,
method: "ui/message",
params: {
role: "user",
content: [{ type: "text", text: text }]
}
}, "*");
}
// Example: button click sends result to agent
document.getElementById('confirm-btn').addEventListener('click', () => {
const value = document.getElementById('result').textContent;
sendToAgent('User selected: ' + value);
});
Key requirements:
jsonrpc MUST be "2.0"id MUST be a unique incrementing number (not a string)method MUST be exactly "ui/message"params.content MUST be an array of ContentBlock objects: [{ type: "text", text: "..." }]params.role should be "user"window.parent.postMessage(msg, "*") — NOT window.postMessageDO NOT USE these hallucinated/non-existent APIs:
// WRONG - window.mcpApp does not exist, never injected by platform
window.mcpApp.sendMessage('result');
// WRONG - wrong message format, host expects JSON-RPC not custom types
window.parent.postMessage({ type: 'mcp-tool-result', result: 'value' }, '*');
// WRONG - this CDN URL does not exist, returns 404
import { sendMessage } from 'https://cdn.example.com/@modelcontextprotocol/ext-apps/0.1.0/dist/index.js';
Complete example with bidirectional communication:
@server.read_resource()
async def read_resource(uri):
uri = str(uri)
if uri == "ui://my-app/picker":
html = """<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>Picker</title></head>
<body>
<h1>Pick a Value</h1>
<div id="value">0</div>
<button onclick="change(1)">+</button>
<button onclick="change(-1)">-</button>
<button onclick="confirm()">Confirm</button>
<script>
let val = 0, msgId = 1;
function change(d) {
val += d;
document.getElementById('value').textContent = val;
}
function confirm() {
window.parent.postMessage({
jsonrpc: '2.0',
id: msgId++,
method: 'ui/message',
params: {
role: 'user',
content: [{ type: 'text', text: 'Selected value: ' + val }]
}
}, '*');
}
</script>
</body>
</html>"""
return {"contents": [{"uri": uri, "mimeType": "text/html", "text": html}]}
/workspace/{name}/server.pypyproject.toml lists mcp as dependencypip install -e .list_tools() returns valid tool definitions_meta.ui.resourceUri using ui:// schemelist_resources() declares all UI resource URIsread_resource(uri) returns {"contents": [{"uri": ..., "mimeType": "text/html", "text": ...}]}stdio_server() transport (default)window.parent.postMessage() with JSON-RPC ui/message formatparams.content is an array of {type: "text", text: "..."} objectstools
Sandbox MCP Server 是一个隔离的代码执行环境,提供完整的文件系统操作、命令执行、 代码分析、测试运行和远程桌面能力。当你需要执行代码、操作文件、运行测试、 分析代码结构、或需要图形界面操作时使用此技能。支持 Python、Node.js、Java 等多语言环境。
tools
Replace with description of the skill and when Claude should use it.
development
Generate high-quality images using ModelScope's Z-Image API. Use this skill when the user wants to generate images using the specific Z-Image model or ModelScope API they provided. Trigger words: 'Zimage', 'ModelScope', 'generate zimage'.
tools
No-code automation democratizes workflow building. Zapier and Make (formerly Integromat) let non-developers automate business processes without writing code. But no-code doesn't mean no-complexity ...