Skip to content

Commit 0b890bd

Browse files
ochafikclaude
andcommitted
fix(e2e): use factory pattern for MCP servers to support parallel connections
McpServer only supports one transport at a time. When multiple browser contexts connected in parallel, calling server.connect(transport) for each session overwrote the previous transport's callbacks, causing connection corruption and test timeouts. Changes: - server-utils.ts: accept factory function instead of single instance - All 9 example servers: wrap server creation in createServer() function - playwright.config.ts: re-enable parallel execution (4 workers) - servers.spec.ts: add toBeEnabled wait for server connection - Update screenshot baselines for basic-vanillajs and cohort-heatmap 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 23d0e2e commit 0b890bd

File tree

14 files changed

+335
-263
lines changed

14 files changed

+335
-263
lines changed

examples/basic-server-react/server.ts

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,28 @@ import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app";
77
import { startServer } from "../shared/server-utils.js";
88

99
const DIST_DIR = path.join(import.meta.dirname, "dist");
10+
const RESOURCE_URI = "ui://get-time/mcp-app.html";
1011

11-
const server = new McpServer({
12-
name: "Basic MCP App Server (React-based)",
13-
version: "1.0.0",
14-
});
15-
16-
// MCP Apps require two-part registration: a tool (what the LLM calls) and a
17-
// resource (the UI it renders). The `_meta` field on the tool links to the
18-
// resource URI, telling hosts which UI to display when the tool executes.
19-
{
20-
const resourceUri = "ui://get-time/mcp-app.html";
12+
/**
13+
* Creates a new MCP server instance with tools and resources registered.
14+
* Each HTTP session needs its own server instance because McpServer only supports one transport.
15+
*/
16+
function createServer(): McpServer {
17+
const server = new McpServer({
18+
name: "Basic MCP App Server (React-based)",
19+
version: "1.0.0",
20+
});
2121

22+
// MCP Apps require two-part registration: a tool (what the LLM calls) and a
23+
// resource (the UI it renders). The `_meta` field on the tool links to the
24+
// resource URI, telling hosts which UI to display when the tool executes.
2225
server.registerTool(
2326
"get-time",
2427
{
2528
title: "Get Time",
2629
description: "Returns the current server time as an ISO 8601 string.",
2730
inputSchema: {},
28-
_meta: { [RESOURCE_URI_META_KEY]: resourceUri },
31+
_meta: { [RESOURCE_URI_META_KEY]: RESOURCE_URI },
2932
},
3033
async (): Promise<CallToolResult> => {
3134
const time = new Date().toISOString();
@@ -36,8 +39,8 @@ const server = new McpServer({
3639
);
3740

3841
server.registerResource(
39-
resourceUri,
40-
resourceUri,
42+
RESOURCE_URI,
43+
RESOURCE_URI,
4144
{},
4245
async (): Promise<ReadResourceResult> => {
4346
const html = await fs.readFile(path.join(DIST_DIR, "mcp-app.html"), "utf-8");
@@ -46,19 +49,21 @@ const server = new McpServer({
4649
contents: [
4750
// Per the MCP App specification, "text/html;profile=mcp-app" signals
4851
// to the Host that this resource is indeed for an MCP App UI.
49-
{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html },
52+
{ uri: RESOURCE_URI, mimeType: RESOURCE_MIME_TYPE, text: html },
5053
],
5154
};
5255
},
5356
);
57+
58+
return server;
5459
}
5560

5661
async function main() {
5762
if (process.argv.includes("--stdio")) {
58-
await server.connect(new StdioServerTransport());
63+
await createServer().connect(new StdioServerTransport());
5964
} else {
6065
const port = parseInt(process.env.PORT ?? "3101", 10);
61-
await startServer(server, { port, name: "Basic MCP App Server (React-based)" });
66+
await startServer(createServer, { port, name: "Basic MCP App Server (React-based)" });
6267
}
6368
}
6469

examples/basic-server-vanillajs/server.ts

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,28 @@ import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app";
77
import { startServer } from "../shared/server-utils.js";
88

99
const DIST_DIR = path.join(import.meta.dirname, "dist");
10+
const RESOURCE_URI = "ui://get-time/mcp-app.html";
1011

11-
const server = new McpServer({
12-
name: "Basic MCP App Server (Vanilla JS)",
13-
version: "1.0.0",
14-
});
15-
16-
// MCP Apps require two-part registration: a tool (what the LLM calls) and a
17-
// resource (the UI it renders). The `_meta` field on the tool links to the
18-
// resource URI, telling hosts which UI to display when the tool executes.
19-
{
20-
const resourceUri = "ui://get-time/mcp-app.html";
12+
/**
13+
* Creates a new MCP server instance with tools and resources registered.
14+
* Each HTTP session needs its own server instance because McpServer only supports one transport.
15+
*/
16+
function createServer(): McpServer {
17+
const server = new McpServer({
18+
name: "Basic MCP App Server (Vanilla JS)",
19+
version: "1.0.0",
20+
});
2121

22+
// MCP Apps require two-part registration: a tool (what the LLM calls) and a
23+
// resource (the UI it renders). The `_meta` field on the tool links to the
24+
// resource URI, telling hosts which UI to display when the tool executes.
2225
server.registerTool(
2326
"get-time",
2427
{
2528
title: "Get Time",
2629
description: "Returns the current server time as an ISO 8601 string.",
2730
inputSchema: {},
28-
_meta: { [RESOURCE_URI_META_KEY]: resourceUri },
31+
_meta: { [RESOURCE_URI_META_KEY]: RESOURCE_URI },
2932
},
3033
async (): Promise<CallToolResult> => {
3134
const time = new Date().toISOString();
@@ -36,8 +39,8 @@ const server = new McpServer({
3639
);
3740

3841
server.registerResource(
39-
resourceUri,
40-
resourceUri,
42+
RESOURCE_URI,
43+
RESOURCE_URI,
4144
{},
4245
async (): Promise<ReadResourceResult> => {
4346
const html = await fs.readFile(path.join(DIST_DIR, "mcp-app.html"), "utf-8");
@@ -46,19 +49,21 @@ const server = new McpServer({
4649
contents: [
4750
// Per the MCP App specification, "text/html;profile=mcp-app" signals
4851
// to the Host that this resource is indeed for an MCP App UI.
49-
{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html },
52+
{ uri: RESOURCE_URI, mimeType: RESOURCE_MIME_TYPE, text: html },
5053
],
5154
};
5255
},
5356
);
57+
58+
return server;
5459
}
5560

5661
async function main() {
5762
if (process.argv.includes("--stdio")) {
58-
await server.connect(new StdioServerTransport());
63+
await createServer().connect(new StdioServerTransport());
5964
} else {
6065
const port = parseInt(process.env.PORT ?? "3102", 10);
61-
await startServer(server, { port, name: "Basic MCP App Server (Vanilla JS)" });
66+
await startServer(createServer, { port, name: "Basic MCP App Server (Vanilla JS)" });
6267
}
6368
}
6469

examples/budget-allocator-server/server.ts

Lines changed: 72 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -223,82 +223,90 @@ function generateHistory(
223223
// MCP Server Setup
224224
// ---------------------------------------------------------------------------
225225

226-
const server = new McpServer({
227-
name: "Budget Allocator Server",
228-
version: "1.0.0",
229-
});
230-
231226
const resourceUri = "ui://budget-allocator/mcp-app.html";
232227

233-
server.registerTool(
234-
"get-budget-data",
235-
{
236-
title: "Get Budget Data",
237-
description:
238-
"Returns budget configuration with 24 months of historical allocations and industry benchmarks by company stage",
239-
inputSchema: {},
240-
_meta: { [RESOURCE_URI_META_KEY]: resourceUri },
241-
},
242-
async (): Promise<CallToolResult> => {
243-
const response: BudgetDataResponse = {
244-
config: {
245-
categories: CATEGORIES.map(({ id, name, color, defaultPercent }) => ({
246-
id,
247-
name,
248-
color,
249-
defaultPercent,
250-
})),
251-
presetBudgets: [50000, 100000, 250000, 500000],
252-
defaultBudget: 100000,
253-
currency: "USD",
254-
currencySymbol: "$",
255-
},
256-
analytics: {
257-
history: generateHistory(CATEGORIES),
258-
benchmarks: BENCHMARKS,
259-
stages: ["Seed", "Series A", "Series B", "Growth"],
260-
defaultStage: "Series A",
261-
},
262-
};
263-
264-
return {
265-
content: [
266-
{
267-
type: "text",
268-
text: JSON.stringify(response),
228+
/**
229+
* Creates a new MCP server instance with tools and resources registered.
230+
* Each HTTP session needs its own server instance because McpServer only supports one transport.
231+
*/
232+
function createServer(): McpServer {
233+
const server = new McpServer({
234+
name: "Budget Allocator Server",
235+
version: "1.0.0",
236+
});
237+
238+
server.registerTool(
239+
"get-budget-data",
240+
{
241+
title: "Get Budget Data",
242+
description:
243+
"Returns budget configuration with 24 months of historical allocations and industry benchmarks by company stage",
244+
inputSchema: {},
245+
_meta: { [RESOURCE_URI_META_KEY]: resourceUri },
246+
},
247+
async (): Promise<CallToolResult> => {
248+
const response: BudgetDataResponse = {
249+
config: {
250+
categories: CATEGORIES.map(({ id, name, color, defaultPercent }) => ({
251+
id,
252+
name,
253+
color,
254+
defaultPercent,
255+
})),
256+
presetBudgets: [50000, 100000, 250000, 500000],
257+
defaultBudget: 100000,
258+
currency: "USD",
259+
currencySymbol: "$",
269260
},
270-
],
271-
};
272-
},
273-
);
274-
275-
server.registerResource(
276-
resourceUri,
277-
resourceUri,
278-
{ description: "Interactive Budget Allocator UI" },
279-
async (): Promise<ReadResourceResult> => {
280-
const html = await fs.readFile(
281-
path.join(DIST_DIR, "mcp-app.html"),
282-
"utf-8",
283-
);
284-
return {
285-
contents: [
286-
{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html },
287-
],
288-
};
289-
},
290-
);
261+
analytics: {
262+
history: generateHistory(CATEGORIES),
263+
benchmarks: BENCHMARKS,
264+
stages: ["Seed", "Series A", "Series B", "Growth"],
265+
defaultStage: "Series A",
266+
},
267+
};
268+
269+
return {
270+
content: [
271+
{
272+
type: "text",
273+
text: JSON.stringify(response),
274+
},
275+
],
276+
};
277+
},
278+
);
279+
280+
server.registerResource(
281+
resourceUri,
282+
resourceUri,
283+
{ description: "Interactive Budget Allocator UI" },
284+
async (): Promise<ReadResourceResult> => {
285+
const html = await fs.readFile(
286+
path.join(DIST_DIR, "mcp-app.html"),
287+
"utf-8",
288+
);
289+
return {
290+
contents: [
291+
{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html },
292+
],
293+
};
294+
},
295+
);
296+
297+
return server;
298+
}
291299

292300
// ---------------------------------------------------------------------------
293301
// Server Startup
294302
// ---------------------------------------------------------------------------
295303

296304
async function main() {
297305
if (process.argv.includes("--stdio")) {
298-
await server.connect(new StdioServerTransport());
306+
await createServer().connect(new StdioServerTransport());
299307
} else {
300308
const port = parseInt(process.env.PORT ?? "3103", 10);
301-
await startServer(server, { port, name: "Budget Allocator Server" });
309+
await startServer(createServer, { port, name: "Budget Allocator Server" });
302310
}
303311
}
304312

examples/cohort-heatmap-server/server.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -147,13 +147,13 @@ function generateCohortData(
147147
};
148148
}
149149

150-
const server = new McpServer({
151-
name: "Cohort Heatmap Server",
152-
version: "1.0.0",
153-
});
150+
function createServer(): McpServer {
151+
const server = new McpServer({
152+
name: "Cohort Heatmap Server",
153+
version: "1.0.0",
154+
});
154155

155-
// Register tool and resource
156-
{
156+
// Register tool and resource
157157
const resourceUri = "ui://get-cohort-data/mcp-app.html";
158158

159159
server.registerTool(
@@ -200,14 +200,16 @@ const server = new McpServer({
200200
};
201201
},
202202
);
203+
204+
return server;
203205
}
204206

205207
async function main() {
206208
if (process.argv.includes("--stdio")) {
207-
await server.connect(new StdioServerTransport());
209+
await createServer().connect(new StdioServerTransport());
208210
} else {
209211
const port = parseInt(process.env.PORT ?? "3104", 10);
210-
await startServer(server, { port, name: "Cohort Heatmap Server" });
212+
await startServer(createServer, { port, name: "Cohort Heatmap Server" });
211213
}
212214
}
213215

0 commit comments

Comments
 (0)