Skip to content

Commit e920bce

Browse files
ochafikclaude
andcommitted
fix: E2E test fixes and host resilience improvements
1. video-resource-server: Add missing server-utils.ts and fix import path - Was importing from non-existent ../shared/server-utils.js - Now correctly imports from ./src/server-utils.js 2. basic-host: Be resilient to individual server connection failures - Use Promise.allSettled instead of Promise.all - Failed connections are logged as warnings but don't crash the UI - Only throws if ALL servers fail to connect 3. threejs-server: Add id to canvas for reliable screenshot masking - Added id="threejs-canvas" to the canvas element - Updated e2e test masks to use #threejs-canvas and .threejs-container - Fixes flaky Three.js screenshot comparisons in CI 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 299a442 commit e920bce

5 files changed

Lines changed: 134 additions & 3 deletions

File tree

examples/basic-host/src/index.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,27 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
350350
async function connectToAllServers(): Promise<ServerInfo[]> {
351351
const serverUrlsResponse = await fetch("/api/servers");
352352
const serverUrls = (await serverUrlsResponse.json()) as string[];
353-
return Promise.all(serverUrls.map((url) => connectToServer(new URL(url))));
353+
354+
// Use allSettled to be resilient to individual server failures
355+
const results = await Promise.allSettled(
356+
serverUrls.map((url) => connectToServer(new URL(url)))
357+
);
358+
359+
const servers: ServerInfo[] = [];
360+
for (let i = 0; i < results.length; i++) {
361+
const result = results[i];
362+
if (result.status === "fulfilled") {
363+
servers.push(result.value);
364+
} else {
365+
console.warn(`[HOST] Failed to connect to ${serverUrls[i]}:`, result.reason);
366+
}
367+
}
368+
369+
if (servers.length === 0 && serverUrls.length > 0) {
370+
throw new Error(`Failed to connect to any servers (${serverUrls.length} attempted)`);
371+
}
372+
373+
return servers;
354374
}
355375

356376
createRoot(document.getElementById("root")!).render(

examples/threejs-server/src/threejs-app.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ export default function ThreeJSApp({
172172
return (
173173
<div ref={containerRef} className="threejs-container">
174174
<canvas
175+
id="threejs-canvas"
175176
ref={canvasRef}
176177
style={{
177178
width: "100%",

examples/video-resource-server/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
RESOURCE_MIME_TYPE,
2323
RESOURCE_URI_META_KEY,
2424
} from "@modelcontextprotocol/ext-apps/server";
25-
import { startServer } from "../shared/server-utils.js";
25+
import { startServer } from "./src/server-utils.js";
2626

2727
const DIST_DIR = path.join(import.meta.dirname, "dist");
2828
const RESOURCE_URI = "ui://video-player/mcp-app.html";
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/**
2+
* Shared utilities for running MCP servers with various transports.
3+
*/
4+
5+
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
6+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
9+
import cors from "cors";
10+
import type { Request, Response } from "express";
11+
12+
/**
13+
* Starts an MCP server using the appropriate transport based on command-line arguments.
14+
*
15+
* If `--stdio` is passed, uses stdio transport. Otherwise, uses Streamable HTTP transport.
16+
*
17+
* @param createServer - Factory function that creates a new McpServer instance.
18+
*/
19+
export async function startServer(
20+
createServer: () => McpServer,
21+
): Promise<void> {
22+
try {
23+
if (process.argv.includes("--stdio")) {
24+
await startStdioServer(createServer);
25+
} else {
26+
await startStreamableHttpServer(createServer);
27+
}
28+
} catch (e) {
29+
console.error(e);
30+
process.exit(1);
31+
}
32+
}
33+
34+
/**
35+
* Starts an MCP server with stdio transport.
36+
*
37+
* @param createServer - Factory function that creates a new McpServer instance.
38+
*/
39+
export async function startStdioServer(
40+
createServer: () => McpServer,
41+
): Promise<void> {
42+
await createServer().connect(new StdioServerTransport());
43+
}
44+
45+
/**
46+
* Starts an MCP server with Streamable HTTP transport in stateless mode.
47+
*
48+
* Each request creates a fresh server and transport instance, which are
49+
* closed when the response ends (no session tracking).
50+
*
51+
* The server listens on the port specified by the PORT environment variable,
52+
* defaulting to 3001 if not set.
53+
*
54+
* @param createServer - Factory function that creates a new McpServer instance per request.
55+
*/
56+
export async function startStreamableHttpServer(
57+
createServer: () => McpServer,
58+
): Promise<void> {
59+
const port = parseInt(process.env.PORT ?? "3001", 10);
60+
61+
// Express app - bind to all interfaces for development/testing
62+
const expressApp = createMcpExpressApp({ host: "0.0.0.0" });
63+
expressApp.use(cors());
64+
65+
expressApp.all("/mcp", async (req: Request, res: Response) => {
66+
// Create fresh server and transport for each request (stateless mode)
67+
const server = createServer();
68+
const transport = new StreamableHTTPServerTransport({
69+
sessionIdGenerator: undefined,
70+
});
71+
72+
// Clean up when response ends
73+
res.on("close", () => {
74+
transport.close().catch(() => {});
75+
server.close().catch(() => {});
76+
});
77+
78+
try {
79+
await server.connect(transport);
80+
await transport.handleRequest(req, res, req.body);
81+
} catch (error) {
82+
console.error("MCP error:", error);
83+
if (!res.headersSent) {
84+
res.status(500).json({
85+
jsonrpc: "2.0",
86+
error: { code: -32603, message: "Internal server error" },
87+
id: null,
88+
});
89+
}
90+
}
91+
});
92+
93+
const { promise, resolve, reject } = Promise.withResolvers<void>();
94+
95+
const httpServer = expressApp.listen(port, (err?: Error) => {
96+
if (err) return reject(err);
97+
console.log(`Server listening on http://localhost:${port}/mcp`);
98+
resolve();
99+
});
100+
101+
const shutdown = () => {
102+
console.log("\nShutting down...");
103+
httpServer.close(() => process.exit(0));
104+
};
105+
106+
process.on("SIGINT", shutdown);
107+
process.on("SIGTERM", shutdown);
108+
109+
return promise;
110+
}

tests/e2e/servers.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const DYNAMIC_MASKS: Record<string, string[]> = {
1717
"#memory-bar-fill", // Memory bar fill level
1818
"#info-uptime", // System uptime
1919
],
20-
threejs: ["canvas"], // 3D render canvas (dynamic animation)
20+
threejs: ["#threejs-canvas", ".threejs-container"], // 3D render canvas (dynamic animation)
2121
"wiki-explorer": ["#graph"], // Force-directed graph (dynamic layout)
2222
};
2323

0 commit comments

Comments
 (0)