Skip to content

Commit 9b591ec

Browse files
王璨claude
andcommitted
feat: upgrade MCP runtime for 2025-11-25 compatibility
Prefer streamable HTTP, surface MCP runtime events and tool errors correctly, and extend schema/UI coverage so the upgraded MCP flow is testable end to end. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 88e5c5b commit 9b591ec

12 files changed

Lines changed: 2426 additions & 180 deletions

File tree

docs/mcp-spec-2025-11-25-review.md

Lines changed: 1152 additions & 0 deletions
Large diffs are not rendered by default.

src/core/config.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { homedir } from "node:os";
33
import { join, resolve } from "node:path";
44

55
import type { HarnessConfig, ThinkingLevel } from "./types.js";
6-
import type { MCPServerConfig } from "../mcp/types.js";
6+
import type { MCPProtocolVersion, MCPServerConfig, MCPTransport } from "../mcp/types.js";
7+
import { DEFAULT_MCP_PROTOCOL_VERSION } from "../mcp/types.js";
78

89

910
function dsConfigHome(): string {
@@ -77,6 +78,26 @@ export function saveUserProjectCwd(startupPath: string, cwd: string): void {
7778
});
7879
}
7980

81+
function normalizeTransport(rawTransport: unknown, hasCommand: boolean, hasUrl: boolean): MCPTransport {
82+
if (rawTransport === "stdio" || rawTransport === "sse" || rawTransport === "streamable-http") {
83+
return rawTransport;
84+
}
85+
if (rawTransport === "http") {
86+
return "streamable-http";
87+
}
88+
if (hasUrl && !hasCommand) {
89+
return "streamable-http";
90+
}
91+
return "stdio";
92+
}
93+
94+
function normalizeProtocolVersion(rawVersion: unknown): MCPProtocolVersion {
95+
if (rawVersion === "2024-11-05" || rawVersion === "2025-03-26" || rawVersion === "2025-11-25") {
96+
return rawVersion;
97+
}
98+
return DEFAULT_MCP_PROTOCOL_VERSION;
99+
}
100+
80101
export function loadConfig(): HarnessConfig {
81102
const startupPath = resolve(process.env.DSCODE_PROJECT_PATH ?? process.cwd());
82103
const configDir = dsConfigHome();
@@ -156,7 +177,7 @@ export function loadConfig(): HarnessConfig {
156177
const mcp: MCPServerConfig[] = mcpServersRaw.map((s: any) => {
157178
const hasCommand = typeof s.command === "string" && s.command.length > 0;
158179
const hasUrl = typeof s.url === "string" && s.url.length > 0;
159-
const transport = (s.transport ?? s.type ?? (hasUrl && !hasCommand ? "sse" : "stdio")) as "stdio" | "sse";
180+
const transport = normalizeTransport(s.transport ?? s.type, hasCommand, hasUrl);
160181

161182
return {
162183
name: s.name,
@@ -166,6 +187,11 @@ export function loadConfig(): HarnessConfig {
166187
args: s.args,
167188
url: s.url,
168189
env: s.env,
190+
headers: s.headers,
191+
preferredProtocolVersion: normalizeProtocolVersion(s.preferredProtocolVersion ?? s.protocolVersion),
192+
allowLegacySseFallback: s.allowLegacySseFallback !== false,
193+
requestTimeoutMs: typeof s.requestTimeoutMs === "number" ? s.requestTimeoutMs : undefined,
194+
connectTimeoutMs: typeof s.connectTimeoutMs === "number" ? s.connectTimeoutMs : undefined,
169195
};
170196
});
171197

@@ -202,4 +228,4 @@ export function loadConfig(): HarnessConfig {
202228
appHost: { enabled: true },
203229
};
204230

205-
}
231+
}

src/core/harness.ts

Lines changed: 138 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { makeDiscoveryDriver } from "../drivers/discovery.js";
1414
import { SkillManager } from "../skills/manager.js";
1515
import { PermissionManager } from "../permissions/manager.js";
1616
import { MCPManager } from "../mcp/manager.js";
17+
import type { MCPClientEvent } from "../mcp/types.js";
1718
import { AppHostManager } from "../mcp/app/host.js";
1819
import { inferLayout } from "../ui/mdx/inference.js";
1920
import { TuiApp } from "../ui/tui-app.js";
@@ -32,6 +33,7 @@ export class Harness {
3233
private config: HarnessConfig;
3334
private tui!: TuiApp;
3435
private baseSystemPrompt = "";
36+
private lastMcpProgress = new Map<string, { progress?: number; total?: number; message?: string }>();
3537

3638
constructor(config: HarnessConfig) {
3739
this.config = config;
@@ -146,6 +148,7 @@ export class Harness {
146148
this.mcpManager = new MCPManager(this.config.mcp);
147149
this.tui.setMcpManager(this.mcpManager);
148150
await this.mcpManager.initialize();
151+
this.mcpManager.onEvent((event) => this.handleMcpEvent(event));
149152
await this.mcpManager.registerDrivers(this.driverRegistry);
150153

151154
if (this.appHostManager) {
@@ -309,18 +312,45 @@ You can also load tools by exact name using \`select:\`: for example \`search_to
309312

310313
private registeredApps = new Map<string, string>();
311314

315+
private getToolResultDetails(toolResult: unknown): Record<string, unknown> | undefined {
316+
if (!toolResult || typeof toolResult !== "object") return undefined;
317+
const details = (toolResult as Record<string, unknown>).details;
318+
return details && typeof details === "object" ? details as Record<string, unknown> : undefined;
319+
}
320+
321+
private getToolPayload(toolResult: unknown): unknown {
322+
const details = this.getToolResultDetails(toolResult);
323+
return details?.mcpResult ?? toolResult;
324+
}
325+
326+
private getStructuredContent(toolResult: unknown): Record<string, unknown> | undefined {
327+
const payload = this.getToolPayload(toolResult);
328+
if (!payload || typeof payload !== "object") return undefined;
329+
const structuredContent = (payload as Record<string, unknown>).structuredContent;
330+
return structuredContent && typeof structuredContent === "object"
331+
? structuredContent as Record<string, unknown>
332+
: undefined;
333+
}
334+
335+
private getEffectiveToolError(toolResult: unknown, isError: boolean): boolean {
336+
if (isError) return true;
337+
const details = this.getToolResultDetails(toolResult);
338+
return Boolean(details?.error);
339+
}
340+
312341
private checkAndRegisterApp(toolName: string, toolResult?: unknown): void {
313342
if (!this.appHostManager || !this.mcpManager) return;
314343
const uiInfo = this.mcpManager.getUiToolMap().get(toolName);
315344
if (!uiInfo) return;
316345

346+
const payload = this.getToolPayload(toolResult);
317347
const existingAppId = this.registeredApps.get(toolName);
318348
if (existingAppId) {
319-
if (toolResult !== undefined) {
349+
if (payload !== undefined) {
320350
this.appHostManager.pushToApp(existingAppId, {
321351
jsonrpc: "2.0",
322352
method: "ui/notifications/tool-result",
323-
params: toolResult,
353+
params: payload,
324354
});
325355
}
326356
return;
@@ -339,11 +369,11 @@ You can also load tools by exact name using \`select:\`: for example \`search_to
339369
});
340370
this.registeredApps.set(toolName, app.id);
341371
this.tui.addAppNotification(app);
342-
if (toolResult !== undefined) {
372+
if (payload !== undefined) {
343373
this.appHostManager!.pushToApp(app.id, {
344374
jsonrpc: "2.0",
345375
method: "ui/notifications/tool-result",
346-
params: toolResult,
376+
params: payload,
347377
});
348378
}
349379
})
@@ -361,10 +391,11 @@ You can also load tools by exact name using \`select:\`: for example \`search_to
361391
): void {
362392
if (!this.appHostManager) return;
363393

364-
const result = toolResult as Record<string, unknown> | undefined;
365-
const structuredContent = result?.structuredContent as Record<string, unknown> | undefined;
394+
const payload = this.getToolPayload(toolResult);
395+
const structuredContent = this.getStructuredContent(toolResult);
396+
const result = payload as Record<string, unknown> | undefined;
366397

367-
if (!structuredContent || typeof structuredContent !== "object") {
398+
if (!structuredContent) {
368399
this.tui.addInfo(`[MDX] no structuredContent (keys: ${result ? Object.keys(result).join(",") : "null"})`);
369400
return;
370401
}
@@ -383,11 +414,11 @@ You can also load tools by exact name using \`select:\`: for example \`search_to
383414
this.registeredApps.set(toolName, app.id);
384415
this.tui.addAppNotification(app);
385416

386-
if (toolResult !== undefined) {
417+
if (payload !== undefined) {
387418
this.appHostManager!.pushToApp(app.id, {
388419
jsonrpc: "2.0",
389420
method: "ui/notifications/tool-result",
390-
params: structuredContent,
421+
params: payload,
391422
});
392423
}
393424
} catch (e: any) {
@@ -414,14 +445,17 @@ You can also load tools by exact name using \`select:\`: for example \`search_to
414445
case "tool_execution_start":
415446
this.tui.toolStart(event.toolName, event.args);
416447
break;
417-
case "tool_execution_end":
448+
case "tool_execution_end": {
449+
const payload = this.getToolPayload(event.result);
450+
const effectiveIsError = this.getEffectiveToolError(event.result, event.isError);
418451
this.tui.toolEnd(
419452
event.toolName,
420-
event.result,
421-
event.isError,
453+
payload,
454+
effectiveIsError,
422455
);
423456
this.checkAndRegisterApp(event.toolName, event.result);
424457
break;
458+
}
425459
}
426460
});
427461

@@ -449,4 +483,96 @@ You can also load tools by exact name using \`select:\`: for example \`search_to
449483
}
450484
});
451485
}
486+
487+
private handleMcpEvent(event: MCPClientEvent): void {
488+
switch (event.type) {
489+
case "progress": {
490+
const key = `${event.serverName}:${event.params.progressToken}`;
491+
const previous = this.lastMcpProgress.get(key);
492+
const next = {
493+
progress: event.params.progress,
494+
total: event.params.total,
495+
message: event.params.message,
496+
};
497+
this.lastMcpProgress.set(key, next);
498+
499+
const shouldReport = !previous
500+
|| event.params.message !== previous.message
501+
|| this.isProgressComplete(event.params.progress, event.params.total)
502+
|| this.progressBucket(event.params.progress, event.params.total) !== this.progressBucket(previous.progress, previous.total);
503+
504+
if (shouldReport) {
505+
const summary = this.formatProgress(event.params.progress, event.params.total);
506+
const detail = event.params.message ? ` ${event.params.message}` : "";
507+
this.tui.addInfo(`MCP ${event.serverName}: ${summary}${detail}`);
508+
}
509+
return;
510+
}
511+
case "message": {
512+
const level = (event.params.level ?? "info").toLowerCase();
513+
const prefix = `MCP ${event.serverName}`;
514+
const text = this.stringifyMcpMessage(event.params.data);
515+
const label = event.params.logger ? `${event.params.logger}: ` : "";
516+
if (level === "error") {
517+
this.tui.addError(`${prefix}: ${label}${text}`);
518+
} else if (level === "warning" || level === "warn") {
519+
this.tui.addInfo(`${prefix} warning: ${label}${text}`);
520+
} else {
521+
this.tui.addInfo(`${prefix}: ${label}${text}`);
522+
}
523+
return;
524+
}
525+
case "tools_list_changed":
526+
this.tui.addInfo(`MCP ${event.serverName}: refreshing tool list...`);
527+
return;
528+
case "tools_refreshed":
529+
this.tui.addInfo(`MCP ${event.serverName}: tool list refreshed (${event.toolCount} tools)`);
530+
return;
531+
case "tools_refresh_failed":
532+
this.tui.addError(`MCP ${event.serverName}: tool refresh failed: ${event.error}`);
533+
return;
534+
case "resources_list_changed":
535+
this.tui.addInfo(`MCP ${event.serverName}: resources updated`);
536+
return;
537+
case "cancelled":
538+
this.tui.addInfo(`MCP ${event.serverName}: request cancelled` + (event.params.reason ? ` (${event.params.reason})` : ""));
539+
return;
540+
default:
541+
return;
542+
}
543+
}
544+
545+
private progressBucket(progress?: number, total?: number): number {
546+
if (typeof progress !== "number") return -1;
547+
if (typeof total === "number" && total > 0) {
548+
return Math.min(10, Math.floor((progress / total) * 10));
549+
}
550+
return Math.floor(progress / 10);
551+
}
552+
553+
private isProgressComplete(progress?: number, total?: number): boolean {
554+
if (typeof progress !== "number") return false;
555+
if (typeof total === "number" && total > 0) {
556+
return progress >= total;
557+
}
558+
return progress >= 100;
559+
}
560+
561+
private formatProgress(progress?: number, total?: number): string {
562+
if (typeof progress !== "number") return "progress update";
563+
if (typeof total === "number" && total > 0) {
564+
const percent = Math.max(0, Math.min(100, Math.round((progress / total) * 100)));
565+
return `progress ${percent}% (${progress}/${total})`;
566+
}
567+
return `progress ${progress}`;
568+
}
569+
570+
private stringifyMcpMessage(data: unknown): string {
571+
if (typeof data === "string") return data;
572+
try {
573+
return JSON.stringify(data);
574+
} catch {
575+
return String(data);
576+
}
577+
}
452578
}

src/mcp/app/sandbox.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,9 @@
121121
var msg = JSON.parse(event.data);
122122
if (mode === 'data' && dataModeActive) {
123123
if (msg.method === 'ui/notifications/tool-result' && msg.params) {
124-
mdxData = msg.params;
124+
mdxData = (msg.params && typeof msg.params === 'object' && msg.params.structuredContent && typeof msg.params.structuredContent === 'object')
125+
? msg.params.structuredContent
126+
: msg.params;
125127
if (typeof renderMDX === 'function' && mdxSource) {
126128
renderMDX(mdxSource, mdxData, mdxContainer);
127129
}

0 commit comments

Comments
 (0)