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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * pretty:fix --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
0408a6e9c7
commit
3a30e94501
10 changed files with 241 additions and 486 deletions
|
|
@ -1,22 +1,18 @@
|
|||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
|
||||
import cors from "cors";
|
||||
import express, { type Request, type Response } from "express";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app";
|
||||
import { startServer } from "../shared/server-utils.js";
|
||||
|
||||
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3001;
|
||||
const DIST_DIR = path.join(import.meta.dirname, "dist");
|
||||
|
||||
|
||||
const server = new McpServer({
|
||||
name: "Basic MCP App Server (React-based)",
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
|
||||
// MCP Apps require two-part registration: a tool (what the LLM calls) and a
|
||||
// resource (the UI it renders). The `_meta` field on the tool links to the
|
||||
// resource URI, telling hosts which UI to display when the tool executes.
|
||||
|
|
@ -57,49 +53,16 @@ const server = new McpServer({
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
app.post("/mcp", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: undefined,
|
||||
enableJsonResponse: true,
|
||||
});
|
||||
res.on("close", () => { transport.close(); });
|
||||
|
||||
await server.connect(transport);
|
||||
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
} catch (error) {
|
||||
console.error("Error handling MCP request:", error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32603, message: "Internal server error" },
|
||||
id: null,
|
||||
});
|
||||
}
|
||||
async function main() {
|
||||
if (process.argv.includes("--stdio")) {
|
||||
await server.connect(new StdioServerTransport());
|
||||
} else {
|
||||
const port = parseInt(process.env.PORT ?? "3101", 10);
|
||||
await startServer(server, { port, name: "Basic MCP App Server (React-based)" });
|
||||
}
|
||||
});
|
||||
|
||||
const httpServer = app.listen(PORT, (err) => {
|
||||
if (err) {
|
||||
console.error("Error starting server:", err);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`Server listening on http://localhost:${PORT}/mcp`);
|
||||
});
|
||||
|
||||
function shutdown() {
|
||||
console.log("\nShutting down...");
|
||||
httpServer.close(() => {
|
||||
console.log("Server closed");
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
process.on("SIGINT", shutdown);
|
||||
process.on("SIGTERM", shutdown);
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,22 +1,18 @@
|
|||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
|
||||
import cors from "cors";
|
||||
import express, { type Request, type Response } from "express";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app";
|
||||
import { startServer } from "../shared/server-utils.js";
|
||||
|
||||
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3001;
|
||||
const DIST_DIR = path.join(import.meta.dirname, "dist");
|
||||
|
||||
|
||||
const server = new McpServer({
|
||||
name: "Basic MCP App Server (Vanilla JS)",
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
|
||||
// MCP Apps require two-part registration: a tool (what the LLM calls) and a
|
||||
// resource (the UI it renders). The `_meta` field on the tool links to the
|
||||
// resource URI, telling hosts which UI to display when the tool executes.
|
||||
|
|
@ -57,49 +53,16 @@ const server = new McpServer({
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
app.post("/mcp", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: undefined,
|
||||
enableJsonResponse: true,
|
||||
});
|
||||
res.on("close", () => { transport.close(); });
|
||||
|
||||
await server.connect(transport);
|
||||
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
} catch (error) {
|
||||
console.error("Error handling MCP request:", error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32603, message: "Internal server error" },
|
||||
id: null,
|
||||
});
|
||||
}
|
||||
async function main() {
|
||||
if (process.argv.includes("--stdio")) {
|
||||
await server.connect(new StdioServerTransport());
|
||||
} else {
|
||||
const port = parseInt(process.env.PORT ?? "3102", 10);
|
||||
await startServer(server, { port, name: "Basic MCP App Server (Vanilla JS)" });
|
||||
}
|
||||
});
|
||||
|
||||
const httpServer = app.listen(PORT, (err) => {
|
||||
if (err) {
|
||||
console.error("Error starting server:", err);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`Server listening on http://localhost:${PORT}/mcp`);
|
||||
});
|
||||
|
||||
function shutdown() {
|
||||
console.log("\nShutting down...");
|
||||
httpServer.close(() => {
|
||||
console.log("Server closed");
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
process.on("SIGINT", shutdown);
|
||||
process.on("SIGTERM", shutdown);
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,19 +6,16 @@
|
|||
*/
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||
import type {
|
||||
CallToolResult,
|
||||
ReadResourceResult,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import cors from "cors";
|
||||
import express, { type Request, type Response } from "express";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { z } from "zod";
|
||||
import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app";
|
||||
import { startServer } from "../shared/server-utils.js";
|
||||
|
||||
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3001;
|
||||
const DIST_DIR = path.join(import.meta.dirname, "dist");
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -298,59 +295,14 @@ server.registerResource(
|
|||
|
||||
async function main() {
|
||||
if (process.argv.includes("--stdio")) {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error("Budget Allocator Server running in stdio mode");
|
||||
await server.connect(new StdioServerTransport());
|
||||
} else {
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
app.post("/mcp", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: undefined,
|
||||
enableJsonResponse: true,
|
||||
});
|
||||
res.on("close", () => {
|
||||
transport.close();
|
||||
});
|
||||
|
||||
await server.connect(transport);
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
} catch (error) {
|
||||
console.error("Error handling MCP request:", error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32603, message: "Internal server error" },
|
||||
id: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const httpServer = app.listen(PORT, (err) => {
|
||||
if (err) {
|
||||
console.error("Error starting server:", err);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(
|
||||
`Budget Allocator Server listening on http://localhost:${PORT}/mcp`,
|
||||
);
|
||||
});
|
||||
|
||||
function shutdown() {
|
||||
console.log("\nShutting down...");
|
||||
httpServer.close(() => {
|
||||
console.log("Server closed");
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
process.on("SIGINT", shutdown);
|
||||
process.on("SIGTERM", shutdown);
|
||||
const port = parseInt(process.env.PORT ?? "3103", 10);
|
||||
await startServer(server, { port, name: "Budget Allocator Server" });
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,15 +1,12 @@
|
|||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||
import type { ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
|
||||
import cors from "cors";
|
||||
import express, { type Request, type Response } from "express";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { z } from "zod";
|
||||
import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app";
|
||||
import { startServer } from "../shared/server-utils.js";
|
||||
|
||||
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3001;
|
||||
const DIST_DIR = path.join(import.meta.dirname, "dist");
|
||||
|
||||
// Schemas - types are derived from these using z.infer
|
||||
|
|
@ -207,60 +204,14 @@ const server = new McpServer({
|
|||
|
||||
async function main() {
|
||||
if (process.argv.includes("--stdio")) {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error("Cohort Heatmap Server running in stdio mode");
|
||||
await server.connect(new StdioServerTransport());
|
||||
} else {
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
app.post("/mcp", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: undefined,
|
||||
enableJsonResponse: true,
|
||||
});
|
||||
res.on("close", () => {
|
||||
transport.close();
|
||||
});
|
||||
|
||||
await server.connect(transport);
|
||||
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
} catch (error) {
|
||||
console.error("Error handling MCP request:", error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32603, message: "Internal server error" },
|
||||
id: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const httpServer = app.listen(PORT, (err) => {
|
||||
if (err) {
|
||||
console.error("Error starting server:", err);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(
|
||||
`Cohort Heatmap Server listening on http://localhost:${PORT}/mcp`,
|
||||
);
|
||||
});
|
||||
|
||||
function shutdown() {
|
||||
console.log("\nShutting down...");
|
||||
httpServer.close(() => {
|
||||
console.log("Server closed");
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
process.on("SIGINT", shutdown);
|
||||
process.on("SIGTERM", shutdown);
|
||||
const port = parseInt(process.env.PORT ?? "3104", 10);
|
||||
await startServer(server, { port, name: "Cohort Heatmap Server" });
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,23 +1,20 @@
|
|||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||
import type {
|
||||
CallToolResult,
|
||||
ReadResourceResult,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import cors from "cors";
|
||||
import express, { type Request, type Response } from "express";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { z } from "zod";
|
||||
import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app";
|
||||
import { startServer } from "../shared/server-utils.js";
|
||||
import {
|
||||
generateCustomers,
|
||||
generateSegmentSummaries,
|
||||
} from "./src/data-generator.ts";
|
||||
import { SEGMENTS, type Customer, type SegmentSummary } from "./src/types.ts";
|
||||
|
||||
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3001;
|
||||
const DIST_DIR = path.join(import.meta.dirname, "dist");
|
||||
|
||||
// Schemas - types are derived from these using z.infer
|
||||
|
|
@ -106,60 +103,14 @@ const server = new McpServer({
|
|||
|
||||
async function main() {
|
||||
if (process.argv.includes("--stdio")) {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error("Customer Segmentation Server running in stdio mode");
|
||||
await server.connect(new StdioServerTransport());
|
||||
} else {
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
app.post("/mcp", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: undefined,
|
||||
enableJsonResponse: true,
|
||||
});
|
||||
res.on("close", () => {
|
||||
transport.close();
|
||||
});
|
||||
|
||||
await server.connect(transport);
|
||||
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
} catch (error) {
|
||||
console.error("Error handling MCP request:", error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32603, message: "Internal server error" },
|
||||
id: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const httpServer = app.listen(PORT, (err) => {
|
||||
if (err) {
|
||||
console.error("Error starting server:", err);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(
|
||||
`Customer Segmentation Server listening on http://localhost:${PORT}/mcp`,
|
||||
);
|
||||
});
|
||||
|
||||
function shutdown() {
|
||||
console.log("\nShutting down...");
|
||||
httpServer.close(() => {
|
||||
console.log("Server closed");
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
process.on("SIGINT", shutdown);
|
||||
process.on("SIGTERM", shutdown);
|
||||
const port = parseInt(process.env.PORT ?? "3105", 10);
|
||||
await startServer(server, { port, name: "Customer Segmentation Server" });
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,18 +1,15 @@
|
|||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||
import type {
|
||||
CallToolResult,
|
||||
ReadResourceResult,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import cors from "cors";
|
||||
import express, { type Request, type Response } from "express";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { z } from "zod";
|
||||
import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app";
|
||||
import { startServer } from "../shared/server-utils.js";
|
||||
|
||||
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3001;
|
||||
const DIST_DIR = path.join(import.meta.dirname, "dist");
|
||||
|
||||
// ============================================================================
|
||||
|
|
@ -314,59 +311,14 @@ const server = new McpServer({
|
|||
|
||||
async function main() {
|
||||
if (process.argv.includes("--stdio")) {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error("SaaS Scenario Modeler Server running in stdio mode");
|
||||
await server.connect(new StdioServerTransport());
|
||||
} else {
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
app.post("/mcp", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: undefined,
|
||||
enableJsonResponse: true,
|
||||
});
|
||||
res.on("close", () => {
|
||||
transport.close();
|
||||
});
|
||||
|
||||
await server.connect(transport);
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
} catch (error) {
|
||||
console.error("Error handling MCP request:", error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32603, message: "Internal server error" },
|
||||
id: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const httpServer = app.listen(PORT, (err) => {
|
||||
if (err) {
|
||||
console.error("Error starting server:", err);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(
|
||||
`SaaS Scenario Modeler Server listening on http://localhost:${PORT}/mcp`,
|
||||
);
|
||||
});
|
||||
|
||||
function shutdown() {
|
||||
console.log("\nShutting down...");
|
||||
httpServer.close(() => {
|
||||
console.log("Server closed");
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
process.on("SIGINT", shutdown);
|
||||
process.on("SIGTERM", shutdown);
|
||||
const port = parseInt(process.env.PORT ?? "3106", 10);
|
||||
await startServer(server, { port, name: "SaaS Scenario Modeler Server" });
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
|
|||
161
examples/shared/server-utils.ts
Normal file
161
examples/shared/server-utils.ts
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
/**
|
||||
* Shared utilities for running MCP servers with HTTP transports.
|
||||
*
|
||||
* Supports:
|
||||
* - Streamable HTTP transport (/mcp) - stateful sessions
|
||||
* - Legacy SSE transport (/sse, /messages) - backwards compatibility
|
||||
*/
|
||||
|
||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
||||
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
||||
import cors from "cors";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { Request, Response } from "express";
|
||||
|
||||
export interface ServerOptions {
|
||||
/** Port to listen on (required). */
|
||||
port: number;
|
||||
/** Server name for logging. */
|
||||
name?: string;
|
||||
}
|
||||
|
||||
type Transport = StreamableHTTPServerTransport | SSEServerTransport;
|
||||
|
||||
/**
|
||||
* Starts an MCP server with HTTP transports.
|
||||
*
|
||||
* Provides:
|
||||
* - /mcp (GET/POST/DELETE): Streamable HTTP with stateful sessions
|
||||
* - /sse (GET) + /messages (POST): Legacy SSE for older clients
|
||||
*/
|
||||
export async function startServer(
|
||||
server: McpServer,
|
||||
options: ServerOptions,
|
||||
): Promise<void> {
|
||||
const { port, name = "MCP Server" } = options;
|
||||
|
||||
// Unified session store for both transport types
|
||||
const sessions = new Map<string, Transport>();
|
||||
|
||||
// Express app - bind to all interfaces for development/testing
|
||||
const app = createMcpExpressApp({ host: "0.0.0.0" });
|
||||
app.use(
|
||||
cors({
|
||||
exposedHeaders: ["mcp-session-id"],
|
||||
}),
|
||||
);
|
||||
|
||||
// Streamable HTTP (stateful)
|
||||
app.all("/mcp", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
||||
let transport = sessionId
|
||||
? (sessions.get(sessionId) as StreamableHTTPServerTransport | undefined)
|
||||
: undefined;
|
||||
|
||||
// Session exists but wrong transport type
|
||||
if (sessionId && sessions.has(sessionId) && !transport) {
|
||||
return res.status(400).json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32000, message: "Session uses different transport" },
|
||||
id: null,
|
||||
});
|
||||
}
|
||||
|
||||
// New session requires initialize request
|
||||
if (!transport) {
|
||||
if (req.method !== "POST" || !isInitializeRequest(req.body)) {
|
||||
return res.status(400).json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32000, message: "Bad request: not initialized" },
|
||||
id: null,
|
||||
});
|
||||
}
|
||||
|
||||
transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: () => randomUUID(),
|
||||
onsessioninitialized: (id) => {
|
||||
sessions.set(id, transport!);
|
||||
},
|
||||
});
|
||||
const t = transport;
|
||||
t.onclose = () => {
|
||||
if (t.sessionId) sessions.delete(t.sessionId);
|
||||
};
|
||||
await server.connect(transport);
|
||||
}
|
||||
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
} catch (error) {
|
||||
console.error("MCP error:", error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32603, message: "Internal server error" },
|
||||
id: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Legacy SSE
|
||||
app.get("/sse", async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const transport = new SSEServerTransport("/messages", res);
|
||||
sessions.set(transport.sessionId, transport);
|
||||
res.on("close", () => sessions.delete(transport.sessionId));
|
||||
await server.connect(transport);
|
||||
} catch (error) {
|
||||
console.error("SSE error:", error);
|
||||
if (!res.headersSent) res.status(500).end();
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/messages", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const transport = sessions.get(req.query.sessionId as string);
|
||||
if (!(transport instanceof SSEServerTransport)) {
|
||||
return res.status(404).json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32001, message: "Session not found" },
|
||||
id: null,
|
||||
});
|
||||
}
|
||||
await transport.handlePostMessage(req, res, req.body);
|
||||
} catch (error) {
|
||||
console.error("Message error:", error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32603, message: "Internal server error" },
|
||||
id: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const httpServer = app.listen(port);
|
||||
|
||||
httpServer.on("listening", () => {
|
||||
console.log(`${name} listening on http://localhost:${port}/mcp`);
|
||||
resolve();
|
||||
});
|
||||
|
||||
httpServer.on("error", (err: Error) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
const shutdown = () => {
|
||||
console.log("\nShutting down...");
|
||||
sessions.forEach((t) => t.close().catch(() => {}));
|
||||
httpServer.close(() => process.exit(0));
|
||||
};
|
||||
|
||||
process.on("SIGINT", shutdown);
|
||||
process.on("SIGTERM", shutdown);
|
||||
});
|
||||
}
|
||||
|
|
@ -1,20 +1,16 @@
|
|||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||
import type {
|
||||
CallToolResult,
|
||||
ReadResourceResult,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import cors from "cors";
|
||||
import express, { type Request, type Response } from "express";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import si from "systeminformation";
|
||||
import { z } from "zod";
|
||||
import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app";
|
||||
|
||||
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3001;
|
||||
import { startServer } from "../shared/server-utils.js";
|
||||
|
||||
// Schemas - types are derived from these using z.infer
|
||||
const CpuCoreSchema = z.object({
|
||||
|
|
@ -178,60 +174,14 @@ const server = new McpServer({
|
|||
|
||||
async function main() {
|
||||
if (process.argv.includes("--stdio")) {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error("System Monitor Server running in stdio mode");
|
||||
await server.connect(new StdioServerTransport());
|
||||
} else {
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
app.post("/mcp", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: undefined,
|
||||
enableJsonResponse: true,
|
||||
});
|
||||
res.on("close", () => {
|
||||
transport.close();
|
||||
});
|
||||
|
||||
await server.connect(transport);
|
||||
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
} catch (error) {
|
||||
console.error("Error handling MCP request:", error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32603, message: "Internal server error" },
|
||||
id: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const httpServer = app.listen(PORT, (err) => {
|
||||
if (err) {
|
||||
console.error("Error starting server:", err);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(
|
||||
`System Monitor Server listening on http://localhost:${PORT}/mcp`,
|
||||
);
|
||||
});
|
||||
|
||||
function shutdown() {
|
||||
console.log("\nShutting down...");
|
||||
httpServer.close(() => {
|
||||
console.log("Server closed");
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
process.on("SIGINT", shutdown);
|
||||
process.on("SIGTERM", shutdown);
|
||||
const port = parseInt(process.env.PORT ?? "3107", 10);
|
||||
await startServer(server, { port, name: "System Monitor Server" });
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,16 +5,13 @@
|
|||
*/
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||
import type { ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
|
||||
import cors from "cors";
|
||||
import express, { type Request, type Response } from "express";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { z } from "zod";
|
||||
import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app";
|
||||
import { startServer } from "../shared/server-utils.js";
|
||||
|
||||
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3001;
|
||||
const DIST_DIR = path.join(import.meta.dirname, "dist");
|
||||
|
||||
const THREEJS_DOCUMENTATION = `# Three.js Widget Documentation
|
||||
|
|
@ -187,54 +184,14 @@ const server = new McpServer({
|
|||
|
||||
async function main() {
|
||||
if (process.argv.includes("--stdio")) {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error("Three.js Server running in stdio mode");
|
||||
await server.connect(new StdioServerTransport());
|
||||
} else {
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
app.post("/mcp", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: undefined,
|
||||
enableJsonResponse: true,
|
||||
});
|
||||
res.on("close", () => {
|
||||
transport.close();
|
||||
});
|
||||
|
||||
await server.connect(transport);
|
||||
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
} catch (error) {
|
||||
console.error("Error handling MCP request:", error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32603, message: "Internal server error" },
|
||||
id: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const httpServer = app.listen(PORT, () => {
|
||||
console.log(`Three.js Server listening on http://localhost:${PORT}/mcp`);
|
||||
});
|
||||
|
||||
function shutdown() {
|
||||
console.log("\nShutting down...");
|
||||
httpServer.close(() => {
|
||||
console.log("Server closed");
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
process.on("SIGINT", shutdown);
|
||||
process.on("SIGTERM", shutdown);
|
||||
const port = parseInt(process.env.PORT ?? "3108", 10);
|
||||
await startServer(server, { port, name: "Three.js Server" });
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,19 +1,16 @@
|
|||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||
import type {
|
||||
CallToolResult,
|
||||
ReadResourceResult,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import * as cheerio from "cheerio";
|
||||
import cors from "cors";
|
||||
import express, { type Request, type Response } from "express";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { z } from "zod";
|
||||
import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app";
|
||||
import { startServer } from "../shared/server-utils.js";
|
||||
|
||||
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3001;
|
||||
const DIST_DIR = path.join(import.meta.dirname, "dist");
|
||||
|
||||
type PageInfo = { url: string; title: string };
|
||||
|
|
@ -144,56 +141,14 @@ const server = new McpServer({
|
|||
|
||||
async function main() {
|
||||
if (process.argv.includes("--stdio")) {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error("Wiki Explorer server running in stdio mode");
|
||||
await server.connect(new StdioServerTransport());
|
||||
} else {
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
app.post("/mcp", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: undefined,
|
||||
enableJsonResponse: true,
|
||||
});
|
||||
res.on("close", () => {
|
||||
transport.close();
|
||||
});
|
||||
|
||||
await server.connect(transport);
|
||||
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
} catch (error) {
|
||||
console.error("Error handling MCP request:", error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32603, message: "Internal server error" },
|
||||
id: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const httpServer = app.listen(PORT, () => {
|
||||
console.log(
|
||||
`Wiki Explorer server listening on http://localhost:${PORT}/mcp`,
|
||||
);
|
||||
});
|
||||
|
||||
function shutdown() {
|
||||
console.log("\nShutting down...");
|
||||
httpServer.close(() => {
|
||||
console.log("Server closed");
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
process.on("SIGINT", shutdown);
|
||||
process.on("SIGTERM", shutdown);
|
||||
const port = parseInt(process.env.PORT ?? "3109", 10);
|
||||
await startServer(server, { port, name: "Wiki Explorer" });
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue