diff --git a/agent/deep_assistant.py b/agent/deep_assistant.py index 085beeb..9146743 100644 --- a/agent/deep_assistant.py +++ b/agent/deep_assistant.py @@ -311,6 +311,22 @@ async def init_agent(config: AgentConfig): sandbox, sandbox_type, workspace_root = await sandbox_task logger.info(f"init_agent sandbox ready, elapsed: {time.time() - create_start:.3f}s") + # Inject shell_env into Daytona sandbox via BASH_ENV file + if sandbox is not None and sandbox_type == "daytona": + _shell_env = { + "ASSISTANT_ID": config.bot_id, + "USER_IDENTIFIER": config.user_identifier, + "TRACE_ID": config.trace_id, + "ENABLE_SELF_KNOWLEDGE": str(config.enable_self_knowledge).lower(), + **(config.shell_env or {}), + } + env_lines = "\n".join(f'export {k}="{v}"' for k, v in _shell_env.items() if v is not None) + if env_lines: + from utils.daytona_sync import REMOTE_BASH_ENV_PATH, REMOTE_WORKSPACE_ROOT + bash_env_content = f"cd {REMOTE_WORKSPACE_ROOT}\n{env_lines}" + sandbox.execute(f"cat > {REMOTE_BASH_ENV_PATH} << 'ENVEOF'\n{bash_env_content}\nENVEOF") + logger.info(f"Injected {len(_shell_env)} env vars into Daytona BASH_ENV") + # Load sub-agents from skill directories subagents = await load_subagents( bot_id=config.bot_id, diff --git a/docs/mcp-app-training.md b/docs/mcp-app-training.md new file mode 100644 index 0000000..a940629 --- /dev/null +++ b/docs/mcp-app-training.md @@ -0,0 +1,1063 @@ +# MCP App 协议培训文档 + +> 面向团队的技术培训材料,帮助理解 MCP App 协议的核心机制、实现方式与应用场景。 + +--- + +## 目录 + +1. [为什么需要 MCP App](#1-为什么需要-mcp-app) +2. [MCP Apps vs A2UI 对比](#2-mcp-apps-vs-a2ui-对比) +3. [核心概念:三层分离架构](#3-核心概念三层分离架构) +4. [协议通信机制详解](#4-协议通信机制详解) +5. [代码实现走读](#5-代码实现走读) +6. [现有实现案例分析](#6-现有实现案例分析) +7. [如何开发一个新的 MCP App Skill](#7-如何开发一个新的-mcp-app-skill) +8. [实际应用场景](#8-实际应用场景) +9. [FAQ](#9-faq) + +--- + +## 1. 为什么需要 MCP App + +### 传统 Agent 的局限 + +传统的 AI Agent 只能输出文本(Markdown、代码块等),无法直接在对话中呈现**可交互的 UI**。用户看到的永远是静态文字。 + +### MCP App 解决的问题 + +MCP App 协议让 Agent 的工具调用(tool call)能够返回**富交互组件**——图表、表单、按钮、嵌入页面等——直接渲染在聊天窗口中,用户可以点击、选择、输入,结果再传回 Agent 继续对话。 + +``` +用户提问 → Agent 思考 → 调用 MCP Tool → 返回 UI 组件 → 用户交互 → 结果回传 Agent +``` + +**核心价值:让 Agent 从"只会说话"变成"能展示、能交互"。** + +--- + +## 2. MCP Apps vs A2UI 对比 + +> 参考:[hia2ui.com - MCP Apps vs A2UI](https://hia2ui.com/zh-cn/blog/mcp-apps-vs-a2ui/) + +在 Agent UI 领域,目前存在两种主流协议方案。了解它们的差异有助于我们做出更好的技术选型。 + +### 2.1 两种哲学 + +| | MCP Apps (Anthropic) | A2UI (Google) | +|---|---|---| +| **核心理念** | **UI 即资源** — 将 UI 视为不透明的、外部获取的黑盒资源 | **UI 即协议** — 通过只具有声明语义的强类型 JSON 蓝图传输 | +| **传输内容** | 完整的 HTML/CSS/JavaScript 代码包 | 纯 JSON 声明式数据,**绝对不包含可执行代码** | +| **URI 格式** | `ui://server-name/resource` | N/A(通过 JSON schema 定义组件类型) | +| **宿主角色** | 被动容器 — 将 HTML 丢进 iframe 渲染 | 主动绘制者 — 用本地原生组件树渲染 JSON 蓝图 | + +用一句话概括差异: +- **MCP Apps**:服务端说"这是我画好的 UI 界面,你直接嵌进去展示" +- **A2UI**:服务端说"这是我要展示的数据结构,你用你自己的 UI 组件来画" + +### 2.2 四大维度详细对比 + +#### 维度一:核心架构 + +``` +MCP Apps(Iframe 沙盒模式): +┌──────────────────────┐ +│ Host(聊天界面) │ +│ ┌──────────────────┐ │ +│ │ Iframe (sandbox) │ │ ← 将服务端返回的 HTML 代码 +│ │ ┌──────────────┐ │ │ "盲目地"丢进 iframe 沙盒 +│ │ │ 完整 HTML/JS │ │ │ +│ │ └──────────────┘ │ │ +│ └──────────────────┘ │ +└──────────────────────┘ + +A2UI(原生声明式蓝图): +┌──────────────────────┐ +│ Host(聊天界面) │ +│ │ +│ ┌─────┐ ┌─────┐ │ ← Host 拿到 JSON 蓝图后 +│ │Button│ │Card │ │ 用自己本地的原生组件渲染 +│ └─────┘ └─────┘ │ +│ ┌─────┐ ┌─────┐ │ +│ │Chart│ │List │ │ +│ └─────┘ └─────┘ │ +└──────────────────────┘ +``` + +#### 维度二:跨平台可移植性 + +| 平台 | MCP Apps | A2UI | +|------|---------|------| +| Web 浏览器 | 原生支持,表现优秀 | 映射为 React/Web 组件 | +| iOS | 需启动 WebView 内核,体验较重 | 映射为原生 UIButton 等 iOS 组件 | +| Android | 需启动 WebView 内核,体验较重 | 映射为 Material Design 组件 | +| 桌面端 | Electron 等框架可支持 | 映射为对应平台原生组件 | + +**结论**:A2UI 在跨平台一致性上优势明显。同一份 JSON 负载可以在各平台渲染为原生体验,而 MCP Apps 在非 Web 平台需要依赖 WebView,体验较重。 + +#### 维度三:样式控制权与设计一致性 + +| 维度 | MCP Apps | A2UI | +|------|---------|------| +| 样式由谁决定 | **服务端**决定(HTML/CSS 自带样式) | **宿主应用**决定(本地组件库样式) | +| 品牌一致性 | 较难保证,第三方 HTML 样式难以覆盖 | 自动继承宿主的 Design Tokens | +| 深色模式 | 需要 HTML App 自行适配 | 自动继承宿主的主题设置 | +| 定制能力 | 需要 CSS overrides 强行覆盖 | 天然支持,AI 只决定"展示什么",不决定"长什么样" | + +**结论**:A2UI 在设计一致性上完胜。MCP Apps 的 HTML 自带样式可能与宿主应用的设计规范冲突。 + +#### 维度四:安全性与信任边界 + +| 维度 | MCP Apps | A2UI | +|------|---------|------| +| 可执行代码 | 包含(HTML 中可嵌入 JS) | **绝对禁止**(纯 JSON 数据) | +| 隔离方式 | 浏览器 Iframe 沙盒 | 在协议层面从根本上阻断代码注入 | +| 攻击面 | Iframe 沙盒逃逸是已知攻击面 | 无可执行代码 = 无 UI 注入攻击面 | +| LLM 生成风险 | HTML 可能由 LLM 实时生成,存在幻觉风险 | 仅传输声明式数据,组件由预审批目录提供 | + +**结论**:A2UI 从架构层面更安全(无可执行代码传输)。MCP Apps 依赖 Iframe 沙盒隔离,安全性取决于浏览器实现。 + +### 2.3 各自适用场景 + +#### 选择 MCP Apps 的场景 + +适合需要展示**完整独立应用**或**重度定制 UI** 的场景: + +- 包含复杂交互逻辑的独立工具(如 3D WebGL 可视化、代码编辑器) +- 需要嵌入的遗留系统界面(老旧的内部报表系统等) +- 有特殊渲染需求的第三方工具(终端模拟器、地图引擎等) +- 全屏弹窗类的沉浸式体验 + +> 特点:Agent 将它们作为**独立的全屏应用**召唤出来,自成一体。 + +#### 选择 A2UI 的场景 + +适合需要在聊天流中**深度内联嵌入**富交互微件的场景: + +- 聊天流中的原生交互卡片(商品卡片、审批卡片等) +- 数据驱动的动态仪表盘 +- 需要完美匹配宿主品牌设计规范的组件 +- 需要跨 Web + Mobile 统一体验的场景 + +> 特点:AI 生成的卡片与前端手写组件**无法分辨真假**。 + +### 2.4 对比总结 + +``` + MCP Apps A2UI + ┌─────────────────┐ ┌─────────────────┐ + 灵活度 │ ★★★★★ 极高 │ │ ★★★☆☆ 受组件库约束│ + 跨平台 │ ★★★☆☆ Web 为主 │ │ ★★★★★ 全平台原生 │ + 设计一致性 │ ★★☆☆☆ 需手动适配│ │ ★★★★★ 自动继承 │ + 安全性 │ ★★★☆☆ 依赖沙盒 │ │ ★★★★★ 架构级安全 │ + 开发门槛 │ ★★★★☆ 写 HTML 即可│ │ ★★★☆☆ 需组件库支持│ + 复杂 UI 能力 │ ★★★★★ 无限制 │ │ ★★★☆☆ 受限于组件集│ + └─────────────────┘ └─────────────────┘ +``` + +### 2.5 我们的选择与思考 + +**我们当前采用 MCP Apps 协议**,原因: + +1. **灵活度优先**:我们的场景需要渲染各种复杂 UI(ECharts 图表、自定义表单、任意 HTML),MCP Apps 的"传 HTML"模式给了我们最大的自由度 +2. **Web 优先**:我们的宿主环境是 Web 聊天界面,MCP Apps 的 iframe 方案天然契合 +3. **开发效率**:写一个 HTML 模板 + 一个 MCP Server 就能实现一个新组件,门槛低 +4. **生态兼容**:MCP 协议本身已有广泛的社区和工具链支持 + +**未来可能的演进**: + +- 对于需要深度内联到聊天流、强调品牌一致性的轻量卡片(如通知卡片、状态标签),可以考虑引入 A2UI +- 两种协议**并不互斥**,未来可能在同一应用中混合使用:重度工具用 MCP Apps,轻量卡片用 A2UI +- 关注 CopilotKit 等框架的双协议支持进展 + +--- + +## 3. 核心概念:三层分离架构 + +MCP App 协议的精髓是 **数据与 UI 分离**: + +``` +┌─────────────────────────────────────────────────┐ +│ Host (宿主) │ +│ 即聊天界面,负责 iframe 管理和消息路由 │ +│ │ +│ ┌─────────────────┐ ┌─────────────────────┐ │ +│ │ MCP Server │ │ HTML App (iframe) │ │ +│ │ (数据层) │ │ (渲染层) │ │ +│ │ │ │ │ │ +│ │ tools/call │ │ 接收 postMessage │ │ +│ │ → 返回纯数据 │ │ → 渲染成可交互 UI │ │ +│ │ │ │ │ │ +│ │ resources/read │ │ 用户操作 │ │ +│ │ → 返回静态 HTML │ │ → postMessage 回传 │ │ +│ └─────────────────┘ └─────────────────────┘ │ +└─────────────────────────────────────────────────┘ +``` + +### 三个角色 + +| 角色 | 职责 | 对应代码 | +|------|------|----------| +| **MCP Server** | 处理 `tools/call` 返回结构化数据;处理 `resources/read` 返回 HTML 模板 | `*_server.py` | +| **HTML App** | 静态 HTML 文件,监听 `postMessage` 接收数据后渲染 UI | `apps/*.html` | +| **Host** | 聊天界面,负责创建 iframe、加载 HTML App、通过 postMessage 传递数据 | 前端聊天框架 | + +### 为什么要分离? + +- **安全**:HTML App 运行在 sandboxed iframe 中,与主页面隔离 +- **复用**:同一个 HTML App 模板可以渲染不同数据 +- **解耦**:MCP Server 不需要关心渲染细节,只负责产出数据 + +--- + +## 4. 协议通信机制详解 + +### 3.1 完整通信流程 + +以 `render_chart` 工具调用为例,完整流程如下: + +``` +步骤 1: Agent 决定调用 render_chart 工具 + ↓ +步骤 2: Host 发送 tools/call 请求到 MCP Server + → {"method": "tools/call", "params": {"name": "render_chart", "arguments": {...}}} + ↓ +步骤 3: MCP Server 返回 App Response(纯数据 + 资源引用) + ← {"type": "app", "resourceUri": "ui://data-dashboard/chart", "data": {...}} + ↓ +步骤 4: Host 解析到 resourceUri,发送 resources/read 请求 + → {"method": "resources/read", "params": {"uri": "ui://data-dashboard/chart"}} + ↓ +步骤 5: MCP Server 返回静态 HTML 模板 + ← {"mimeType": "text/html;profile=mcp-app", "text": "...chart.html内容..."} + ↓ +步骤 6: Host 将 HTML 加载到 sandboxed iframe + ↓ +步骤 7: iframe 内 HTML App 发送就绪信号 + → window.parent.postMessage({type: 'mcp-app-ready'}, '*') + ↓ +步骤 8: Host 收到就绪信号后,通过 postMessage 发送数据 + → iframe.postMessage({type: 'mcp-app-data', payload: {...}}, '*') + ↓ +步骤 9: HTML App 接收数据并渲染 UI + ↓ +步骤 10: (可选)用户交互后,HTML App 通过 postMessage 回传结果 + → window.parent.postMessage({type: 'mcp-app-response', payload: {...}}, '*') +``` + +### 3.2 三种关键消息类型 + +| 消息类型 | 方向 | 用途 | +|----------|------|------| +| `mcp-app-ready` | iframe → Host | HTML App 加载完毕,请求数据 | +| `mcp-app-data` | Host → iframe | 向 HTML App 传递工具调用返回的数据 | +| `mcp-app-response` | iframe → Host | 用户交互结果回传给 Agent | + +### 3.3 App Response 数据结构 + +MCP Server 的 `tools/call` 返回的核心结构: + +```json +{ + "type": "app", + "resourceUri": "ui://data-dashboard/chart", + "data": { + "title": "Monthly Revenue", + "chart_type": "line", + "data": { + "categories": ["Jan", "Feb", "Mar"], + "series": [{"name": "Revenue", "data": [820, 932, 901]}] + } + }, + "_meta": { + "mcpui.dev/ui-preferred-frame-size": ["100%", "400px"] + } +} +``` + +关键字段: +- `type: "app"` — 告诉 Host 这是一个需要渲染的 App 类型结果 +- `resourceUri` — 指向要加载的 HTML App 模板的 URI +- `data` — 传递给 HTML App 的业务数据 +- `_meta` — 元信息,如建议的 iframe 尺寸 + +### 3.4 Resource URI 协议 + +资源 URI 使用 `ui://` scheme: + +``` +ui://data-dashboard/chart → chart.html +ui://data-dashboard/metrics → metrics.html +ui://mcp-ui/html → html.html (通用 HTML 渲染器) +ui://mcp-ui/ask-user → ask-user.html (交互式问答) +``` + +MIME Type 约定: +- `text/html;profile=mcp-app` — 标准 MCP App HTML +- `text/uri-list` — 外部 URL 嵌入 + +--- + +## 5. 代码实现走读 + +### 5.1 目录结构(以 data-dashboard 为例) + +``` +skills/common/data-dashboard/ +├── .claude-plugin/ +│ └── plugin.json # 插件注册配置 +├── hooks/ +│ ├── pre_prompt.py # PrePrompt 钩子:注入工具使用指南 +│ └── dashboard_guide.md # Agent 使用指南(注入到 system prompt) +├── apps/ +│ ├── chart.html # 单图表渲染器 +│ ├── metrics.html # KPI 指标卡渲染器 +│ └── multi-chart.html # 多图表网格渲染器 +├── dashboard_server.py # MCP Server 主体 +├── dashboard_tools.json # 工具定义(JSON Schema) +└── mcp_common.py # 共享工具函数 +``` + +### 5.2 plugin.json — 插件注册 + +```json +{ + "name": "data-dashboard", + "description": "Renders data as interactive dashboard card UI", + "hooks": { + "PrePrompt": [{"type": "command", "command": "python hooks/pre_prompt.py"}] + }, + "mcpServers": { + "data_dashboard": { + "transport": "stdio", + "command": "python", + "args": ["./dashboard_server.py", "{bot_id}"] + } + } +} +``` + +要点: +- `mcpServers` 注册 MCP Server,使用 stdio 传输 +- `hooks.PrePrompt` 在对话开始前注入工具使用指南,让 Agent 知道何时/如何调用这些工具 +- `{bot_id}` 是动态参数,运行时替换为实际的 bot ID + +### 5.3 MCP Server 核心逻辑 + +Server 需要处理 5 种 MCP 方法: + +```python +# 1. initialize — 握手,声明 capabilities +if method == "initialize": + return {"capabilities": {"tools": {}, "resources": {}}} + +# 2. tools/list — 返回可用工具列表(含 _meta.ui.resourceUri) +elif method == "tools/list": + tools = load_tools_from_json("dashboard_tools.json") + return {"tools": tools} + +# 3. resources/list — 返回可用资源列表 +elif method == "resources/list": + return {"resources": RESOURCE_DEFINITIONS} + +# 4. resources/read — 返回 HTML App 文件内容 +elif method == "resources/read": + html = _load_app_html(uri) + return {"contents": [{"uri": uri, "mimeType": "text/html;profile=mcp-app", "text": html}]} + +# 5. tools/call — 执行工具,返回 App Response +elif method == "tools/call": + result = handler(arguments) + return result +``` + +### 5.4 工具定义中的 `_meta` 字段 + +工具定义(`dashboard_tools.json`)中的 `_meta.ui.resourceUri` 是关键: + +```json +{ + "name": "render_chart", + "description": "Render a single ECharts chart", + "inputSchema": { ... }, + "_meta": { + "ui": { + "resourceUri": "ui://data-dashboard/chart" + } + } +} +``` + +这个字段告诉 Host:调用这个工具的结果需要使用 `ui://data-dashboard/chart` 对应的 HTML App 来渲染。 + +### 5.5 HTML App 模板的标准写法 + +每个 HTML App 都遵循同一个模式: + +```html + + + + + + +
+ + + +``` + +如果需要**回传用户交互结果**(如 ask-user),额外添加: + +```javascript +// 用户点击提交后 +window.parent.postMessage({ + type: 'mcp-app-response', + payload: { /* 用户选择的数据 */ } +}, '*'); +``` + +--- + +## 6. 现有实现案例分析 + +### 6.1 mcp-ui — 基础 UI 组件库 + +提供三个通用工具: + +| 工具 | 功能 | HTML App | +|------|------|----------| +| `render_html` | 渲染任意 HTML/CSS/JS | `html.html` — 通用 HTML 注入渲染器 | +| `render_url` | 嵌入外部 URL | `url.html` — iframe 嵌套器 | +| `ask_user` | 交互式问答(单选/多选) | `ask-user.html` — 选项卡界面 | + +**特点**:`render_html` 是万能的——Agent 可以生成任意 HTML 内容直接渲染,灵活度最高。 + +### 6.2 data-dashboard — 数据可视化 + +提供三个专用图表工具: + +| 工具 | 功能 | HTML App | +|------|------|----------| +| `render_metrics` | KPI 指标卡 | `metrics.html` — 网格卡片布局 | +| `render_chart` | 单图表(6种类型) | `chart.html` — ECharts 渲染器 | +| `render_multi_chart` | 多图表网格 | `multi-chart.html` — 网格 ECharts | + +**特点**:Agent 只需提供结构化数据(类型、分类、数值),HTML App 负责用 ECharts 渲染出专业图表。 + +### 6.3 static-hosting — 静态文件托管 + +与前两者不同,static-hosting 不走 MCP App 协议,而是: +- Agent 将生成的 HTML/CSS/JS 写入文件系统 +- 通过 FastAPI 静态文件服务提供公开 URL +- 适用于需要**持久化访问**的场景(生成报告、网页等) + +**与 MCP App 的区别**: +- MCP App → 嵌入在聊天窗口内,临时性 +- static-hosting → 独立 URL,持久化,可分享 + +--- + +## 7. 如何开发一个新的 MCP App Skill + +### 步骤总结 + +``` +1. 创建目录结构 + skills/common/my-skill/ + ├── .claude-plugin/plugin.json + ├── hooks/ + │ ├── pre_prompt.py + │ └── usage_guide.md + ├── apps/ + │ └── my-widget.html + ├── my_server.py + ├── my_tools.json + └── mcp_common.py (symlink 或 copy) + +2. 定义工具 (my_tools.json) + - name, description, inputSchema + - _meta.ui.resourceUri 指向你的 HTML App + +3. 编写 HTML App (apps/my-widget.html) + - 监听 mcp-app-data + - 发送 mcp-app-ready + - (可选)发送 mcp-app-response + +4. 编写 MCP Server (my_server.py) + - 实现 initialize / tools/list / resources/list / resources/read / tools/call + - tools/call 中将参数转为 App Response + +5. 编写 Agent 使用指南 (hooks/usage_guide.md) + - 告诉 Agent 什么时候用、怎么用这个工具 + +6. 注册插件 (.claude-plugin/plugin.json) + - 配置 mcpServers 和 hooks +``` + +### 快速模板 + +一个最小的 MCP App Server(Python): + +```python +import asyncio, json, os +from mcp_common import (create_error_response, create_ping_response, + create_tools_list_response, load_tools_from_json, + handle_mcp_streaming) + +RESOURCE_MIME_TYPE = "text/html;profile=mcp-app" +APPS_DIR = os.path.join(os.path.dirname(__file__), "apps") + +RESOURCE_MAP = {"ui://my-skill/widget": "widget.html"} + +def _load_app_html(uri): + filename = RESOURCE_MAP.get(uri) + if not filename: + raise ValueError(f"Unknown resource URI: {uri}") + with open(os.path.join(APPS_DIR, filename), "r") as f: + return f.read() + +def _create_app_response(resource_uri, data, width="100%", height="auto"): + return {"content": [{"type": "text", "text": json.dumps({ + "type": "app", "resourceUri": resource_uri, "data": data, + "_meta": {"mcpui.dev/ui-preferred-frame-size": [width, height]}, + }, ensure_ascii=False)}]} + +async def handle_request(request): + method = request.get("method") + params = request.get("params", {}) + rid = request.get("id") + + if method == "initialize": + return {"jsonrpc": "2.0", "id": rid, "result": { + "protocolVersion": "2024-11-05", + "capabilities": {"tools": {}, "resources": {}}, + "serverInfo": {"name": "my-skill", "version": "1.0.0"}}} + elif method == "tools/list": + return create_tools_list_response(rid, load_tools_from_json("my_tools.json")) + elif method == "resources/list": + return {"jsonrpc": "2.0", "id": rid, "result": {"resources": [ + {"uri": "ui://my-skill/widget", "name": "widget", + "mimeType": RESOURCE_MIME_TYPE}]}} + elif method == "resources/read": + html = _load_app_html(params.get("uri", "")) + return {"jsonrpc": "2.0", "id": rid, "result": {"contents": [ + {"uri": params["uri"], "mimeType": RESOURCE_MIME_TYPE, "text": html}]}} + elif method == "tools/call": + # 在这里处理你的业务逻辑 + return {"jsonrpc": "2.0", "id": rid, "result": _create_app_response( + "ui://my-skill/widget", params.get("arguments", {}))} + return create_error_response(rid, -32601, f"Unknown method: {method}") + +if __name__ == "__main__": + asyncio.run(handle_mcp_streaming(handle_request)) +``` + +--- + +## 8. 实际应用场景 + +### 8.1 已实现的场景 + +| 场景 | 使用的 Skill | 说明 | +|------|-------------|------| +| 数据可视化 | data-dashboard | 将查询结果渲染为图表(折线图、饼图、KPI 卡片等) | +| 通用 HTML 渲染 | mcp-ui / render_html | Agent 生成任意 HTML 内容直接展示 | +| 交互式问答 | mcp-ui / ask_user | 让用户通过点击选项来回答问题 | +| 外部页面嵌入 | mcp-ui / render_url | 在聊天中嵌入外部网页 | +| 报告生成 | static-hosting | 生成独立 HTML 报告,提供持久化 URL | + +--- + +### 8.2 电商 & 零售行业 + +#### 场景 1:商品浏览与选购 + +Agent 作为导购,在对话中展示可交互的商品卡片,用户直接点选下单。 +``` +用户: "我想买一杯咖啡" +Agent: 查询商品 → render_html 展示商品卡片列表(图片、价格、规格选择按钮) +用户: 点击"大杯拿铁" → mcp-app-response 回传选择 +Agent: 生成订单 → render_html 展示订单确认页(含二维码/支付链接) +用户: 点击"确认支付" → 跳转支付或内嵌支付页 +``` + +#### 场景 2:订单追踪可视化 + +用户查询订单后,Agent 用图表和时间轴展示物流进度。 +``` +用户: "我的订单到哪了?" +Agent: 查询物流 API → render_html 渲染物流时间轴(已下单→已发货→运输中→派送中) + render_url 嵌入地图展示快递实时位置 +``` + +#### 场景 3:智能比价与推荐 + +Agent 搜索多平台价格,用图表对比展示,用户点击心仪商品直接购买。 +``` +用户: "帮我比较一下 AirPods Pro 各平台价格" +Agent: 调用多个 API → render_chart(chart_type="bar") 展示各平台价格柱状图 + render_html 渲染商品对比卡片(含"去购买"按钮) +用户: 点击最低价平台的"去购买" → render_url 嵌入购买页面 +``` + +#### 场景 4:营销数据看板 + +商家用 Agent 查看店铺运营数据,实时图表展示 GMV、转化率、流量来源等。 +``` +店主: "看看这周的店铺数据" +Agent: 查询后台 API + → render_metrics 展示 KPI 卡片(GMV、订单数、客单价、退货率) + → render_multi_chart 展示趋势图 + 流量来源饼图 + 热销品排行柱状图 +``` + +#### 场景 5:退换货自助流程 + +用户在对话中完成退换货的完整流程,无需跳出。 +``` +用户: "我想退货" +Agent: ask_user 选择订单 → ask_user 选择退货原因 + → render_html 展示退货信息确认页(商品图、退款金额、退货地址) +用户: 点击"确认退货" → Agent 调用退货 API → 展示退货单号和进度 +``` + +--- + +### 8.3 教育 & 培训行业 + +#### 场景 1:交互式测验 + +Agent 出题后用选项卡 UI 让学生作答,即时批改并展示成绩图表。 +``` +老师: "给学生出一套 10 道英语选择题" +Agent: 生成题目 → ask_user 逐题展示选项(支持单选/多选) +学生: 逐题点击选项并提交 +Agent: 批改 → render_chart(chart_type="gauge") 展示总分 + → render_multi_chart 展示各知识点得分雷达图 + 错题分布 +``` + +#### 场景 2:学习进度仪表盘 + +学生查看自己的学习数据,Agent 用图表展示进度和薄弱环节。 +``` +学生: "我的学习情况怎么样?" +Agent: 查询学习记录 + → render_metrics 展示关键指标(完成率、连续学习天数、总学时) + → render_chart(chart_type="radar") 展示各科目能力雷达图 + → render_chart(chart_type="line") 展示近 30 天学习时长趋势 +``` + +#### 场景 3:代码 Playground + +编程教学中,Agent 在对话内嵌入可运行的代码编辑器,学生即学即练。 +``` +老师: "教学生用 Python 写一个冒泡排序" +Agent: 讲解算法 → render_html 渲染一个内嵌的代码编辑器 + 运行按钮 + 输出区 +学生: 修改代码 → 点击运行 → 实时看到排序过程动画 +``` + +#### 场景 4:课程安排与选课 + +用交互式日历展示课表,学生直接点选空闲时段报名课程。 +``` +学生: "我想报名下周的课程" +Agent: 查询可选课程 → render_html 渲染交互式周历(已有课程标灰,可选课程高亮) +学生: 点击某个时段的课程 → mcp-app-response 回传选课信息 +Agent: 确认选课 → 更新课表 +``` + +#### 场景 5:知识图谱可视化 + +复杂知识点之间的关系用交互式图谱展示,点击节点展开详情。 +``` +学生: "帮我梳理一下机器学习的知识体系" +Agent: render_html 渲染交互式知识图谱(D3.js 力导向图) + 节点:监督学习、无监督学习、强化学习... +学生: 点击"监督学习" → 回传节点 ID → Agent 展开子图谱(回归、分类、SVM、决策树...) +``` + +--- + +### 8.4 金融 & 财务行业 + +#### 场景 1:投资组合分析 + +Agent 查询持仓数据后,用图表展示资产配置和收益走势。 +``` +用户: "看看我的投资组合表现" +Agent: → render_chart(chart_type="pie") 展示资产配置比例(股票/债券/基金/现金) + → render_chart(chart_type="line") 展示近一年收益率走势(vs 沪深300) + → render_metrics 展示关键指标(总资产、日收益、年化收益率、最大回撤) +``` + +#### 场景 2:财务报表可视化 + +企业财务数据自动生成专业图表,支持钻取查看明细。 +``` +CFO: "展示 Q2 的财务概况" +Agent: 查询 ERP → render_multi_chart 四宫格展示: + - 收入趋势(折线图) + - 成本结构(饼图) + - 各部门预算执行(堆叠柱状图) + - 现金流仪表盘(gauge) +用户: 点击"销售部"柱子 → Agent 展开销售部详细费用明细 +``` + +#### 场景 3:风险评估与审批 + +贷款/保险审核场景,Agent 渲染评估报告和审批按钮。 +``` +风控: "审核这笔贷款申请" +Agent: 查询征信 → render_html 渲染风险评估报告(信用评分、负债率、历史逾期) + → render_chart(chart_type="gauge") 展示综合风险分数 + → render_html 底部渲染"批准 / 拒绝 / 补充材料"操作按钮 +风控: 点击"批准" → Agent 调用审批 API → 流程完成 +``` + +#### 场景 4:账单与报销管理 + +员工在对话中完成报销申请的完整流程。 +``` +员工: "我要提交上周出差的报销" +Agent: ask_user 选择出差项目和费用类型 + → render_html 渲染报销单表单(日期、金额、发票上传区) +员工: 填写并提交 → Agent 校验金额 → 提交审批流 + → render_html 展示审批进度时间轴 +``` + +--- + +### 8.5 医疗 & 健康行业 + +#### 场景 1:健康数据可视化 + +患者查看自己的健康指标趋势,Agent 用图表直观展示。 +``` +患者: "看看我最近的血压数据" +Agent: 查询健康记录 + → render_chart(chart_type="line") 展示近 30 天血压趋势(收缩压/舒张压双线) + → render_metrics 展示平均值、最高值、异常次数 + → render_html 底部展示健康建议卡片 +``` + +#### 场景 2:在线问诊引导 + +Agent 用交互式问答收集症状,辅助分诊。 +``` +患者: "我最近头疼" +Agent: ask_user 收集症状细节: + Q1: "头疼持续多久?" → [1-3天, 一周以上, 反复发作] + Q2: "伴随哪些症状?" → [恶心, 视力模糊, 发热, 无其他] (multi_select) + Q3: "疼痛位置?" → render_html 展示头部示意图,用户点击标记位置 +Agent: 综合分析 → render_html 展示初步评估结果 + 推荐科室 + 预约按钮 +``` + +#### 场景 3:预约挂号 + +在对话中展示医生排班表,患者点选预约。 +``` +患者: "帮我挂神经内科的号" +Agent: 查询排班 → render_html 渲染医生列表卡片(照片、职称、擅长、可约时段) +患者: 点击某医生 → ask_user 选择具体时间段 +Agent: 确认预约 → 展示预约成功信息和就诊提醒 +``` + +--- + +### 8.6 企业办公 & HR + +#### 场景 1:OA 审批流程 + +请假、采购、出差等审批在对话中一键完成。 +``` +员工: "帮我请三天年假" +Agent: ask_user 选择请假类型和日期范围 + → render_html 渲染请假申请预览(含剩余年假天数) +员工: 确认 → Agent 提交 OA 系统 + → render_html 展示审批链进度(直属经理→HR→完成) +``` + +#### 场景 2:团队数据看板 + +管理者查看团队运营指标,图表一目了然。 +``` +经理: "看看我们团队这个月的情况" +Agent: → render_metrics 展示 KPI(项目完成率、工时利用率、bug 数、客户满意度) + → render_multi_chart 展示: + - 各成员工时分布(堆叠柱状图) + - 项目进度甘特图(render_html 自定义) + - 本月 vs 上月对比(折线图) +``` + +#### 场景 3:招聘面试管理 + +HR 用 Agent 管理候选人,在对话中查看面试安排和评估。 +``` +HR: "今天有哪些面试?" +Agent: 查询日程 → render_html 渲染今日面试时间轴(候选人、岗位、面试官、时间) +HR: 点击某候选人 → Agent 展示简历摘要 + 前几轮面评 + → ask_user 选择面试结论(通过/待定/淘汰) +``` + +#### 场景 4:会议纪要与投票 + +会议中实时收集决策投票,即时展示结果。 +``` +主持人: "大家投票选择 Q3 的主推方案" +Agent: ask_user 展示方案选项(方案A/方案B/方案C),支持多人投票 + → 汇总结果 → render_chart(chart_type="pie") 展示投票比例 + → render_html 展示会议结论摘要 + 行动项清单 +``` + +--- + +### 8.7 房产 & 本地生活 + +#### 场景 1:房源浏览与对比 + +在对话中展示房源卡片,支持对比和地图查看。 +``` +用户: "帮我找朝阳区两居室,预算 500 万以内" +Agent: 搜索房源 → render_html 渲染房源卡片列表(图片轮播、价格、面积、户型图) +用户: 选中 3 套 → Agent → render_html 渲染对比表格(价格/面积/楼层/朝向/学区) +用户: 点击"查看位置" → render_html 渲染地图标注(3 套房源 + 周边配套) +``` + +#### 场景 2:餐厅推荐与订位 + +Agent 推荐餐厅,用户在对话中直接预约座位。 +``` +用户: "今晚想吃日料,帮我推荐" +Agent: 搜索附近餐厅 → render_html 渲染餐厅卡片(评分、人均、距离、招牌菜图片) +用户: 点击某餐厅 → Agent 展示详情 + 可用时段 + → ask_user 选择用餐时间和人数 +Agent: 调用预约 API → 展示预约确认信息 +``` + +#### 场景 3:装修进度管理 + +业主用 Agent 追踪装修进度,图文结合展示。 +``` +业主: "装修到哪一步了?" +Agent: 查询工程系统 → render_html 渲染装修进度时间轴(水电→瓦工→木工→油漆→软装) + 每个节点可展开查看现场照片 + → render_chart(chart_type="pie") 展示费用分配 + → render_metrics 展示预算执行(总预算、已花费、剩余) +``` + +--- + +### 8.8 物流 & 供应链 + +#### 场景 1:运输追踪大屏 + +在对话中展示车队/货物的实时状态。 +``` +调度员: "看看今天所有在途车辆" +Agent: 查询 TMS → render_html 渲染地图(标注所有在途车辆位置和状态) + → render_metrics 展示概览(在途 23 辆、已到达 15 辆、异常 2 辆) +调度员: 点击异常车辆标注 → Agent 展示异常详情 + 处理选项按钮 +``` + +#### 场景 2:库存预警与补货 + +Agent 监控库存,低于阈值时主动展示预警图表和补货建议。 +``` +仓管: "哪些商品快缺货了?" +Agent: 查询 WMS → render_html 渲染库存预警表格(红/黄/绿三色标注) + → render_chart(chart_type="bar") 展示 Top 10 紧缺商品及预计断货天数 + → render_html 底部渲染"一键生成补货单"按钮 +仓管: 点击按钮 → Agent 自动生成采购单 → ask_user 确认供应商和数量 +``` + +--- + +### 8.9 旅游 & 酒店行业 + +#### 场景 1:行程规划助手 + +Agent 生成交互式行程表,用户拖拽调整。 +``` +用户: "帮我规划 5 天东京自由行" +Agent: 生成行程 → render_html 渲染交互式日程表(每天的景点、交通、餐厅、住宿) + 每个景点卡片含图片、预计时长、门票价格 +用户: 拖拽调整顺序 / 删除某景点 → mcp-app-response 回传新顺序 +Agent: 重新优化路线 → render_html 更新地图路线图 +``` + +#### 场景 2:酒店预订对比 + +Agent 搜索多个平台,在对话中渲染对比界面。 +``` +用户: "帮我找东京新宿的酒店,2 晚" +Agent: 搜索多平台 → render_html 渲染酒店对比卡片(图片轮播、评分、价格、设施标签) + → render_chart(chart_type="scatter") 展示价格 vs 评分散点图 +用户: 点击心仪酒店 → render_url 嵌入预订页面 +``` + +#### 场景 3:景区实时信息 + +展示景区客流量、天气、开放状态等实时信息。 +``` +用户: "迪士尼现在人多吗?" +Agent: → render_metrics 展示实时数据(当前客流 12,000、各区域排队时长) + → render_chart(chart_type="line") 展示今日客流量趋势(预测下午 3 点高峰) + → render_html 展示热门项目排队时间排行(红绿标注) +``` + +--- + +### 8.10 制造 & 工业行业 + +#### 场景 1:生产监控大屏 + +Agent 实时展示产线状态和异常告警。 +``` +厂长: "1 号产线今天的情况怎么样?" +Agent: → render_metrics 展示 KPI(产量、良品率、设备 OEE、停机次数) + → render_multi_chart 展示: + - 每小时产量趋势(折线图) + - 不良品分类(饼图) + - 各工位效率对比(柱状图) + - 设备温度仪表盘(gauge) +``` + +#### 场景 2:设备维保管理 + +展示设备健康状态,支持一键报修。 +``` +维保工程师: "哪些设备需要保养了?" +Agent: 查询设备系统 → render_html 渲染设备列表(状态灯:绿/黄/红) + → render_chart(chart_type="bar") 展示各设备距下次保养的剩余天数 +工程师: 点击某设备 → Agent 展示维保记录 + 零件清单 + → render_html 渲染"创建工单"按钮 → 点击后自动创建维保工单 +``` + +#### 场景 3:质检报告可视化 + +质检数据自动生成可视化报告,支持导出。 +``` +质检员: "生成今天的质检报告" +Agent: 查询质检数据 + → render_multi_chart 展示各检测项合格率、SPC 控制图 + → render_html 渲染质检报告摘要(含不合格批次明细表格) + → static-hosting 生成 PDF 格式报告,提供下载 URL +``` + +--- + +### 8.11 IT & 运维行业 + +#### 场景 1:服务监控仪表盘 + +在对话中实时展示系统运行状态。 +``` +运维: "生产环境现在状态怎么样?" +Agent: → render_metrics 展示关键指标(在线实例 12/12、P99: 230ms、错误率 0.02%) + → render_multi_chart 展示 CPU/内存/QPS/错误率四宫格图表 + → render_url 嵌入 Grafana 面板查看更多细节 +``` + +#### 场景 2:故障排查向导 + +Agent 引导运维人员逐步排查问题。 +``` +运维: "用户反馈页面加载很慢" +Agent: + Step 1 → ask_user 选择受影响的服务(API/Web/DB) + Step 2 → Agent 查询日志 → render_chart 展示该服务近 1 小时响应时间 + Step 3 → 发现 DB 慢查询 → render_html 渲染慢查询 Top 10 表格 + Step 4 → ask_user 选择处理方式(添加索引 / 重启连接池 / 扩容) + Step 5 → Agent 执行 → render_metrics 展示恢复后的指标 +``` + +#### 场景 3:CI/CD 流水线管理 + +在对话中创建和监控部署流水线。 +``` +开发者: "帮我配一个新的部署流水线" +Agent: + → ask_user 选择代码仓库和分支策略 + → ask_user 选择构建环境(Node 20 / Python 3.12 / Go 1.22) + → ask_user 选择部署目标(dev / staging / prod) + → render_html 展示生成的 YAML 配置预览 + → 用户确认 → Agent 调 API 创建 → render_html 展示流水线状态时间轴 +``` + +--- + +### 8.12 场景速查表 + +| 行业 | 典型场景 | 核心工具组合 | +|------|---------|-------------| +| 电商零售 | 商品浏览、比价、订单追踪、营销看板、退换货 | render_html + ask_user + render_chart | +| 教育培训 | 在线测验、学习看板、代码练习、选课、知识图谱 | ask_user + render_chart + render_html | +| 金融财务 | 投资分析、财报可视化、风控审批、报销管理 | render_multi_chart + render_metrics + render_html | +| 医疗健康 | 健康趋势、问诊引导、预约挂号 | render_chart + ask_user + render_html | +| 企业办公 | OA 审批、团队看板、招聘管理、会议投票 | ask_user + render_metrics + render_chart | +| 房产生活 | 房源对比、餐厅预约、装修追踪 | render_html + render_chart + ask_user | +| 物流供应链 | 运输追踪、库存预警、补货管理 | render_html + render_metrics + render_chart | +| 旅游酒店 | 行程规划、酒店对比、景区实时信息 | render_html + render_chart + render_url | +| 制造工业 | 产线监控、设备维保、质检报告 | render_multi_chart + render_metrics + render_html | +| IT 运维 | 服务监控、故障排查、CI/CD 管理 | render_metrics + render_multi_chart + ask_user | + +## 9. FAQ + +### Q: MCP App 和 static-hosting 有什么区别? + +| 维度 | MCP App | static-hosting | +|------|---------|----------------| +| 渲染位置 | 聊天窗口内(iframe) | 独立浏览器 Tab | +| 生命周期 | 临时,随对话存在 | 持久化,有独立 URL | +| 交互能力 | 双向(postMessage) | 单向(只展示) | +| 适用场景 | 对话内交互组件 | 报告、网页、可分享内容 | + +### Q: HTML App 是每次重新加载还是缓存的? + +HTML App 模板是通过 `resources/read` 获取的静态文件,Host 可以缓存。但每次 `tools/call` 的数据是动态的,通过 postMessage 实时传入。 + +### Q: 用户交互结果如何回传给 Agent? + +HTML App 通过 `window.parent.postMessage({type: 'mcp-app-response', payload: ...})` 发送,Host 接收后将 payload 作为工具调用结果传回 Agent,Agent 可以据此继续对话。 + +### Q: 安全性如何保证? + +- HTML App 运行在 **sandboxed iframe** 中,无法访问主页面 DOM +- 使用 `postMessage` 通信,有明确的消息协议 +- 不执行任意远程代码,HTML 模板是预定义的静态文件 + +### Q: 我能用 React/Vue 来写 HTML App 吗? + +可以,但建议将构建产物打包为单个 HTML 文件(inline CSS/JS)。因为 `resources/read` 返回的是一个完整的 HTML 字符串,不支持多文件加载。也可以用 CDN 引入框架(如示例中 ECharts 用了 CDN)。 + +### Q: PrePrompt Hook 是做什么的? + +PrePrompt 钩子在每次对话开始前执行,将 `usage_guide.md` 的内容注入到 Agent 的 system prompt 中。这让 Agent 知道有哪些工具可用,以及什么时候应该调用它们。**这是让 Agent "学会"使用 UI 工具的关键。** + +--- + +## 附录:参考资源 + +- 项目内 mcp-ui 源码:`skills/common/mcp-ui/` +- 项目内 data-dashboard 源码:`skills/common/data-dashboard/` +- 项目内 static-hosting 定义:`skills/support/static-hosting/SKILL.md` +- MCP UI 社区项目:[github.com/idosal/mcp-ui](https://github.com/idosal/mcp-ui) +- MCP 协议规范:[modelcontextprotocol.io](https://modelcontextprotocol.io) diff --git a/skills/developing/ecommerce-storefront/.claude-plugin/plugin.json b/skills/developing/ecommerce-storefront/.claude-plugin/plugin.json new file mode 100644 index 0000000..37d6d8e --- /dev/null +++ b/skills/developing/ecommerce-storefront/.claude-plugin/plugin.json @@ -0,0 +1,19 @@ +{ + "name": "ecommerce-storefront", + "description": "Renders interactive product browsing, selection, and order confirmation UI for e-commerce scenarios.", + "hooks": { + "PrePrompt": [ + { + "type": "command", + "command": "python hooks/pre_prompt.py" + } + ] + }, + "mcpServers": { + "ecommerce_storefront": { + "transport": "stdio", + "command": "python", + "args": ["./ecommerce_server.py", "{bot_id}"] + } + } +} diff --git a/skills/developing/ecommerce-storefront/apps/order-confirm.html b/skills/developing/ecommerce-storefront/apps/order-confirm.html new file mode 100644 index 0000000..bbbfa1d --- /dev/null +++ b/skills/developing/ecommerce-storefront/apps/order-confirm.html @@ -0,0 +1,233 @@ + + + + + +Order Confirmation + + + +
+
+
+
+
+
+
+
+ + + diff --git a/skills/developing/ecommerce-storefront/apps/product-list.html b/skills/developing/ecommerce-storefront/apps/product-list.html new file mode 100644 index 0000000..8e00171 --- /dev/null +++ b/skills/developing/ecommerce-storefront/apps/product-list.html @@ -0,0 +1,288 @@ + + + + + +Product List + + + +

+
+ + + diff --git a/skills/developing/ecommerce-storefront/ecommerce_server.py b/skills/developing/ecommerce-storefront/ecommerce_server.py new file mode 100644 index 0000000..54eb5cf --- /dev/null +++ b/skills/developing/ecommerce-storefront/ecommerce_server.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 +""" +E-Commerce Storefront MCP Server - standard MCP Apps protocol (SEP-1865). + +- tools/call returns structured data only (no HTML) +- resources/read returns static HTML App files +- Host renders HTML App in iframe, passes tool data via postMessage +""" + +import asyncio +import json +import os +from typing import Any, Dict + +from mcp_common import ( + create_error_response, + create_ping_response, + create_tools_list_response, + load_tools_from_json, + handle_mcp_streaming, +) + +RESOURCE_MIME_TYPE = "text/html;profile=mcp-app" +APPS_DIR = os.path.join(os.path.dirname(__file__), "apps") + +# Resource URI -> static HTML App file mapping +RESOURCE_MAP = { + "ui://ecommerce-storefront/product-list": "product-list.html", + "ui://ecommerce-storefront/order-confirm": "order-confirm.html", +} + +RESOURCE_DEFINITIONS = [ + { + "uri": "ui://ecommerce-storefront/product-list", + "name": "product-list", + "title": "Product List", + "description": "Interactive product cards with spec selection", + "mimeType": RESOURCE_MIME_TYPE, + }, + { + "uri": "ui://ecommerce-storefront/order-confirm", + "name": "order-confirm", + "title": "Order Confirmation", + "description": "Order summary with payment options", + "mimeType": RESOURCE_MIME_TYPE, + }, +] + + +def _load_app_html(uri: str) -> str: + """Load static HTML App file for the given resource URI.""" + filename = RESOURCE_MAP.get(uri) + if not filename: + raise ValueError(f"Unknown resource URI: {uri}") + filepath = os.path.join(APPS_DIR, filename) + with open(filepath, "r", encoding="utf-8") as f: + return f.read() + + +def _create_app_response(resource_uri: str, data: Dict[str, Any], + width: str = "100%", height: str = "auto") -> Dict[str, Any]: + """Create a tool result for MCP Apps protocol.""" + app_json = json.dumps({ + "type": "app", + "resourceUri": resource_uri, + "data": data, + "_meta": { + "mcpui.dev/ui-preferred-frame-size": [width, height], + }, + }, ensure_ascii=False) + return {"content": [{"type": "text", "text": app_json}]} + + +# --------------------------------------------------------------------------- +# Tool handlers +# --------------------------------------------------------------------------- + +def _handle_render_product_list(arguments: Dict[str, Any]) -> Dict[str, Any]: + title = arguments.get("title", "Products") + products = arguments.get("products", []) + if not products: + raise ValueError("Missing required parameter: products") + return _create_app_response( + "ui://ecommerce-storefront/product-list", + {"title": title, "products": products}, + "100%", "auto", + ) + + +def _handle_render_order_confirm(arguments: Dict[str, Any]) -> Dict[str, Any]: + title = arguments.get("title", "Order Confirmation") + order = arguments.get("order") + payment = arguments.get("payment") + if not order: + raise ValueError("Missing required parameter: order") + if not payment: + raise ValueError("Missing required parameter: payment") + if not order.get("items"): + raise ValueError("Missing required parameter: order.items") + return _create_app_response( + "ui://ecommerce-storefront/order-confirm", + {"title": title, "order": order, "payment": payment}, + "100%", "auto", + ) + + +TOOL_HANDLERS = { + "render_product_list": _handle_render_product_list, + "render_order_confirm": _handle_render_order_confirm, +} + + +async def handle_request(request: Dict[str, Any]) -> Dict[str, Any]: + """Handle an MCP request.""" + try: + method = request.get("method") + params = request.get("params", {}) + request_id = request.get("id") + + if method == "initialize": + return { + "jsonrpc": "2.0", + "id": request_id, + "result": { + "protocolVersion": "2024-11-05", + "capabilities": { + "tools": {}, + "resources": {}, + }, + "serverInfo": { + "name": "ecommerce-storefront", + "version": "1.0.0", + }, + }, + } + + elif method == "ping": + return create_ping_response(request_id) + + elif method == "tools/list": + tools = load_tools_from_json("ecommerce_tools.json") + if not tools: + tools = [ + { + "name": name, + "description": f"Render {name.replace('render_', '')}", + "inputSchema": {"type": "object", "properties": {}, "required": []}, + "_meta": {"ui": {"resourceUri": uri}}, + } + for name, uri in [ + ("render_product_list", "ui://ecommerce-storefront/product-list"), + ("render_order_confirm", "ui://ecommerce-storefront/order-confirm"), + ] + ] + return create_tools_list_response(request_id, tools) + + elif method == "resources/list": + return { + "jsonrpc": "2.0", + "id": request_id, + "result": {"resources": RESOURCE_DEFINITIONS}, + } + + elif method == "resources/read": + uri = params.get("uri", "") + try: + html = _load_app_html(uri) + except (ValueError, FileNotFoundError) as e: + return create_error_response(request_id, -32602, str(e)) + return { + "jsonrpc": "2.0", + "id": request_id, + "result": { + "contents": [ + { + "uri": uri, + "mimeType": RESOURCE_MIME_TYPE, + "text": html, + } + ] + }, + } + + elif method == "tools/call": + tool_name = params.get("name") + arguments = params.get("arguments", {}) + + handler = TOOL_HANDLERS.get(tool_name) + if not handler: + return create_error_response(request_id, -32601, f"Unknown tool: {tool_name}") + + try: + result = handler(arguments) + except ValueError as e: + return create_error_response(request_id, -32602, str(e)) + + return {"jsonrpc": "2.0", "id": request_id, "result": result} + + else: + return create_error_response(request_id, -32601, f"Unknown method: {method}") + + except Exception as e: + return create_error_response( + request.get("id"), -32603, f"Internal error: {str(e)}" + ) + + +async def main(): + await handle_mcp_streaming(handle_request) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/skills/developing/ecommerce-storefront/ecommerce_tools.json b/skills/developing/ecommerce-storefront/ecommerce_tools.json new file mode 100644 index 0000000..031998b --- /dev/null +++ b/skills/developing/ecommerce-storefront/ecommerce_tools.json @@ -0,0 +1,125 @@ +[ + { + "name": "render_product_list", + "description": "Render an interactive product card list. Users can browse products, select specifications (size/flavor/etc.), and add items to cart. Returns the user's selection via mcp-app-response.", + "inputSchema": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Title displayed at the top of the product list" + }, + "products": { + "type": "array", + "description": "Array of product objects to display as cards", + "items": { + "type": "object", + "properties": { + "id": { "type": "string", "description": "Unique product identifier" }, + "name": { "type": "string", "description": "Product name" }, + "description": { "type": "string", "description": "Short product description" }, + "image": { "type": "string", "description": "Product image URL" }, + "price": { "type": "number", "description": "Base price" }, + "currency": { "type": "string", "description": "Currency symbol, default: '$'", "default": "$" }, + "specs": { + "type": "array", + "description": "Available specification groups (e.g. size, flavor)", + "items": { + "type": "object", + "properties": { + "label": { "type": "string", "description": "Spec group label, e.g. 'Size'" }, + "options": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { "type": "string", "description": "Option name, e.g. 'Large'" }, + "price_delta": { "type": "number", "description": "Price adjustment, default 0", "default": 0 } + }, + "required": ["name"] + } + } + }, + "required": ["label", "options"] + } + }, + "tags": { + "type": "array", + "items": { "type": "string" }, + "description": "Optional tags like 'Hot', 'New', 'Sale'" + } + }, + "required": ["id", "name", "price"] + } + } + }, + "required": ["title", "products"] + }, + "_meta": { + "ui": { + "resourceUri": "ui://ecommerce-storefront/product-list" + } + } + }, + { + "name": "render_order_confirm", + "description": "Render an order confirmation page with order details, total price, and a payment action button or QR code. Returns 'confirmed' or 'cancelled' via mcp-app-response.", + "inputSchema": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Page title, e.g. 'Order Confirmation'" + }, + "order": { + "type": "object", + "description": "Order details", + "properties": { + "order_id": { "type": "string", "description": "Order ID" }, + "items": { + "type": "array", + "description": "Ordered items", + "items": { + "type": "object", + "properties": { + "name": { "type": "string", "description": "Product name with specs" }, + "quantity": { "type": "integer", "description": "Quantity", "default": 1 }, + "price": { "type": "number", "description": "Unit price" }, + "image": { "type": "string", "description": "Product image URL" } + }, + "required": ["name", "price"] + } + }, + "subtotal": { "type": "number", "description": "Subtotal before tax/fees" }, + "tax": { "type": "number", "description": "Tax amount", "default": 0 }, + "discount": { "type": "number", "description": "Discount amount", "default": 0 }, + "total": { "type": "number", "description": "Final total" }, + "currency": { "type": "string", "description": "Currency symbol", "default": "$" } + }, + "required": ["order_id", "items", "total"] + }, + "payment": { + "type": "object", + "description": "Payment configuration", + "properties": { + "method": { + "type": "string", + "enum": ["button", "qrcode", "link"], + "description": "Payment UI type: button (confirm button), qrcode (QR code image), link (external payment URL)" + }, + "qrcode_url": { "type": "string", "description": "QR code image URL (when method=qrcode)" }, + "payment_url": { "type": "string", "description": "External payment URL (when method=link)" }, + "button_text": { "type": "string", "description": "Payment button text, default: 'Confirm Payment'", "default": "Confirm Payment" } + }, + "required": ["method"] + } + }, + "required": ["title", "order", "payment"] + }, + "_meta": { + "ui": { + "resourceUri": "ui://ecommerce-storefront/order-confirm" + } + } + } +] diff --git a/skills/developing/ecommerce-storefront/hooks/ecommerce_guide.md b/skills/developing/ecommerce-storefront/hooks/ecommerce_guide.md new file mode 100644 index 0000000..609f5e8 --- /dev/null +++ b/skills/developing/ecommerce-storefront/hooks/ecommerce_guide.md @@ -0,0 +1,102 @@ +## E-Commerce Storefront Tools Usage Guide + +Two tools are available for e-commerce product browsing and ordering: +- `render_product_list` — Display interactive product cards with spec selection +- `render_order_confirm` — Display order confirmation with payment options + +--- + +### 1. render_product_list + +When to use: User wants to browse, search, or buy products. Show product cards with images, prices, and selectable specs (size, flavor, color, etc.). + +The user can select specs and click "Add to Cart" on any product. The selection is returned as an mcp-app-response containing: `{ product_id, product_name, selected_specs: {...}, final_price, quantity }`. + +``` +render_product_list( + title="Coffee Menu", + products=[ + { + "id": "latte-001", + "name": "Caffe Latte", + "description": "Rich espresso with steamed milk", + "image": "https://images.unsplash.com/photo-1572442388796-11668a67e53d?w=300&h=200&fit=crop", + "price": 4.50, + "currency": "$", + "specs": [ + { + "label": "Size", + "options": [ + {"name": "Small", "price_delta": 0}, + {"name": "Medium", "price_delta": 0.5}, + {"name": "Large", "price_delta": 1.0} + ] + }, + { + "label": "Milk", + "options": [ + {"name": "Whole Milk", "price_delta": 0}, + {"name": "Oat Milk", "price_delta": 0.6}, + {"name": "Almond Milk", "price_delta": 0.6} + ] + } + ], + "tags": ["Hot", "Popular"] + } + ] +) +``` + +After receiving the user's selection, proceed to generate an order and call `render_order_confirm`. + +--- + +### 2. render_order_confirm + +When to use: After the user selects a product and you have generated an order. + +Payment method options: +- `"button"` — Simple confirm/cancel buttons (default) +- `"qrcode"` — Show a QR code image for mobile payment +- `"link"` — Provide an external payment URL + +``` +render_order_confirm( + title="Order Confirmation", + order={ + "order_id": "ORD-20260522-001", + "items": [ + {"name": "Caffe Latte (Large, Oat Milk)", "quantity": 1, "price": 6.10} + ], + "subtotal": 6.10, + "tax": 0.49, + "total": 6.59, + "currency": "$" + }, + payment={ + "method": "button", + "button_text": "Confirm Payment" + } +) +``` + +The user response will be: `{ action: "confirm", order_id }` or `{ action: "cancel", order_id }`. + +--- + +### Workflow + +The typical e-commerce flow is: +1. User asks to buy something → call `render_product_list` +2. User selects a product → receive mcp-app-response with selection details +3. Generate order from selection → call `render_order_confirm` +4. User confirms or cancels → receive mcp-app-response with action +5. If confirmed → process payment / show success message +6. If cancelled → ask user what they'd like to do next + +### Tips +- Always include product images when available for better visual experience +- Use tags like "Hot", "New", "Sale" to highlight special products +- Keep product descriptions short (under 50 characters) +- Generate a unique order_id for each order +- Include tax/discount breakdowns when applicable for transparency diff --git a/skills/developing/ecommerce-storefront/hooks/pre_prompt.py b/skills/developing/ecommerce-storefront/hooks/pre_prompt.py new file mode 100644 index 0000000..9703836 --- /dev/null +++ b/skills/developing/ecommerce-storefront/hooks/pre_prompt.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 +"""PrePrompt hook - inject ecommerce tool guide into system prompt.""" +import os + +guide_path = os.path.join(os.path.dirname(__file__), "ecommerce_guide.md") +if os.path.exists(guide_path): + with open(guide_path, "r", encoding="utf-8") as f: + print(f.read()) diff --git a/skills/developing/ecommerce-storefront/mcp_common.py b/skills/developing/ecommerce-storefront/mcp_common.py new file mode 100644 index 0000000..0baeb01 --- /dev/null +++ b/skills/developing/ecommerce-storefront/mcp_common.py @@ -0,0 +1,252 @@ +#!/usr/bin/env python3 +""" +Shared utility functions for the MCP server. +Provides common functionality for path handling, file validation, and request processing. +""" + +import json +import os +import sys +import asyncio +from typing import Any, Dict, List, Optional, Union +import re + +def get_allowed_directory(): + """Get the directory that is allowed to be accessed.""" + # Prefer dataset_dir passed through command-line arguments. + if len(sys.argv) > 1: + dataset_dir = sys.argv[1] + return os.path.abspath(dataset_dir) + + # Read the project data directory from the environment variable. + project_dir = os.getenv("PROJECT_DATA_DIR", "./projects/data") + return os.path.abspath(project_dir) + + +def resolve_file_path(file_path: str, default_subfolder: str = "default") -> str: + """ + Resolve a file path, supporting both folder/document.txt and document.txt formats. + + Args: + file_path: Input file path. + default_subfolder: Default subfolder name to use when only a filename is provided. + + Returns: + The resolved full file path. + """ + # If the path contains a folder separator, use it directly. + if '/' in file_path or '\\' in file_path: + clean_path = file_path.replace('\\', '/') + + # Remove the projects/ prefix if it exists. + if clean_path.startswith('projects/'): + clean_path = clean_path[9:] # Remove the 'projects/' prefix. + elif clean_path.startswith('./projects/'): + clean_path = clean_path[11:] # Remove the './projects/' prefix. + else: + # If only a filename is provided, add the default subfolder. + clean_path = f"{default_subfolder}/{file_path}" + + # Get the allowed directory. + project_data_dir = get_allowed_directory() + + # Try to locate the file directly under the project directory. + full_path = os.path.join(project_data_dir, clean_path.lstrip('./')) + if os.path.exists(full_path): + return full_path + + # If the direct path does not exist, try a recursive search. + found = find_file_in_project(clean_path, project_data_dir) + if found: + return found + + # If this is a bare filename and it was not found under the default subfolder, + # try looking in the project root. + if '/' not in file_path and '\\' not in file_path: + root_path = os.path.join(project_data_dir, file_path) + if os.path.exists(root_path): + return root_path + + raise FileNotFoundError(f"File not found: {file_path} (searched in {project_data_dir})") + + +def find_file_in_project(filename: str, project_dir: str) -> Optional[str]: + """Recursively search for a file inside the project directory.""" + # If filename includes a path, only search within the specified path. + if '/' in filename: + parts = filename.split('/') + target_file = parts[-1] + search_dir = os.path.join(project_dir, *parts[:-1]) + + if os.path.exists(search_dir): + target_path = os.path.join(search_dir, target_file) + if os.path.exists(target_path): + return target_path + else: + # For a bare filename, recursively search the whole project directory. + for root, dirs, files in os.walk(project_dir): + if filename in files: + return os.path.join(root, filename) + return None + + +def load_tools_from_json(tools_file_name: str) -> List[Dict[str, Any]]: + """Load tool definitions from a JSON file.""" + try: + tools_file = os.path.join(os.path.dirname(__file__), tools_file_name) + if os.path.exists(tools_file): + with open(tools_file, 'r', encoding='utf-8') as f: + return json.load(f) + else: + # If the JSON file does not exist, use the default definitions. + return [] + except Exception as e: + print(f"Warning: Unable to load tool definition JSON file: {str(e)}") + return [] + + +def create_error_response(request_id: Any, code: int, message: str) -> Dict[str, Any]: + """Create a standardized error response.""" + return { + "jsonrpc": "2.0", + "id": request_id, + "error": { + "code": code, + "message": message + } + } + + +def create_success_response(request_id: Any, result: Any) -> Dict[str, Any]: + """Create a standardized success response.""" + return { + "jsonrpc": "2.0", + "id": request_id, + "result": result + } + + +def create_initialize_response(request_id: Any, server_name: str, server_version: str = "1.0.0") -> Dict[str, Any]: + """Create a standardized initialize response.""" + return { + "jsonrpc": "2.0", + "id": request_id, + "result": { + "protocolVersion": "2024-11-05", + "capabilities": { + "tools": {} + }, + "serverInfo": { + "name": server_name, + "version": server_version + } + } + } + + +def create_ping_response(request_id: Any) -> Dict[str, Any]: + """Create a standardized ping response.""" + return { + "jsonrpc": "2.0", + "id": request_id, + "result": { + "pong": True + } + } + + +def create_tools_list_response(request_id: Any, tools: List[Dict[str, Any]]) -> Dict[str, Any]: + """Create a standardized tools/list response.""" + return { + "jsonrpc": "2.0", + "id": request_id, + "result": { + "tools": tools + } + } + + +def is_regex_pattern(pattern: str) -> bool: + """Check whether a string should be treated as a regular expression pattern.""" + # Check the /pattern/ format. + if pattern.startswith('/') and pattern.endswith('/') and len(pattern) > 2: + return True + + # Check the r"pattern" or r'pattern' format. + if pattern.startswith(('r"', "r'")) and pattern.endswith(('"', "'")) and len(pattern) > 3: + return True + + # Check whether it contains regex metacharacters. + regex_chars = {'*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '^', '$', '\\', '.'} + return any(char in pattern for char in regex_chars) + + +def compile_pattern(pattern: str) -> Union[re.Pattern, str, None]: + """Compile a regex pattern, or return the original string if it is not regex.""" + if not is_regex_pattern(pattern): + return pattern + + try: + # Handle the /pattern/ format. + if pattern.startswith('/') and pattern.endswith('/'): + regex_body = pattern[1:-1] + return re.compile(regex_body) + + # Handle the r"pattern" or r'pattern' format. + if pattern.startswith(('r"', "r'")) and pattern.endswith(('"', "'")): + regex_body = pattern[2:-1] + return re.compile(regex_body) + + # Directly compile strings that contain regex metacharacters. + return re.compile(pattern) + except re.error as e: + # If compilation fails, return None to indicate an invalid regex. + print(f"Warning: Regular expression '{pattern}' compilation failed: {e}") + return None + + +async def handle_mcp_streaming(request_handler): + """Handle the standard main loop for MCP requests.""" + try: + while True: + # Read from stdin + line = await asyncio.get_event_loop().run_in_executor(None, sys.stdin.readline) + if not line: + break + + line = line.strip() + if not line: + continue + + try: + request = json.loads(line) + response = await request_handler(request) + + # Write to stdout + sys.stdout.write(json.dumps(response, ensure_ascii=False) + "\n") + sys.stdout.flush() + + except json.JSONDecodeError: + error_response = { + "jsonrpc": "2.0", + "error": { + "code": -32700, + "message": "Parse error" + } + } + sys.stdout.write(json.dumps(error_response, ensure_ascii=False) + "\n") + sys.stdout.flush() + + except Exception as e: + error_response = { + "jsonrpc": "2.0", + "error": { + "code": -32603, + "message": f"Internal error: {str(e)}" + } + } + sys.stdout.write(json.dumps(error_response, ensure_ascii=False) + "\n") + sys.stdout.flush() + + except KeyboardInterrupt: + pass