Skip to content

Commit 4cb3bf2

Browse files
王璨claude
andcommitted
feat: add MDX MCP app rendering workflow
Add the MDX-based MCP app host/runtime, scenario-modeler example updates, tests, and root README examples documentation. This keeps server-provided HTML as the preferred path while enabling structuredContent-driven UI fallback. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent ce302a6 commit 4cb3bf2

29 files changed

Lines changed: 1797 additions & 162 deletions

File tree

README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,17 @@ Always push the branch before creating a PR.
306306
| `DSCODE_CONFIG_HOME` | 自定义配置目录(默认 `~/.dscode`|
307307
| `DSCODE_DATA_HOME` | 自定义数据目录(默认 `~/.dscode`|
308308

309+
## Examples
310+
311+
`examples/` 提供可直接运行的示例项目,用来演示 dscode 的 MCP 集成方式和交互式 UI 能力。
312+
313+
| 示例 | 说明 | 快速开始 |
314+
| --- | --- | --- |
315+
| `examples/scenario-modeler` | 一个 SaaS 场景建模 MCP Server。演示 tool 返回 `structuredContent` 后,dscode 如何渲染 MCP App;没有 server HTML 时走 MDX,有 HTML resource 时优先使用 server 自带页面。 | `cd examples/scenario-modeler && npm install && npm start` |
316+
317+
更多使用说明见:
318+
- `examples/scenario-modeler/README.md`
319+
309320
## Project structure
310321

311322
```text
@@ -316,7 +327,7 @@ src/
316327
├── memory/ # 跨 session 记忆
317328
├── drivers/ # 驱动注册 + 内置驱动 (fs, shell, search)
318329
├── skills/ # Skill 管理器 + SKILL.md 加载器
319-
├── mcp/ # MCP 客户端(stdio/SSE)+ 管理器
330+
├── mcp/ # MCP 客户端(stdio/SSE)+ 管理器 + MCP App host/runtime
320331
├── permissions/ # 权限拦截
321332
└── ui/ # REPL、流式渲染、slash commands
322333
```

examples/scenario-modeler/README.md

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,28 +18,32 @@ Server starts on http://localhost:3100/mcp
1818
/config mcp add scenario-modeler --url http://localhost:3100/mcp
1919
```
2020

21-
Then ask the agent: "Show me the SaaS scenario modeler"
21+
Then ask the agent: "Show me the current SaaS scenario projections."
2222

23-
Agent calls `get-scenario-data` → dscode fetches the UI HTML → TUI displays a localhost URL → open in browser.
23+
That phrasing is more natural, but still points the agent toward the `get-scenario-data` MCP tool because it asks for live scenario output rather than code analysis or setup help.
24+
25+
Agent calls `get-scenario-data` → dscode renders the MCP App → TUI highlights the localhost link → open it in your browser.
2426

2527
## How it works
2628

2729
- **server.ts** — Standard MCP server using `@modelcontextprotocol/sdk`. Registers:
2830
- `get-scenario-data` tool with `_meta.ui.resourceUri = "ui://scenario-modeler/mcp-app"`
29-
- `ui://scenario-modeler/mcp-app` resource returning `mcp-app.html`
30-
- **mcp-app.html** — Single-file interactive View. Zero external dependencies.
31-
- MCP protocol handshake via postMessage
32-
- Canvas API for projection chart
33-
- DOM API for sliders and metric cards
34-
- Local calculation for instant feedback
31+
- Returns `structuredContent` with templates, projections, and summary data
32+
- Includes `_ui.mdx` to demonstrate a custom MDX layout override
33+
- **No HTML required** — dscode renders the dashboard from data + MDX
34+
- **Auto-generated UI** — dscode inspects `structuredContent` and renders:
35+
- Chart from projection arrays (line chart with MRR/netProfit curves)
36+
- Metrics cards from summary key-value pairs
37+
- Table from template/projection data
38+
- **No external dependencies** — UI is rendered by dscode's built-in MDX Runtime
3539

3640
## Features
3741

38-
- 5 sliders: Starting MRR, Growth Rate, Churn Rate, Gross Margin, Fixed Costs
39-
- 12-month line chart (MRR, Gross Profit, Net Profit)
42+
- 12-month line chart (MRR, Gross Profit, Net Profit) — auto-generated from data
43+
- Metric cards showing ending MRR, ARR, total revenue, profit, growth %, break-even
4044
- 5 pre-built templates (Bootstrapped, VC Rocketship, Cash Cow, Turnaround, Efficient Growth)
41-
- Template comparison with dashed overlay lines
42-
- Light/dark theme support
45+
- Custom projection computation via tool arguments
46+
- Light/dark theme support (via CSS custom properties)
4347

4448
## Transport
4549

examples/scenario-modeler/server.ts

Lines changed: 89 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js
1010
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
1111
import cors from "cors";
1212
import { z } from "zod";
13-
import fs from "node:fs";
14-
import path from "node:path";
1513

1614
// ============================================================================
1715
// Types & Business Logic (adapted from ext-apps scenario-modeler)
@@ -88,8 +86,60 @@ const TEMPLATES: ScenarioTemplate[] = [
8886

8987
const DEFAULT: ScenarioInputs = { startingMRR: 50000, monthlyGrowthRate: 5, monthlyChurnRate: 3, grossMargin: 80, fixedCosts: 30000 };
9088

89+
const DEFAULT_PROJECTIONS = calculateProjections(DEFAULT);
90+
const DEFAULT_SUMMARY = calculateSummary(DEFAULT_PROJECTIONS, DEFAULT);
91+
9192
const URI = "ui://scenario-modeler/mcp-app";
92-
const MT = "text/html;profile=mcp-app";
93+
94+
const SCENARIO_MODELER_MDX = `
95+
<Card title="SaaS Scenario Modeler">
96+
<Card title="Current Scenario Inputs">
97+
<Metrics items={inputMetrics}/>
98+
</Card>
99+
<Card title="12-Month Projection">
100+
<Chart type="line" data={activeProjections} x="month" y={["mrr","grossProfit","netProfit"]}/>
101+
<Metrics items={summaryMetrics}/>
102+
<Table rows={activeProjections}/>
103+
</Card>
104+
<Card title="Scenario Templates">
105+
<Table rows={templateRows}/>
106+
</Card>
107+
</Card>
108+
`.trim();
109+
110+
function formatTemplateRows(templates: ScenarioTemplate[]) {
111+
return templates.map((template) => ({
112+
icon: template.icon,
113+
name: template.name,
114+
description: template.description,
115+
keyInsight: template.keyInsight,
116+
breakEven: template.summary.breakEvenMonth ? `Month ${template.summary.breakEvenMonth}` : "Not achieved",
117+
endingMRR: fmt(template.summary.endingMRR),
118+
arr: fmt(template.summary.arr),
119+
}));
120+
}
121+
122+
function formatInputsMetrics(inputs: ScenarioInputs) {
123+
return {
124+
startingMRR: fmt(inputs.startingMRR),
125+
monthlyGrowthRate: `${inputs.monthlyGrowthRate}%`,
126+
monthlyChurnRate: `${inputs.monthlyChurnRate}%`,
127+
grossMargin: `${inputs.grossMargin}%`,
128+
fixedCosts: fmt(inputs.fixedCosts),
129+
};
130+
}
131+
132+
function formatSummaryMetrics(summary: ScenarioSummary) {
133+
return {
134+
endingMRR: fmt(summary.endingMRR),
135+
arr: fmt(summary.arr),
136+
totalRevenue: fmt(summary.totalRevenue),
137+
totalProfit: fmt(summary.totalProfit),
138+
mrrGrowth: `${summary.mrrGrowthPct}%`,
139+
avgMargin: `${summary.avgMargin}%`,
140+
breakEven: summary.breakEvenMonth ? `Month ${summary.breakEvenMonth}` : "Not achieved",
141+
};
142+
}
93143

94144
// ============================================================================
95145
// MCP Server Registration (standard SDK — no ext-apps wrapper)
@@ -101,33 +151,52 @@ function createServer(): McpServer {
101151
s.registerTool("get-scenario-data",
102152
{
103153
title: "Get Scenario Data",
104-
description: "Get SaaS financial scenario templates and optionally compute custom 12-month projections. Returns data for the interactive dashboard.",
154+
description: "Get SaaS financial scenario templates and optionally compute custom 12-month projections. Returns data for the interactive dashboard. Call with customInputs omitted (or empty object) to get templates with defaults.",
105155
inputSchema: {
106156
customInputs: z.object({
107-
startingMRR: z.number().describe("Starting MRR ($)"),
108-
monthlyGrowthRate: z.number().describe("Growth rate (%)"),
109-
monthlyChurnRate: z.number().describe("Churn rate (%)"),
110-
grossMargin: z.number().describe("Gross margin (%)"),
111-
fixedCosts: z.number().describe("Fixed costs ($)"),
112-
}).optional().describe("Custom scenario parameters to compute projections for"),
157+
startingMRR: z.number().optional().describe("Starting MRR ($)"),
158+
monthlyGrowthRate: z.number().optional().describe("Growth rate (%)"),
159+
monthlyChurnRate: z.number().optional().describe("Churn rate (%)"),
160+
grossMargin: z.number().optional().describe("Gross margin (%)"),
161+
fixedCosts: z.number().optional().describe("Fixed costs ($)"),
162+
}).optional().describe("Custom scenario parameters. Omit all fields to use defaults."),
113163
},
114164
_meta: { ui: { resourceUri: URI, visibility: ["model", "app"] } } as any,
115165
},
116-
async (args: { customInputs?: ScenarioInputs }) => {
117-
const custom = args.customInputs;
118-
const cp = custom ? calculateProjections(custom) : undefined;
119-
const cs = cp ? calculateSummary(cp, custom!) : undefined;
166+
async (args: { customInputs?: Partial<ScenarioInputs> }) => {
167+
const raw = args.customInputs;
168+
const custom: ScenarioInputs | undefined = raw
169+
? {
170+
startingMRR: raw.startingMRR ?? DEFAULT.startingMRR,
171+
monthlyGrowthRate: raw.monthlyGrowthRate ?? DEFAULT.monthlyGrowthRate,
172+
monthlyChurnRate: raw.monthlyChurnRate ?? DEFAULT.monthlyChurnRate,
173+
grossMargin: raw.grossMargin ?? DEFAULT.grossMargin,
174+
fixedCosts: raw.fixedCosts ?? DEFAULT.fixedCosts,
175+
}
176+
: undefined;
177+
const activeInputs = custom ?? DEFAULT;
178+
const activeProjections = custom ? calculateProjections(custom) : DEFAULT_PROJECTIONS;
179+
const activeSummary = custom ? calculateSummary(activeProjections, activeInputs) : DEFAULT_SUMMARY;
120180
const lines = ["SaaS Scenario Modeler", "=".repeat(40), "", "Templates:"];
121181
for (const t of TEMPLATES) lines.push(` ${t.icon} ${t.name}: ${t.description}`);
122-
if (cs) lines.push("", "Custom:", ` Ending MRR: ${fmt(cs.endingMRR)}`, ` ARR: ${fmt(cs.arr)}`);
123-
return { content: [{ type: "text", text: lines.join("\n") }], structuredContent: { templates: TEMPLATES, defaultInputs: DEFAULT, customProjections: cp, customSummary: cs } };
182+
if (custom) lines.push("", "Custom:", ` Ending MRR: ${fmt(activeSummary.endingMRR)}`, ` ARR: ${fmt(activeSummary.arr)}`);
183+
return {
184+
content: [{ type: "text", text: lines.join("\n") }],
185+
structuredContent: {
186+
_ui: { mdx: SCENARIO_MODELER_MDX },
187+
templateRows: formatTemplateRows(TEMPLATES),
188+
inputMetrics: formatInputsMetrics(activeInputs),
189+
summaryMetrics: formatSummaryMetrics(activeSummary),
190+
activeProjections,
191+
defaultInputs: DEFAULT,
192+
customInputs: custom ?? null,
193+
},
194+
};
124195
},
125196
);
126197

127-
s.resource(URI, URI, { mimeType: MT, description: "Scenario Modeler UI" }, async () => {
128-
const html = fs.readFileSync(path.join(import.meta.dirname, "mcp-app.html"), "utf-8");
129-
return { contents: [{ uri: URI, mimeType: MT, text: html }] };
130-
});
198+
// No s.resource() call — dscode renders this example from structuredContent.
199+
// The example includes a server-provided _ui.mdx override to avoid nested object cells.
131200

132201
return s;
133202
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: spec-driven
2+
created: 2026-05-16
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
## Context
2+
3+
Current MCP App flow requires Server to provide `mcp-app.html` via `resources/read`. dscode's `AppHostManager` fetches it, registers an app, and serves it through a sandbox proxy. This works but forces every Server developer to write HTML — even for simple data-display tools.
4+
5+
The proposal introduces a browser-side MDX Runtime that auto-generates UIs from `structuredContent`, eliminating the Server HTML requirement. Server can still provide HTML (backward compatible) or opt into auto-layout with optional MDX overrides.
6+
7+
## Goals / Non-Goals
8+
9+
**Goals:**
10+
1. Auto-generate interactive UIs from `structuredContent` without Server-provided HTML
11+
2. Ship a lightweight MDX Runtime (Chart, Metrics, Table, Slider, Card, Row) in the sandbox
12+
3. Support Server-provided MDX overrides (`_ui.mdx`) for custom layouts
13+
4. Backward compatible — existing `mcp-app.html` Servers continue working
14+
5. example `scenario-modeler` works with zero HTML
15+
16+
**Non-Goals:**
17+
- No React/Vue/JS framework dependency
18+
- No LLM involvement in UI generation (deterministic, fast)
19+
- No build step for the MDX Runtime (plain JS, concatenated into sandbox)
20+
- No drag-and-drop or WYSIWYG editing
21+
- Not replacing the sandbox architecture (postMessage ↔ bridge remains)
22+
23+
## Decisions
24+
25+
### 1. MDX Runtime: Hand-written parser, not React MDX
26+
27+
**Choice**: Write a ~200-line tag parser that recognizes `<Chart/>`, `<Metrics/>`, etc., with inline JSON data binding. No JSX compilation.
28+
29+
```
30+
Input: `<Chart type="line" data={projections} x="month" y={["mrr","netProfit"]}/>`
31+
Parser: match <Chart ... /> → extract props → resolve data bindings
32+
Output: Canvas element with bound data
33+
```
34+
35+
**Alternative**: Full MDX/React compiler (esbuild + MDX plugin). Too heavy for a single-file sandbox runtime.
36+
37+
**Rationale**: The component surface is tiny (6 components), data is pre-loaded JSON, event model is minimal. A simple regex-based parser suffices.
38+
39+
### 2. MDX Runtime delivery: Injected into sandbox.html
40+
41+
**Choice**: Bundle the MDX Runtime (parser + 6 components) as a single JS string, inject into `sandbox.html` at serve time. The sandbox page detects data mode vs HTML mode.
42+
43+
```
44+
sandbox.html (template with injection point)
45+
├── <style> ... </style>
46+
├── <script>/* MDX_RUNTIME_PLACEHOLDER */</script> ← injected at serve time
47+
└── <script>
48+
if (dataMode) {
49+
parseMDX(layout, data);
50+
renderComponents();
51+
} else {
52+
// legacy: injectApp() with srcdoc
53+
}
54+
</script>
55+
```
56+
57+
**Alternative**: Serve MDX Runtime as a separate .js file. Adds a network request, CSP complexity.
58+
59+
**Rationale**: Single HTML response, zero extra requests. CSP stays simple (`connect-src 'self'`). Works when the page is a static file too (dev mode).
60+
61+
### 3. Auto-layout inference: heuristic rules
62+
63+
**Choice**: Inspect `structuredContent` shape and apply priority-ordered rules:
64+
65+
```
66+
Rule 1: { x: number[], y: number[] } or array of { label, value } → Metrics cards
67+
Rule 2: Array of objects with numeric fields → Table
68+
Rule 3: Array of objects where one field is sequential (month, date) and others numeric → Chart + Table
69+
Rule 4: Object with nested arrays and numeric summaries → Chart + Metrics + Table combo
70+
Rule 5: Flat key-value → Cards
71+
```
72+
73+
**Alternative**: Machine learning model. Overkill, unpredictable, no training data.
74+
75+
**Rationale**: 80% of tool results fall into these patterns. Simple heuristics cover the common cases.
76+
77+
### 4. Server MDX override: `_ui.mdx` in structuredContent
78+
79+
**Choice**: If `structuredContent._ui.mdx` is a string, use it as the layout. Data bindings reference sibling keys in `structuredContent`.
80+
81+
```json
82+
{
83+
"structuredContent": {
84+
"templates": [...],
85+
"projections": [...],
86+
"summary": {...},
87+
"_ui": {
88+
"mdx": "<Metrics data={summary}/>\n<Chart data={projections} x=\"month\" y={[\"mrr\",\"netProfit\"]}/>",
89+
"bindings": {}
90+
}
91+
}
92+
}
93+
```
94+
95+
**Alternative**: Namespace the data under a `data` key. But current tools return data at the top level.
96+
97+
**Rationale**: Minimal Server-side change — just add a string. No new protocol fields needed (SDK doesn't need updating).
98+
99+
### 5. Backward compatibility: Phase detection
100+
101+
**Choice**: `checkAndRegisterApp` detects whether the tool has a UI resource:
102+
103+
```
104+
if (resourceUri) {
105+
fetchUiResource(html) → legacy mode (srcdoc)
106+
} else if (result.structuredContent) {
107+
generateMDXLayout(data) → data mode (MDX Runtime)
108+
} else {
109+
skip (text-only result)
110+
}
111+
```
112+
113+
**Rationale**: No breaking change. Existing Servers with `s.resource()` continue working. New Servers just omit the resource and get auto-layout.
114+
115+
### 6. Component Canvas: Canvas 2D (same as current)
116+
117+
**Choice**: Chart component uses Canvas 2D API, same approach as current `mcp-app.html`.
118+
119+
**Alternative**: SVG. More DOM elements, harder to animate, no real advantage for simple line/bar charts.
120+
121+
**Rationale**: Proven, already works in sandbox, zero dependencies.
122+
123+
## Risks / Trade-offs
124+
125+
| Risk | Mitigation |
126+
|------|------------|
127+
| Auto-layout produces wrong chart type | Server can override with `_ui.mdx` |
128+
| MDX parser too simple for edge cases | Component surface is tiny; escape to raw HTML is always available |
129+
| Runtime JS bundle increases page size | MDX Runtime is ~15KB uncompressed (~5KB gzipped), acceptable for a single-page tool |
130+
| Inference heuristics miss important data patterns | Add rules incrementally; scope to common SaaS/data patterns first |
131+
132+
## Open Questions
133+
134+
- Should the MDX Runtime support `<Form/>` for input-bound tools? (defer to future change)
135+
- Should we support themes (light/dark)? (yes, reuse sandbox CSS variables)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
## Why
2+
3+
Currently MCP App Views require MCP Server developers to hand-write a complete HTML file (`mcp-app.html`) for each tool. This creates unnecessary friction: a simple data-returning tool needs a whole frontend page just to display results. dscode should dynamically generate interactive UIs from tool result data, eliminating the need for Server-provided HTML in the common case.
4+
5+
## What Changes
6+
7+
- **Built-in MDX renderer**: dscode ships a lightweight MDX runtime component system (`<Chart/>`, `<Metrics/>`, `<Table/>`, `<Slider/>`, `<Card/>`, `<Row/>`) for rendering MCP tool results
8+
- **Auto-layout inference**: When a tool returns `structuredContent` without an explicit UI resource, dscode inspects the data structure and generates a default layout using built-in components
9+
- **Optional MDX override**: Server can include an `_ui.mdx` string in `structuredContent` to override the auto-generated layout, giving full control without writing HTML
10+
- **No Server HTML required**: `mcp-app.html` becomes optional; `examples/scenario-modeler/server.ts` no longer needs `s.resource()` for UI
11+
- **All rendering runs in-browser**: MDX Runtime is injected into the sandbox page, parsed and rendered client-side for instant interactivity (sliders, chart updates)
12+
13+
## Capabilities
14+
15+
### New Capabilities
16+
17+
- `mdx-runtime`: Lightweight MDX-like parser and component renderer (Chart, Metrics, Table, Slider, Card, Row) running in the browser sandbox, capable of transforming structured data into interactive HTML
18+
- `auto-layout-inference`: Heuristic engine that inspects `structuredContent` shape and selects appropriate MDX components and layout (array → Table, numeric series → Chart, key-value pairs → Metrics cards)
19+
- `server-ui-override`: Server can optionally include `_ui.mdx` in `structuredContent` to provide explicit layout instructions, overriding auto-inference
20+
21+
### Modified Capabilities
22+
23+
- `mcp-app-sandbox-host`: Sandbox proxy page now includes the MDX Runtime JS bundle and supports both srcdoc (legacy HTML) and data-driven (MDX + data) rendering modes
24+
25+
## Impact
26+
27+
- **New files**: `src/ui/mdx/parser.ts` (MDX parser), `src/ui/mdx/components.ts` (built-in components), `src/ui/mdx/renderer.ts` (runtime orchestrator), `src/ui/mdx/inference.ts` (auto-layout engine)
28+
- **Modified**: `src/apps/host.ts` (generate MDX+data when no HTML resource), `src/apps/sandbox.html` (include MDX runtime, support data mode)
29+
- **Modified**: `examples/scenario-modeler/server.ts` (remove `s.resource()` and `mcp-app.html` dependency)
30+
- **Removed**: `examples/scenario-modeler/mcp-app.html` (replaced by auto-generated MDX)
31+
- **No new dependencies**: MDX parser is hand-written (~200 lines), no React/MDX libraries required

0 commit comments

Comments
 (0)