Skip to content

Commit d1facd4

Browse files
author
王璨
committed
feat(web): MCP App HTML inline rendering in web chat + port conflict handling
- Add mcp_app WebSocket event to stream MCP app URLs to web frontend - WebUiBackend proxies AppHostManager routes for same-origin iframe embedding - ToolCard renders expandable iframe when tool has MCP app - Friendly EADDRINUSE error message instead of crash - Fix mcp_app toolName mismatch (prefixed vs original name) - Register mcp-app.html as MCP resource in scenario-modeler example - Update README, AGENTS.md, example docs - Compress and add screen shots (web-ui.gif, mcp-app.gif)
1 parent 6db29f5 commit d1facd4

13 files changed

Lines changed: 306 additions & 69 deletions

File tree

AGENTS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ Agent Loop (pi-agent-core,已有)
4343
| `src/mcp/` | MCP 客户端(stdio/SSE)+ 管理器 | `client.ts`, `manager.ts`, `types.ts` |
4444
| `src/permissions/` | 权限拦截(deny/ask/allow) | `manager.ts`, `rules.ts` |
4545
| `src/ui/` | REPL、流式渲染、slash commands | `tui-app.ts`, `conversation.ts`, `commands.ts` |
46+
| `web/` | Web 前端(独立 Vite + React 项目) | `src/components/`, `src/hooks/`, `src/types/` |
4647

4748
## 内置驱动 (Drivers)
4849

@@ -54,6 +55,8 @@ Agent Loop (pi-agent-core,已有)
5455

5556
MCP 服务器连接后也会注册为驱动,source 为 `"mcp"`
5657

58+
`web/` 放在根目录而非 `src/` 下,因为它是独立的 Vite + React 项目,有自己的 `tsconfig.json``package.json``vite.config.ts`,不和 `src/` 共用 tsc 构建。构建产物输出到 `dist/web/`,由 dscode 的 HTTP server 直接 serve。
59+
5760
## 关键 Hook 接线
5861

5962
```typescript
@@ -84,6 +87,8 @@ new Agent({
8487

8588
```bash
8689
npm start # 本地开发:启动交互式 REPL
90+
npm start -- --web # Web 模式:浏览器中对话
91+
npm run build:web # 构建前端(npm start 前需先执行)
8792
npm run typecheck # 类型检查
8893
npm test # 运行测试
8994
```

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,13 @@ npm run build:web # 先构建前端
111111
node dist/dscode.mjs --web # 启动 Web 模式
112112
```
113113

114-
浏览器打开 `http://localhost:3000` 即可使用。Web UI 包含:
114+
浏览器打开 `http://localhost:3000` 即可使用:
115+
116+
<p align="center">
117+
<img src="docs/screen_shots/web-ui.gif" alt="DSCode Web UI" width="720" />
118+
</p>
119+
120+
Web UI 包含:
115121

116122
| 功能 | 说明 |
117123
| --- | --- |
@@ -367,6 +373,12 @@ Always push the branch before creating a PR.
367373
| --- | --- | --- |
368374
| `examples/scenario-modeler` | 一个 SaaS 场景建模 MCP Server。演示 tool 返回 `structuredContent` / `isError` 后,dscode 如何渲染 MCP App;没有 server HTML 时走 MDX,有 HTML resource 时优先使用 server 自带页面。 | `cd examples/scenario-modeler && npm install && npm start` |
369375

376+
<p align="center">
377+
<img src="docs/screen_shots/mcp-app.gif" alt="MCP App 演示" width="720" />
378+
</p>
379+
380+
> 上图演示了 scenario-modeler 在 Web UI 中内联展示 MCP App HTML —— 点击 "Open App ▼" 即可展开交互式 SaaS 财务建模仪表盘。
381+
370382
更多使用说明见:
371383
- `examples/scenario-modeler/README.md`
372384

docs/screen_shots/mcp-app.gif

1.23 MB
Loading

docs/screen_shots/web-ui.gif

2.49 MB
Loading

examples/scenario-modeler/README.md

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,52 +4,58 @@ A SaaS financial scenario modeler MCP App. Interactive 12-month projections with
44

55
## Quick Start
66

7+
### CLI Mode (TUI)
8+
79
```bash
810
cd examples/scenario-modeler
911
npm install
1012
npm --prefix ../.. run build
1113
npm start
1214
```
1315

14-
`npm start` launches the current repo build of dscode from this example directory. dscode then starts the `scenario-modeler` MCP server through the existing stdio MCP config, so you only need one terminal.
15-
16-
Then ask the agent: "Show me the current SaaS scenario projections."
17-
18-
If the agent replies with plain text only, ask it to use the `get-scenario-data` MCP tool explicitly.
16+
Then ask: "Show me the current SaaS scenario projections."
1917

2018
Agent calls `get-scenario-data` → dscode renders the MCP App → TUI highlights the localhost link → open it in your browser.
2119

22-
## Alternate startup modes
20+
### Web Mode
21+
22+
Run from the example directory so dscode picks up the local `.dscode/settings.json`:
2323

2424
```bash
25-
npm run start:server # start only the HTTP MCP server on localhost:3100
26-
npm run start:stdio # start only the stdio MCP server
25+
cd examples/scenario-modeler
26+
npm install
27+
npm --prefix ../.. run build
28+
npm --prefix ../.. run build:web
29+
npm start -- --web --web-port 3000
2730
```
2831

29-
## How it works
30-
31-
- **server.ts** — Standard MCP server using `@modelcontextprotocol/sdk`. Registers:
32-
- `get-scenario-data` tool with `_meta.ui.resourceUri = "ui://scenario-modeler/mcp-app"`
33-
- Returns `structuredContent` with templates, projections, and summary data
34-
- Includes `_ui.mdx` to demonstrate a custom MDX layout override
35-
- **No HTML required** — dscode renders the dashboard from data + MDX
36-
- **Auto-generated UI** — dscode inspects `structuredContent` and renders:
37-
- Chart from projection arrays (line chart with MRR/netProfit curves)
38-
- Metrics cards from summary key-value pairs
39-
- Table from template/projection data
40-
- **No external dependencies** — UI is rendered by dscode's built-in MDX Runtime
32+
> `npm start` runs `node ../../dist/dscode.mjs` from the current directory — no `--prefix` needed, so cwd stays as `examples/scenario-modeler` and the local MCP config is loaded.
4133
42-
## Features
34+
Open `http://localhost:3000`, then ask: "Show me the current SaaS scenario projections."
4335

44-
- 12-month line chart (MRR, Gross Profit, Net Profit) — auto-generated from data
45-
- Metric cards showing ending MRR, ARR, total revenue, profit, growth %, break-even
46-
- 5 pre-built templates (Bootstrapped, VC Rocketship, Cash Cow, Turnaround, Efficient Growth)
47-
- Custom projection computation via tool arguments
48-
- Light/dark theme support (via CSS custom properties)
36+
When the agent calls `get-scenario-data`, the MCP App renders **inline in the chat** — click **"Open App ▼"** to expand the interactive dashboard with sliders, chart, and templates.
4937

50-
## Transport
38+
## Alternate startup
5139

5240
```bash
53-
npm start # HTTP (default, port 3100)
54-
npm run start:stdio # stdio for direct MCP client connection
41+
npm run start:server # HTTP MCP server only (localhost:3100)
42+
npm run start:stdio # stdio MCP server only
5543
```
44+
45+
## How it works
46+
47+
- **server.ts** — MCP server using `@modelcontextprotocol/sdk`. Registers:
48+
- `get-scenario-data` tool with `_meta.ui.resourceUri = "ui://scenario-modeler/mcp-app"`
49+
- `mcp-app.html` as a UI resource (`text/html;profile=mcp-app`)
50+
- `structuredContent` + `_ui.mdx` for MDX auto-layout fallback
51+
- **mcp-app.html** — Pure JS dashboard with Canvas chart, 5 sliders, template comparison, postMessage bridge
52+
- **Web inline rendering** — iframe embedded in the tool card, communicating via SSE bridge
53+
54+
## Files
55+
56+
| File | Purpose |
57+
|------|---------|
58+
| `server.ts` | MCP server with tool + resource registration |
59+
| `mcp-app.html` | Interactive dashboard (served as MCP resource) |
60+
| `package.json` | Dependencies and scripts |
61+
| `.dscode/settings.json` | MCP config (loaded when running from this directory) |

examples/scenario-modeler/server.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
* Registration: server.tool() + server.resource() with _meta.ui
55
* No @modelcontextprotocol/ext-apps dependency.
66
*/
7+
import { readFileSync } from "node:fs";
8+
import { join, dirname } from "node:path";
9+
import { fileURLToPath } from "node:url";
710
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
811
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
912
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
@@ -195,7 +198,22 @@ function createServer(): McpServer {
195198
},
196199
);
197200

198-
// No s.resource() call — dscode renders this example from structuredContent.
201+
// No s.registerResource() call — dscode renders this example from structuredContent.
202+
203+
// Register UI resource — serves mcp-app.html as the MCP App UI
204+
const mcpAppHtml = readFileSync(join(dirname(fileURLToPath(import.meta.url)), "mcp-app.html"), "utf-8");
205+
s.registerResource(
206+
"Scenario Modeler App",
207+
URI,
208+
{
209+
mimeType: "text/html;profile=mcp-app",
210+
description: "Interactive SaaS financial scenario modeler",
211+
_meta: { ui: { domain: "mcp-app", prefersBorder: false } },
212+
},
213+
async () => ({
214+
contents: [{ uri: URI, mimeType: "text/html;profile=mcp-app", text: mcpAppHtml }],
215+
}),
216+
);
199217
// The example includes a server-provided _ui.mdx override to avoid nested object cells.
200218

201219
return s;

src/core/harness.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export class Harness {
3131
private skillManager: SkillManager;
3232
private permissionManager: PermissionManager;
3333
private mcpManager?: MCPManager;
34-
private appHostManager?: AppHostManager;
34+
public appHostManager?: AppHostManager;
3535
private config: HarnessConfig;
3636
private ui!: UiBackend;
3737
private baseSystemPrompt = "";

src/core/main.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ async function main(): Promise<void> {
7171
harness,
7272
config,
7373
});
74+
// Pass AppHostManager to web backend so MCP apps can be served
75+
if (harness.appHostManager) {
76+
webUi.setAppHostManager(harness.appHostManager);
77+
}
7478
await harness.run(webUi);
7579
} else {
7680
// CLI mode: default TuiBackend

src/ui/web/protocol.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ export interface ConfigData {
5252
maxTokens: number;
5353
}
5454

55+
export interface McpAppInfo {
56+
toolName: string;
57+
appUrl: string;
58+
resourceUri: string;
59+
}
60+
5561
export type ServerEvent =
5662
| { type: "ready"; model: string; config: ConfigData; messages: ConversationMessage[] }
5763
| { type: "user_message"; text: string }
@@ -69,7 +75,8 @@ export type ServerEvent =
6975
| { type: "sessions"; data: SessionInfo[] }
7076
| { type: "mcp_state"; servers: McpServerInfo[] }
7177
| { type: "model"; name: string }
72-
| { type: "slash_result"; text: string };
78+
| { type: "slash_result"; text: string }
79+
| { type: "mcp_app"; app: McpAppInfo };
7380

7481
export interface ConversationMessage {
7582
role: "user" | "assistant" | "system";
@@ -83,4 +90,5 @@ export interface ToolCallEntry {
8390
args: string;
8491
result: string;
8592
isError: boolean;
93+
mcpApp?: McpAppInfo;
8694
}

0 commit comments

Comments
 (0)