Skip to content

Commit 3a30e94

Browse files
ochafikclaude
andauthored
feat(examples): add SSE transport support and shared server utility (#136)
* feat(examples): add SSE transport support to all example servers Add backwards-compatible SSE transport support alongside the current Streamable HTTP transport. Each server now exposes: - /mcp (GET, POST, DELETE) - Streamable HTTP transport (current spec) - /sse (GET) - Legacy SSE transport stream endpoint - /messages (POST) - Legacy SSE transport message endpoint This enables older clients using the deprecated HTTP+SSE protocol (version 2024-11-05) to connect to the example servers. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * refactor(examples): migrate 6 servers to use shared server utility Refactored the following example servers to use the shared startServer utility from examples/shared/server-utils.ts: - threejs-server - system-monitor-server - cohort-heatmap-server - budget-allocator-server - customer-segmentation-server - scenario-modeler-server Changes for each server: - Removed direct imports: SSEServerTransport, StdioServerTransport, StreamableHTTPServerTransport, cors, express - Removed PORT constant (now handled by shared utility) - Added import for startServer from ../shared/server-utils.js - Replaced entire async main() function with single startServer() call - Preserved all business logic (tool registration, resource registration, helper functions) This reduces code duplication by ~700 lines and ensures consistent transport handling across all example servers. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * refactor(examples): migrate remaining servers to shared utility Complete the migration of example servers to use the shared server-utils.ts module for transport setup: - basic-server-react - basic-server-vanillajs - wiki-explorer-server - Add shared/server-utils.ts This centralizes all transport handling (stdio, Streamable HTTP, SSE) in one place, reducing code duplication across ~500 lines. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * fix(examples): improve server-utils with stateful sessions and security Address critical issues in the shared server utility: 1. Stateful sessions: Use sessionIdGenerator + onsessioninitialized to persist StreamableHTTPServerTransport across requests 2. Unified session store: Single Map for both transport types with proper type discrimination 3. Error handling: Try/catch on all endpoints with JSON-RPC errors 4. DNS rebinding protection: Use SDK's createMcpExpressApp helper 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * fix(examples): allow connections from any host for development Use host: "0.0.0.0" in createMcpExpressApp to disable DNS rebinding protection, allowing connections from Android emulators and other devices on the network. Also re-add CORS middleware. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * refactor(examples): use getPort() helper for transport selection - Add getPort() helper that returns undefined for --stdio, else port - Caller now explicitly passes port to startServer() - port: undefined → stdio mode, port: number → HTTP mode This makes the transport selection explicit at the call site. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * fix(examples): startServer returns promise that resolves/rejects on listen Use httpServer.on('listening') and httpServer.on('error') events to properly resolve or reject the returned promise. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * fix(examples): add error handling to startServer calls Exit with code 1 if server fails to start (e.g., port in use). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * refactor(examples): update server files to use main() pattern with stdio support Update all example server files to use the new main() pattern that supports both HTTP and stdio transports: - Add StdioServerTransport import - Remove getPort import (replaced with direct env var parsing) - Replace direct startServer call with async main() function - Add --stdio flag detection to choose between transports - Use PORT env var with default 3001 for HTTP mode Updated servers: - basic-server-vanillajs - wiki-explorer-server - threejs-server - system-monitor-server - cohort-heatmap-server - budget-allocator-server - customer-segmentation-server - scenario-modeler-server Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * fix(examples): use correct default ports matching run-all.ts Each server now defaults to its assigned port from run-all.ts: - basic-server-react: 3101 - basic-server-vanillajs: 3102 - budget-allocator-server: 3103 - cohort-heatmap-server: 3104 - customer-segmentation-server: 3105 - scenario-modeler-server: 3106 - system-monitor-server: 3107 - threejs-server: 3108 - wiki-explorer-server: 3109 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * fix(examples): expose mcp-session-id header for CORS requests Without exposedHeaders in the CORS config, browsers block JavaScript from reading the mcp-session-id response header. This caused the SDK's StreamableHTTPClientTransport to never capture the session ID, breaking all subsequent requests with "Bad request: not initialized" errors. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * pretty:fix --------- Co-authored-by: Claude <[email protected]>
1 parent 0408a6e commit 3a30e94

File tree

10 files changed

+241
-486
lines changed

10 files changed

+241
-486
lines changed
Lines changed: 12 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,18 @@
11
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2-
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
2+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
33
import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
4-
import cors from "cors";
5-
import express, { type Request, type Response } from "express";
64
import fs from "node:fs/promises";
75
import path from "node:path";
86
import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app";
7+
import { startServer } from "../shared/server-utils.js";
98

10-
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3001;
119
const DIST_DIR = path.join(import.meta.dirname, "dist");
1210

13-
1411
const server = new McpServer({
1512
name: "Basic MCP App Server (React-based)",
1613
version: "1.0.0",
1714
});
1815

19-
2016
// MCP Apps require two-part registration: a tool (what the LLM calls) and a
2117
// resource (the UI it renders). The `_meta` field on the tool links to the
2218
// resource URI, telling hosts which UI to display when the tool executes.
@@ -57,49 +53,16 @@ const server = new McpServer({
5753
);
5854
}
5955

60-
61-
const app = express();
62-
app.use(cors());
63-
app.use(express.json());
64-
65-
app.post("/mcp", async (req: Request, res: Response) => {
66-
try {
67-
const transport = new StreamableHTTPServerTransport({
68-
sessionIdGenerator: undefined,
69-
enableJsonResponse: true,
70-
});
71-
res.on("close", () => { transport.close(); });
72-
73-
await server.connect(transport);
74-
75-
await transport.handleRequest(req, res, req.body);
76-
} catch (error) {
77-
console.error("Error handling MCP request:", error);
78-
if (!res.headersSent) {
79-
res.status(500).json({
80-
jsonrpc: "2.0",
81-
error: { code: -32603, message: "Internal server error" },
82-
id: null,
83-
});
84-
}
56+
async function main() {
57+
if (process.argv.includes("--stdio")) {
58+
await server.connect(new StdioServerTransport());
59+
} else {
60+
const port = parseInt(process.env.PORT ?? "3101", 10);
61+
await startServer(server, { port, name: "Basic MCP App Server (React-based)" });
8562
}
86-
});
87-
88-
const httpServer = app.listen(PORT, (err) => {
89-
if (err) {
90-
console.error("Error starting server:", err);
91-
process.exit(1);
92-
}
93-
console.log(`Server listening on http://localhost:${PORT}/mcp`);
94-
});
95-
96-
function shutdown() {
97-
console.log("\nShutting down...");
98-
httpServer.close(() => {
99-
console.log("Server closed");
100-
process.exit(0);
101-
});
10263
}
10364

104-
process.on("SIGINT", shutdown);
105-
process.on("SIGTERM", shutdown);
65+
main().catch((e) => {
66+
console.error(e);
67+
process.exit(1);
68+
});
Lines changed: 12 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,18 @@
11
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2-
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
2+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
33
import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
4-
import cors from "cors";
5-
import express, { type Request, type Response } from "express";
64
import fs from "node:fs/promises";
75
import path from "node:path";
86
import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app";
7+
import { startServer } from "../shared/server-utils.js";
98

10-
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3001;
119
const DIST_DIR = path.join(import.meta.dirname, "dist");
1210

13-
1411
const server = new McpServer({
1512
name: "Basic MCP App Server (Vanilla JS)",
1613
version: "1.0.0",
1714
});
1815

19-
2016
// MCP Apps require two-part registration: a tool (what the LLM calls) and a
2117
// resource (the UI it renders). The `_meta` field on the tool links to the
2218
// resource URI, telling hosts which UI to display when the tool executes.
@@ -57,49 +53,16 @@ const server = new McpServer({
5753
);
5854
}
5955

60-
61-
const app = express();
62-
app.use(cors());
63-
app.use(express.json());
64-
65-
app.post("/mcp", async (req: Request, res: Response) => {
66-
try {
67-
const transport = new StreamableHTTPServerTransport({
68-
sessionIdGenerator: undefined,
69-
enableJsonResponse: true,
70-
});
71-
res.on("close", () => { transport.close(); });
72-
73-
await server.connect(transport);
74-
75-
await transport.handleRequest(req, res, req.body);
76-
} catch (error) {
77-
console.error("Error handling MCP request:", error);
78-
if (!res.headersSent) {
79-
res.status(500).json({
80-
jsonrpc: "2.0",
81-
error: { code: -32603, message: "Internal server error" },
82-
id: null,
83-
});
84-
}
56+
async function main() {
57+
if (process.argv.includes("--stdio")) {
58+
await server.connect(new StdioServerTransport());
59+
} else {
60+
const port = parseInt(process.env.PORT ?? "3102", 10);
61+
await startServer(server, { port, name: "Basic MCP App Server (Vanilla JS)" });
8562
}
86-
});
87-
88-
const httpServer = app.listen(PORT, (err) => {
89-
if (err) {
90-
console.error("Error starting server:", err);
91-
process.exit(1);
92-
}
93-
console.log(`Server listening on http://localhost:${PORT}/mcp`);
94-
});
95-
96-
function shutdown() {
97-
console.log("\nShutting down...");
98-
httpServer.close(() => {
99-
console.log("Server closed");
100-
process.exit(0);
101-
});
10263
}
10364

104-
process.on("SIGINT", shutdown);
105-
process.on("SIGTERM", shutdown);
65+
main().catch((e) => {
66+
console.error(e);
67+
process.exit(1);
68+
});

examples/budget-allocator-server/server.ts

Lines changed: 8 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,16 @@
66
*/
77
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
88
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9-
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
109
import type {
1110
CallToolResult,
1211
ReadResourceResult,
1312
} from "@modelcontextprotocol/sdk/types.js";
14-
import cors from "cors";
15-
import express, { type Request, type Response } from "express";
1613
import fs from "node:fs/promises";
1714
import path from "node:path";
1815
import { z } from "zod";
1916
import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app";
17+
import { startServer } from "../shared/server-utils.js";
2018

21-
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3001;
2219
const DIST_DIR = path.join(import.meta.dirname, "dist");
2320

2421
// ---------------------------------------------------------------------------
@@ -298,59 +295,14 @@ server.registerResource(
298295

299296
async function main() {
300297
if (process.argv.includes("--stdio")) {
301-
const transport = new StdioServerTransport();
302-
await server.connect(transport);
303-
console.error("Budget Allocator Server running in stdio mode");
298+
await server.connect(new StdioServerTransport());
304299
} else {
305-
const app = express();
306-
app.use(cors());
307-
app.use(express.json());
308-
309-
app.post("/mcp", async (req: Request, res: Response) => {
310-
try {
311-
const transport = new StreamableHTTPServerTransport({
312-
sessionIdGenerator: undefined,
313-
enableJsonResponse: true,
314-
});
315-
res.on("close", () => {
316-
transport.close();
317-
});
318-
319-
await server.connect(transport);
320-
await transport.handleRequest(req, res, req.body);
321-
} catch (error) {
322-
console.error("Error handling MCP request:", error);
323-
if (!res.headersSent) {
324-
res.status(500).json({
325-
jsonrpc: "2.0",
326-
error: { code: -32603, message: "Internal server error" },
327-
id: null,
328-
});
329-
}
330-
}
331-
});
332-
333-
const httpServer = app.listen(PORT, (err) => {
334-
if (err) {
335-
console.error("Error starting server:", err);
336-
process.exit(1);
337-
}
338-
console.log(
339-
`Budget Allocator Server listening on http://localhost:${PORT}/mcp`,
340-
);
341-
});
342-
343-
function shutdown() {
344-
console.log("\nShutting down...");
345-
httpServer.close(() => {
346-
console.log("Server closed");
347-
process.exit(0);
348-
});
349-
}
350-
351-
process.on("SIGINT", shutdown);
352-
process.on("SIGTERM", shutdown);
300+
const port = parseInt(process.env.PORT ?? "3103", 10);
301+
await startServer(server, { port, name: "Budget Allocator Server" });
353302
}
354303
}
355304

356-
main().catch(console.error);
305+
main().catch((e) => {
306+
console.error(e);
307+
process.exit(1);
308+
});

examples/cohort-heatmap-server/server.ts

Lines changed: 8 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
22
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3-
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
43
import type { ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
5-
import cors from "cors";
6-
import express, { type Request, type Response } from "express";
74
import fs from "node:fs/promises";
85
import path from "node:path";
96
import { z } from "zod";
107
import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app";
8+
import { startServer } from "../shared/server-utils.js";
119

12-
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3001;
1310
const DIST_DIR = path.join(import.meta.dirname, "dist");
1411

1512
// Schemas - types are derived from these using z.infer
@@ -207,60 +204,14 @@ const server = new McpServer({
207204

208205
async function main() {
209206
if (process.argv.includes("--stdio")) {
210-
const transport = new StdioServerTransport();
211-
await server.connect(transport);
212-
console.error("Cohort Heatmap Server running in stdio mode");
207+
await server.connect(new StdioServerTransport());
213208
} else {
214-
const app = express();
215-
app.use(cors());
216-
app.use(express.json());
217-
218-
app.post("/mcp", async (req: Request, res: Response) => {
219-
try {
220-
const transport = new StreamableHTTPServerTransport({
221-
sessionIdGenerator: undefined,
222-
enableJsonResponse: true,
223-
});
224-
res.on("close", () => {
225-
transport.close();
226-
});
227-
228-
await server.connect(transport);
229-
230-
await transport.handleRequest(req, res, req.body);
231-
} catch (error) {
232-
console.error("Error handling MCP request:", error);
233-
if (!res.headersSent) {
234-
res.status(500).json({
235-
jsonrpc: "2.0",
236-
error: { code: -32603, message: "Internal server error" },
237-
id: null,
238-
});
239-
}
240-
}
241-
});
242-
243-
const httpServer = app.listen(PORT, (err) => {
244-
if (err) {
245-
console.error("Error starting server:", err);
246-
process.exit(1);
247-
}
248-
console.log(
249-
`Cohort Heatmap Server listening on http://localhost:${PORT}/mcp`,
250-
);
251-
});
252-
253-
function shutdown() {
254-
console.log("\nShutting down...");
255-
httpServer.close(() => {
256-
console.log("Server closed");
257-
process.exit(0);
258-
});
259-
}
260-
261-
process.on("SIGINT", shutdown);
262-
process.on("SIGTERM", shutdown);
209+
const port = parseInt(process.env.PORT ?? "3104", 10);
210+
await startServer(server, { port, name: "Cohort Heatmap Server" });
263211
}
264212
}
265213

266-
main().catch(console.error);
214+
main().catch((e) => {
215+
console.error(e);
216+
process.exit(1);
217+
});

0 commit comments

Comments
 (0)