Skip to content

Commit 97d9ccd

Browse files
王璨claude
andcommitted
fix: streamline scenario modeler startup
Run the scenario modeler example through the built dscode CLI so the MCP App demo works from one terminal, and shut down managed MCP processes cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 4cb3bf2 commit 97d9ccd

6 files changed

Lines changed: 42 additions & 26 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,8 @@ Always push the branch before creating a PR.
310310

311311
`examples/` 提供可直接运行的示例项目,用来演示 dscode 的 MCP 集成方式和交互式 UI 能力。
312312

313+
### MCP App
314+
313315
| 示例 | 说明 | 快速开始 |
314316
| --- | --- | --- |
315317
| `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` |

examples/scenario-modeler/README.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,25 @@ A SaaS financial scenario modeler MCP App. Interactive 12-month projections with
77
```bash
88
cd examples/scenario-modeler
99
npm install
10+
npm --prefix ../.. run build
1011
npm start
1112
```
1213

13-
Server starts on http://localhost:3100/mcp
14-
15-
## Connect to dscode
16-
17-
```
18-
/config mcp add scenario-modeler --url http://localhost:3100/mcp
19-
```
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.
2015

2116
Then ask the agent: "Show me the current SaaS scenario projections."
2217

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.
18+
If the agent replies with plain text only, ask it to use the `get-scenario-data` MCP tool explicitly.
2419

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

22+
## Alternate startup modes
23+
24+
```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
27+
```
28+
2729
## How it works
2830

2931
- **server.ts** — Standard MCP server using `@modelcontextprotocol/sdk`. Registers:

examples/scenario-modeler/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"type": "module",
66
"description": "SaaS Scenario Modeler MCP App — dscode example",
77
"scripts": {
8-
"start": "tsx server.ts",
8+
"start": "node ../../dist/dscode.mjs",
9+
"start:server": "tsx server.ts",
910
"start:stdio": "tsx server.ts --stdio",
1011
"demo": "tsx ../../src/core/main.ts",
1112
"demo:server": "tsx server.ts"

examples/scenario-modeler/server.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,8 @@ function createServer(): McpServer {
202202
}
203203

204204
async function startHttp() {
205-
const app = createMcpExpressApp({ host: "0.0.0.0" });
205+
const host = process.env.SCENARIO_MODELER_HOST ?? "127.0.0.1";
206+
const app = createMcpExpressApp({ host });
206207
app.use(cors());
207208
app.all("/mcp", async (req: any, res: any) => {
208209
const srv = createServer();
@@ -211,7 +212,7 @@ async function startHttp() {
211212
await srv.connect(t);
212213
await t.handleRequest(req, res, req.body);
213214
});
214-
const port = 3100;
215+
const port = Number(process.env.PORT ?? 3100);
215216
app.listen(port, () => console.log(`MCP Server → http://localhost:${port}/mcp`));
216217
}
217218

src/core/main.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,7 @@ async function main(): Promise<void> {
4545
process.exit(0);
4646
}
4747

48-
main();
48+
main().catch((err) => {
49+
console.error(err);
50+
process.exit(1);
51+
});

src/mcp/client.ts

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ export class MCPClient {
3434
await this.connectSSE();
3535
}
3636

37-
// send initialize with MCP Apps ui extension capability
3837
const result = await this.request("initialize", {
3938
protocolVersion: MCP_PROTOCOL_VERSION,
4039
capabilities: {
@@ -52,7 +51,6 @@ export class MCPClient {
5251
throw new Error(`MCP server "${this.config.name}" returned invalid initialize response`);
5352
}
5453

55-
// send initialized notification
5654
this.sendNotification("notifications/initialized");
5755
}
5856

@@ -88,21 +86,22 @@ export class MCPClient {
8886
if (this.closed) return;
8987
this.closed = true;
9088

91-
try {
92-
await this.request("shutdown", undefined, 5_000);
93-
} catch {
94-
// ignore shutdown errors
89+
if (this.config.transport === "stdio") {
90+
this.killProcessTree();
91+
} else {
92+
try {
93+
await this.request("shutdown", undefined, 1_000);
94+
} catch {
95+
}
9596
}
9697

97-
// clear pending
9898
for (const [, entry] of this.pending) {
9999
clearTimeout(entry.timer);
100100
entry.reject(new Error("MCP client closed"));
101101
}
102102
this.pending.clear();
103103

104104
if (this.process) {
105-
this.process.kill();
106105
this.process = null;
107106
}
108107
}
@@ -115,6 +114,7 @@ export class MCPClient {
115114
this.process = spawn(cmd, expandedArgs, {
116115
stdio: ["pipe", "pipe", "pipe"],
117116
env: { ...process.env, ...this.config.env },
117+
detached: process.platform !== "win32",
118118
});
119119

120120
// Suppress EPIPE errors on stdin when the child process exits unexpectedly
@@ -237,22 +237,19 @@ export class MCPClient {
237237
const dataMatch = line.match(/^data:\s*(.+)/);
238238

239239
if (eventMatch && eventMatch[1] === "endpoint") {
240-
// wait for the next data line with the actual endpoint URL
241240
continue;
242241
}
243242

244243
if (dataMatch) {
245244
try {
246245
const data = JSON.parse(dataMatch[1]);
247246
if (data.method === "endpoint") {
248-
// SSE endpoint for sending messages back
249247
this.sseUrl = data.params?.endpoint ?? null;
250248
resolve();
251249
} else {
252250
this.handleMessage(dataMatch[1]);
253251
}
254252
} catch {
255-
// not JSON, skip
256253
}
257254
}
258255
}
@@ -279,6 +276,18 @@ export class MCPClient {
279276
});
280277
}
281278

279+
private killProcessTree(): void {
280+
if (!this.process?.pid) return;
281+
try {
282+
if (process.platform !== "win32") {
283+
process.kill(-this.process.pid, "SIGTERM");
284+
} else {
285+
this.process.kill("SIGTERM");
286+
}
287+
} catch {
288+
}
289+
}
290+
282291
private handleMessage(raw: string): void {
283292
let msg: any;
284293
try {
@@ -329,7 +338,6 @@ export class MCPClient {
329338
if (this.config.transport === "stdio") {
330339
this.process?.stdin?.write(msg + "\n");
331340
} else if (this.sseUrl) {
332-
// POST to SSE endpoint
333341
const parsed = new URL(this.sseUrl, this.config.url);
334342
const requester = parsed.protocol === "https:" ? httpsRequest : httpRequest;
335343
const body = JSON.stringify({ jsonrpc: "2.0", id, method, params });
@@ -340,8 +348,7 @@ export class MCPClient {
340348
method: "POST",
341349
headers: { "Content-Type": "application/json" },
342350
},
343-
(res) => {
344-
// response comes via SSE stream, handled in handleMessage
351+
() => {
345352
},
346353
);
347354
req.on("error", (err) => {

0 commit comments

Comments
 (0)