Skip to content

Commit 9e9f5f0

Browse files
Clean up examples (#182)
* Centralize example server transport logic in shared utils Move stdio/HTTP transport handling into `server-utils.ts` so example servers only need to call `startServer(createServer)`. Switch HTTP transport to stateless mode and remove legacy SSE endpoints. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> * Make example servers self-contained with local server-utils Copy `server-utils.ts` into each example's `src/` directory so examples can be used standalone without the shared directory. Updates imports from `"../shared/server-utils.js"` to `"./src/server-utils.js"`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> * Add `integration-server` example for E2E testing Add dedicated `integration-server` example (duplicated from `basic-server-react`) to preserve E2E testing capabilities while allowing `basic-server-react` to be simplified as a minimal example. Changes: - Add `examples/integration-server` exercising SDK communication APIs - Update E2E tests to use `integration-server` instead of `basic-server-react` and `basic-server-vanillajs` - Use server name labels instead of indices for more robust test selection - Remove `basic-react.png` and `basic-vanillajs.png` golden snapshots 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * Simplify `basic-server-*` examples - Clarify comments - Return plain text from `get-time` tool instead of JSON - Remove simulated cleanup delay from `onteardown` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> --------- Co-authored-by: Claude Opus 4.5 <[email protected]>
1 parent 3222c87 commit 9e9f5f0

File tree

36 files changed

+1748
-475
lines changed

36 files changed

+1748
-475
lines changed
Lines changed: 15 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,52 @@
11
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
32
import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
43
import fs from "node:fs/promises";
54
import path from "node:path";
65
import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "@modelcontextprotocol/ext-apps/server";
7-
import { startServer } from "../shared/server-utils.js";
6+
import { startServer } from "./src/server-utils.js";
87

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

1210
/**
1311
* 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.
1512
*/
1613
function createServer(): McpServer {
1714
const server = new McpServer({
18-
name: "Basic MCP App Server (React-based)",
15+
name: "Basic MCP App Server (React)",
1916
version: "1.0.0",
2017
});
2118

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.
19+
// Two-part registration: tool + resource, tied together by the resource URI.
20+
const resourceUri = "ui://get-time/mcp-app.html";
21+
22+
// Register a tool with UI metadata. When the host calls this tool, it reads
23+
// `_meta[RESOURCE_URI_META_KEY]` to know which resource to fetch and render
24+
// as an interactive UI.
2525
registerAppTool(server,
2626
"get-time",
2727
{
2828
title: "Get Time",
2929
description: "Returns the current server time as an ISO 8601 string.",
3030
inputSchema: {},
31-
_meta: { [RESOURCE_URI_META_KEY]: RESOURCE_URI },
31+
_meta: { [RESOURCE_URI_META_KEY]: resourceUri },
3232
},
3333
async (): Promise<CallToolResult> => {
3434
const time = new Date().toISOString();
35-
return {
36-
content: [{ type: "text", text: JSON.stringify({ time }) }],
37-
};
35+
return { content: [{ type: "text", text: time }] };
3836
},
3937
);
4038

39+
// Register the resource, which returns the bundled HTML/JavaScript for the UI.
4140
registerAppResource(server,
42-
RESOURCE_URI,
43-
RESOURCE_URI,
41+
resourceUri,
42+
resourceUri,
4443
{ mimeType: RESOURCE_MIME_TYPE },
4544
async (): Promise<ReadResourceResult> => {
4645
const html = await fs.readFile(path.join(DIST_DIR, "mcp-app.html"), "utf-8");
4746

4847
return {
4948
contents: [
50-
// Per the MCP App specification, "text/html;profile=mcp-app" signals
51-
// to the Host that this resource is indeed for an MCP App UI.
52-
{ uri: RESOURCE_URI, mimeType: RESOURCE_MIME_TYPE, text: html },
49+
{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html },
5350
],
5451
};
5552
},
@@ -58,16 +55,4 @@ function createServer(): McpServer {
5855
return server;
5956
}
6057

61-
async function main() {
62-
if (process.argv.includes("--stdio")) {
63-
await createServer().connect(new StdioServerTransport());
64-
} else {
65-
const port = parseInt(process.env.PORT ?? "3101", 10);
66-
await startServer(createServer, { port, name: "Basic MCP App Server (React-based)" });
67-
}
68-
}
69-
70-
main().catch((e) => {
71-
console.error(e);
72-
process.exit(1);
73-
});
58+
startServer(createServer);

examples/basic-server-react/src/mcp-app.tsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,8 @@ const log = {
2020

2121

2222
function extractTime(callToolResult: CallToolResult): string {
23-
const text = callToolResult.content!
24-
.filter((c): c is { type: "text"; text: string } => c.type === "text")
25-
.map((c) => c.text)
26-
.join("");
27-
const { time } = JSON.parse(text) as { time: string };
28-
return time;
23+
const { text } = callToolResult.content?.find((c) => c.type === "text")!;
24+
return text;
2925
}
3026

3127

@@ -37,8 +33,6 @@ function GetTimeApp() {
3733
onAppCreated: (app) => {
3834
app.onteardown = async () => {
3935
log.info("App is being torn down");
40-
await new Promise((resolve) => setTimeout(resolve, 500)); // Simulate cleanup work
41-
log.info("App teardown complete");
4236
return {};
4337
};
4438
app.ontoolinput = async (input) => {
@@ -120,7 +114,7 @@ function GetTimeAppInner({ app, toolResult }: GetTimeAppInnerProps) {
120114

121115
<div className={styles.action}>
122116
<p>
123-
<strong>Server Time:</strong> <code>{serverTime}</code>
117+
<strong>Server Time:</strong> <code id="server-time">{serverTime}</code>
124118
</p>
125119
<button onClick={handleGetTime}>Get Server Time</button>
126120
</div>
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+
}
Lines changed: 14 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,52 @@
11
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
32
import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
43
import fs from "node:fs/promises";
54
import path from "node:path";
65
import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "@modelcontextprotocol/ext-apps/server";
7-
import { startServer } from "../shared/server-utils.js";
6+
import { startServer } from "./src/server-utils.js";
87

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

1210
/**
1311
* 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.
1512
*/
1613
function createServer(): McpServer {
1714
const server = new McpServer({
1815
name: "Basic MCP App Server (Vanilla JS)",
1916
version: "1.0.0",
2017
});
2118

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.
19+
// Two-part registration: tool + resource, tied together by the resource URI.
20+
const resourceUri = "ui://get-time/mcp-app.html";
21+
22+
// Register a tool with UI metadata. When the host calls this tool, it reads
23+
// `_meta[RESOURCE_URI_META_KEY]` to know which resource to fetch and render
24+
// as an interactive UI.
2525
registerAppTool(server,
2626
"get-time",
2727
{
2828
title: "Get Time",
2929
description: "Returns the current server time as an ISO 8601 string.",
3030
inputSchema: {},
31-
_meta: { [RESOURCE_URI_META_KEY]: RESOURCE_URI },
31+
_meta: { [RESOURCE_URI_META_KEY]: resourceUri },
3232
},
3333
async (): Promise<CallToolResult> => {
3434
const time = new Date().toISOString();
35-
return {
36-
content: [{ type: "text", text: JSON.stringify({ time }) }],
37-
};
35+
return { content: [{ type: "text", text: time }] };
3836
},
3937
);
4038

39+
// Register the resource, which returns the bundled HTML/JavaScript for the UI.
4140
registerAppResource(server,
42-
RESOURCE_URI,
43-
RESOURCE_URI,
41+
resourceUri,
42+
resourceUri,
4443
{ mimeType: RESOURCE_MIME_TYPE },
4544
async (): Promise<ReadResourceResult> => {
4645
const html = await fs.readFile(path.join(DIST_DIR, "mcp-app.html"), "utf-8");
4746

4847
return {
4948
contents: [
50-
// Per the MCP App specification, "text/html;profile=mcp-app" signals
51-
// to the Host that this resource is indeed for an MCP App UI.
52-
{ uri: RESOURCE_URI, mimeType: RESOURCE_MIME_TYPE, text: html },
49+
{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html },
5350
],
5451
};
5552
},
@@ -58,16 +55,4 @@ function createServer(): McpServer {
5855
return server;
5956
}
6057

61-
async function main() {
62-
if (process.argv.includes("--stdio")) {
63-
await createServer().connect(new StdioServerTransport());
64-
} else {
65-
const port = parseInt(process.env.PORT ?? "3102", 10);
66-
await startServer(createServer, { port, name: "Basic MCP App Server (Vanilla JS)" });
67-
}
68-
}
69-
70-
main().catch((e) => {
71-
console.error(e);
72-
process.exit(1);
73-
});
58+
startServer(createServer);

examples/basic-server-vanillajs/src/mcp-app.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,8 @@ const log = {
1515

1616

1717
function extractTime(result: CallToolResult): string {
18-
const text = result.content!
19-
.filter((c): c is { type: "text"; text: string } => c.type === "text")
20-
.map((c) => c.text)
21-
.join("");
22-
const { time } = JSON.parse(text) as { time: string };
23-
return time;
18+
const { text } = result.content?.find((c) => c.type === "text")!;
19+
return text;
2420
}
2521

2622

@@ -40,8 +36,6 @@ const app = new App({ name: "Get Time App", version: "1.0.0" });
4036

4137
app.onteardown = async () => {
4238
log.info("App is being torn down");
43-
await new Promise((resolve) => setTimeout(resolve, 500)); // Simulate cleanup work
44-
log.info("App teardown complete");
4539
return {};
4640
};
4741

0 commit comments

Comments
 (0)