From 6eb82b5562fde770ce16bb9d83b5540398fc0cd3 Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Fri, 23 Jan 2026 09:51:29 -0600 Subject: [PATCH 1/3] Refactor server startup functions Rename `startServer` to `startStreamableHTTPServer` and add a new `startStdioServer` helper to make the transport type explicit. Remove the `ServerOptions` interface in favor of reading `PORT` directly from the environment inside the function (defaulting to `3001`). Also update Python examples to default to port `3001` for consistency. Co-Authored-By: Claude Opus 4.5 --- examples/basic-server-preact/main.ts | 33 +++++++++-------- examples/basic-server-react/main.ts | 29 ++++++++------- examples/basic-server-solid/main.ts | 33 +++++++++-------- examples/basic-server-svelte/main.ts | 33 +++++++++-------- examples/basic-server-vanillajs/main.ts | 33 +++++++++-------- examples/basic-server-vue/main.ts | 33 +++++++++-------- examples/budget-allocator-server/main.ts | 33 +++++++++-------- examples/cohort-heatmap-server/main.ts | 33 +++++++++-------- examples/customer-segmentation-server/main.ts | 36 +++++++++---------- examples/debug-server/main.ts | 33 +++++++++-------- examples/integration-server/main.ts | 33 +++++++++-------- examples/map-server/main.ts | 33 +++++++++-------- examples/pdf-server/main.ts | 28 ++++++++------- examples/qr-server/server.py | 2 +- examples/say-server/server.py | 2 +- examples/scenario-modeler-server/main.ts | 33 +++++++++-------- examples/shadertoy-server/main.ts | 33 +++++++++-------- examples/sheet-music-server/main.ts | 33 +++++++++-------- examples/system-monitor-server/main.ts | 33 +++++++++-------- examples/threejs-server/main.ts | 33 +++++++++-------- examples/transcript-server/main.ts | 32 ++++++++--------- examples/video-resource-server/main.ts | 33 +++++++++-------- examples/wiki-explorer-server/main.ts | 33 +++++++++-------- 23 files changed, 338 insertions(+), 352 deletions(-) diff --git a/examples/basic-server-preact/main.ts b/examples/basic-server-preact/main.ts index d9a51a2e..76426326 100644 --- a/examples/basic-server-preact/main.ts +++ b/examples/basic-server-preact/main.ts @@ -4,10 +4,6 @@ * Or: node dist/index.js [--stdio] */ -/** - * Shared utilities for running MCP servers with Streamable HTTP transport. - */ - import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; @@ -16,22 +12,15 @@ import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; -export interface ServerOptions { - port: number; - name?: string; -} - /** * Starts an MCP server with Streamable HTTP transport in stateless mode. * * @param createServer - Factory function that creates a new McpServer instance per request. - * @param options - Server configuration options. */ -export async function startServer( +export async function startStreamableHTTPServer( createServer: () => McpServer, - options: ServerOptions, ): Promise { - const { port, name = "MCP Server" } = options; + const port = parseInt(process.env.PORT ?? "3001", 10); const app = createMcpExpressApp({ host: "0.0.0.0" }); app.use(cors()); @@ -67,7 +56,7 @@ export async function startServer( console.error("Failed to start server:", err); process.exit(1); } - console.log(`${name} listening on http://localhost:${port}/mcp`); + console.log(`MCP server listening on http://localhost:${port}/mcp`); }); const shutdown = () => { @@ -79,12 +68,22 @@ export async function startServer( process.on("SIGTERM", shutdown); } +/** + * Starts an MCP server with stdio transport. + * + * @param createServer - Factory function that creates a new McpServer instance. + */ +export async function startStdioServer( + createServer: () => McpServer, +): Promise { + await createServer().connect(new StdioServerTransport()); +} + async function main() { if (process.argv.includes("--stdio")) { - await createServer().connect(new StdioServerTransport()); + await startStdioServer(createServer); } else { - const port = parseInt(process.env.PORT ?? "3001", 10); - await startServer(createServer, { port, name: "Basic MCP App Server (Preact)" }); + await startStreamableHTTPServer(createServer); } } diff --git a/examples/basic-server-react/main.ts b/examples/basic-server-react/main.ts index 39840a13..ec187b68 100644 --- a/examples/basic-server-react/main.ts +++ b/examples/basic-server-react/main.ts @@ -12,22 +12,15 @@ import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; -export interface ServerOptions { - port: number; - name?: string; -} - /** * Starts an MCP server with Streamable HTTP transport in stateless mode. * * @param createServer - Factory function that creates a new McpServer instance per request. - * @param options - Server configuration options. */ -export async function startServer( +export async function startStreamableHTTPServer( createServer: () => McpServer, - options: ServerOptions, ): Promise { - const { port, name = "MCP Server" } = options; + const port = parseInt(process.env.PORT ?? "3001", 10); const app = createMcpExpressApp({ host: "0.0.0.0" }); app.use(cors()); @@ -63,7 +56,7 @@ export async function startServer( console.error("Failed to start server:", err); process.exit(1); } - console.log(`${name} listening on http://localhost:${port}/mcp`); + console.log(`MCP server listening on http://localhost:${port}/mcp`); }); const shutdown = () => { @@ -75,12 +68,22 @@ export async function startServer( process.on("SIGTERM", shutdown); } +/** + * Starts an MCP server with stdio transport. + * + * @param createServer - Factory function that creates a new McpServer instance. + */ +export async function startStdioServer( + createServer: () => McpServer, +): Promise { + await createServer().connect(new StdioServerTransport()); +} + async function main() { if (process.argv.includes("--stdio")) { - await createServer().connect(new StdioServerTransport()); + await startStdioServer(createServer); } else { - const port = parseInt(process.env.PORT ?? "3001", 10); - await startServer(createServer, { port, name: "Basic MCP App Server (React)" }); + await startStreamableHTTPServer(createServer); } } diff --git a/examples/basic-server-solid/main.ts b/examples/basic-server-solid/main.ts index 23353486..c8d9de22 100644 --- a/examples/basic-server-solid/main.ts +++ b/examples/basic-server-solid/main.ts @@ -4,10 +4,6 @@ * Or: node dist/index.js [--stdio] */ -/** - * Shared utilities for running MCP servers with Streamable HTTP transport. - */ - import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; @@ -16,22 +12,15 @@ import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; -export interface ServerOptions { - port: number; - name?: string; -} - /** * Starts an MCP server with Streamable HTTP transport in stateless mode. * * @param createServer - Factory function that creates a new McpServer instance per request. - * @param options - Server configuration options. */ -export async function startServer( +export async function startStreamableHTTPServer( createServer: () => McpServer, - options: ServerOptions, ): Promise { - const { port, name = "MCP Server" } = options; + const port = parseInt(process.env.PORT ?? "3001", 10); const app = createMcpExpressApp({ host: "0.0.0.0" }); app.use(cors()); @@ -67,7 +56,7 @@ export async function startServer( console.error("Failed to start server:", err); process.exit(1); } - console.log(`${name} listening on http://localhost:${port}/mcp`); + console.log(`MCP server listening on http://localhost:${port}/mcp`); }); const shutdown = () => { @@ -79,12 +68,22 @@ export async function startServer( process.on("SIGTERM", shutdown); } +/** + * Starts an MCP server with stdio transport. + * + * @param createServer - Factory function that creates a new McpServer instance. + */ +export async function startStdioServer( + createServer: () => McpServer, +): Promise { + await createServer().connect(new StdioServerTransport()); +} + async function main() { if (process.argv.includes("--stdio")) { - await createServer().connect(new StdioServerTransport()); + await startStdioServer(createServer); } else { - const port = parseInt(process.env.PORT ?? "3001", 10); - await startServer(createServer, { port, name: "Basic MCP App Server (Solid)" }); + await startStreamableHTTPServer(createServer); } } diff --git a/examples/basic-server-svelte/main.ts b/examples/basic-server-svelte/main.ts index 762a40eb..6c50a254 100644 --- a/examples/basic-server-svelte/main.ts +++ b/examples/basic-server-svelte/main.ts @@ -4,10 +4,6 @@ * Or: node dist/index.js [--stdio] */ -/** - * Shared utilities for running MCP servers with Streamable HTTP transport. - */ - import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; @@ -16,22 +12,15 @@ import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; -export interface ServerOptions { - port: number; - name?: string; -} - /** * Starts an MCP server with Streamable HTTP transport in stateless mode. * * @param createServer - Factory function that creates a new McpServer instance per request. - * @param options - Server configuration options. */ -export async function startServer( +export async function startStreamableHTTPServer( createServer: () => McpServer, - options: ServerOptions, ): Promise { - const { port, name = "MCP Server" } = options; + const port = parseInt(process.env.PORT ?? "3001", 10); const app = createMcpExpressApp({ host: "0.0.0.0" }); app.use(cors()); @@ -67,7 +56,7 @@ export async function startServer( console.error("Failed to start server:", err); process.exit(1); } - console.log(`${name} listening on http://localhost:${port}/mcp`); + console.log(`MCP server listening on http://localhost:${port}/mcp`); }); const shutdown = () => { @@ -79,12 +68,22 @@ export async function startServer( process.on("SIGTERM", shutdown); } +/** + * Starts an MCP server with stdio transport. + * + * @param createServer - Factory function that creates a new McpServer instance. + */ +export async function startStdioServer( + createServer: () => McpServer, +): Promise { + await createServer().connect(new StdioServerTransport()); +} + async function main() { if (process.argv.includes("--stdio")) { - await createServer().connect(new StdioServerTransport()); + await startStdioServer(createServer); } else { - const port = parseInt(process.env.PORT ?? "3001", 10); - await startServer(createServer, { port, name: "Basic MCP App Server (Svelte)" }); + await startStreamableHTTPServer(createServer); } } diff --git a/examples/basic-server-vanillajs/main.ts b/examples/basic-server-vanillajs/main.ts index d53d53b5..286fa34f 100644 --- a/examples/basic-server-vanillajs/main.ts +++ b/examples/basic-server-vanillajs/main.ts @@ -4,10 +4,6 @@ * Or: node dist/index.js [--stdio] */ -/** - * Shared utilities for running MCP servers with Streamable HTTP transport. - */ - import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; @@ -16,22 +12,15 @@ import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; -export interface ServerOptions { - port: number; - name?: string; -} - /** * Starts an MCP server with Streamable HTTP transport in stateless mode. * * @param createServer - Factory function that creates a new McpServer instance per request. - * @param options - Server configuration options. */ -export async function startServer( +export async function startStreamableHTTPServer( createServer: () => McpServer, - options: ServerOptions, ): Promise { - const { port, name = "MCP Server" } = options; + const port = parseInt(process.env.PORT ?? "3001", 10); const app = createMcpExpressApp({ host: "0.0.0.0" }); app.use(cors()); @@ -67,7 +56,7 @@ export async function startServer( console.error("Failed to start server:", err); process.exit(1); } - console.log(`${name} listening on http://localhost:${port}/mcp`); + console.log(`MCP server listening on http://localhost:${port}/mcp`); }); const shutdown = () => { @@ -79,12 +68,22 @@ export async function startServer( process.on("SIGTERM", shutdown); } +/** + * Starts an MCP server with stdio transport. + * + * @param createServer - Factory function that creates a new McpServer instance. + */ +export async function startStdioServer( + createServer: () => McpServer, +): Promise { + await createServer().connect(new StdioServerTransport()); +} + async function main() { if (process.argv.includes("--stdio")) { - await createServer().connect(new StdioServerTransport()); + await startStdioServer(createServer); } else { - const port = parseInt(process.env.PORT ?? "3102", 10); - await startServer(createServer, { port, name: "Basic MCP App Server (Vanilla JS)" }); + await startStreamableHTTPServer(createServer); } } diff --git a/examples/basic-server-vue/main.ts b/examples/basic-server-vue/main.ts index 0304600e..669a0718 100644 --- a/examples/basic-server-vue/main.ts +++ b/examples/basic-server-vue/main.ts @@ -4,10 +4,6 @@ * Or: node dist/index.js [--stdio] */ -/** - * Shared utilities for running MCP servers with Streamable HTTP transport. - */ - import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; @@ -16,22 +12,15 @@ import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; -export interface ServerOptions { - port: number; - name?: string; -} - /** * Starts an MCP server with Streamable HTTP transport in stateless mode. * * @param createServer - Factory function that creates a new McpServer instance per request. - * @param options - Server configuration options. */ -export async function startServer( +export async function startStreamableHTTPServer( createServer: () => McpServer, - options: ServerOptions, ): Promise { - const { port, name = "MCP Server" } = options; + const port = parseInt(process.env.PORT ?? "3001", 10); const app = createMcpExpressApp({ host: "0.0.0.0" }); app.use(cors()); @@ -67,7 +56,7 @@ export async function startServer( console.error("Failed to start server:", err); process.exit(1); } - console.log(`${name} listening on http://localhost:${port}/mcp`); + console.log(`MCP server listening on http://localhost:${port}/mcp`); }); const shutdown = () => { @@ -79,12 +68,22 @@ export async function startServer( process.on("SIGTERM", shutdown); } +/** + * Starts an MCP server with stdio transport. + * + * @param createServer - Factory function that creates a new McpServer instance. + */ +export async function startStdioServer( + createServer: () => McpServer, +): Promise { + await createServer().connect(new StdioServerTransport()); +} + async function main() { if (process.argv.includes("--stdio")) { - await createServer().connect(new StdioServerTransport()); + await startStdioServer(createServer); } else { - const port = parseInt(process.env.PORT ?? "3001", 10); - await startServer(createServer, { port, name: "Basic MCP App Server (Vue)" }); + await startStreamableHTTPServer(createServer); } } diff --git a/examples/budget-allocator-server/main.ts b/examples/budget-allocator-server/main.ts index 534e876d..cc488f0b 100644 --- a/examples/budget-allocator-server/main.ts +++ b/examples/budget-allocator-server/main.ts @@ -4,10 +4,6 @@ * Or: node dist/index.js [--stdio] */ -/** - * Shared utilities for running MCP servers with Streamable HTTP transport. - */ - import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; @@ -16,22 +12,15 @@ import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; -export interface ServerOptions { - port: number; - name?: string; -} - /** * Starts an MCP server with Streamable HTTP transport in stateless mode. * * @param createServer - Factory function that creates a new McpServer instance per request. - * @param options - Server configuration options. */ -export async function startServer( +export async function startStreamableHTTPServer( createServer: () => McpServer, - options: ServerOptions, ): Promise { - const { port, name = "MCP Server" } = options; + const port = parseInt(process.env.PORT ?? "3001", 10); const app = createMcpExpressApp({ host: "0.0.0.0" }); app.use(cors()); @@ -67,7 +56,7 @@ export async function startServer( console.error("Failed to start server:", err); process.exit(1); } - console.log(`${name} listening on http://localhost:${port}/mcp`); + console.log(`MCP server listening on http://localhost:${port}/mcp`); }); const shutdown = () => { @@ -79,12 +68,22 @@ export async function startServer( process.on("SIGTERM", shutdown); } +/** + * Starts an MCP server with stdio transport. + * + * @param createServer - Factory function that creates a new McpServer instance. + */ +export async function startStdioServer( + createServer: () => McpServer, +): Promise { + await createServer().connect(new StdioServerTransport()); +} + async function main() { if (process.argv.includes("--stdio")) { - await createServer().connect(new StdioServerTransport()); + await startStdioServer(createServer); } else { - const port = parseInt(process.env.PORT ?? "3103", 10); - await startServer(createServer, { port, name: "Marketing" }); + await startStreamableHTTPServer(createServer); } } diff --git a/examples/cohort-heatmap-server/main.ts b/examples/cohort-heatmap-server/main.ts index 0b716d1d..d59903b3 100644 --- a/examples/cohort-heatmap-server/main.ts +++ b/examples/cohort-heatmap-server/main.ts @@ -4,10 +4,6 @@ * Or: node dist/index.js [--stdio] */ -/** - * Shared utilities for running MCP servers with Streamable HTTP transport. - */ - import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; @@ -16,22 +12,15 @@ import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; -export interface ServerOptions { - port: number; - name?: string; -} - /** * Starts an MCP server with Streamable HTTP transport in stateless mode. * * @param createServer - Factory function that creates a new McpServer instance per request. - * @param options - Server configuration options. */ -export async function startServer( +export async function startStreamableHTTPServer( createServer: () => McpServer, - options: ServerOptions, ): Promise { - const { port, name = "MCP Server" } = options; + const port = parseInt(process.env.PORT ?? "3001", 10); const app = createMcpExpressApp({ host: "0.0.0.0" }); app.use(cors()); @@ -67,7 +56,7 @@ export async function startServer( console.error("Failed to start server:", err); process.exit(1); } - console.log(`${name} listening on http://localhost:${port}/mcp`); + console.log(`MCP server listening on http://localhost:${port}/mcp`); }); const shutdown = () => { @@ -79,12 +68,22 @@ export async function startServer( process.on("SIGTERM", shutdown); } +/** + * Starts an MCP server with stdio transport. + * + * @param createServer - Factory function that creates a new McpServer instance. + */ +export async function startStdioServer( + createServer: () => McpServer, +): Promise { + await createServer().connect(new StdioServerTransport()); +} + async function main() { if (process.argv.includes("--stdio")) { - await createServer().connect(new StdioServerTransport()); + await startStdioServer(createServer); } else { - const port = parseInt(process.env.PORT ?? "3104", 10); - await startServer(createServer, { port, name: "Cohort Heatmap Server" }); + await startStreamableHTTPServer(createServer); } } diff --git a/examples/customer-segmentation-server/main.ts b/examples/customer-segmentation-server/main.ts index 7be715a4..130076c5 100644 --- a/examples/customer-segmentation-server/main.ts +++ b/examples/customer-segmentation-server/main.ts @@ -4,10 +4,6 @@ * Or: node dist/index.js [--stdio] */ -/** - * Shared utilities for running MCP servers with Streamable HTTP transport. - */ - import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; @@ -16,22 +12,15 @@ import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; -export interface ServerOptions { - port: number; - name?: string; -} - /** * Starts an MCP server with Streamable HTTP transport in stateless mode. * * @param createServer - Factory function that creates a new McpServer instance per request. - * @param options - Server configuration options. */ -export async function startServer( +export async function startStreamableHTTPServer( createServer: () => McpServer, - options: ServerOptions, ): Promise { - const { port, name = "MCP Server" } = options; + const port = parseInt(process.env.PORT ?? "3001", 10); const app = createMcpExpressApp({ host: "0.0.0.0" }); app.use(cors()); @@ -67,7 +56,7 @@ export async function startServer( console.error("Failed to start server:", err); process.exit(1); } - console.log(`${name} listening on http://localhost:${port}/mcp`); + console.log(`MCP server listening on http://localhost:${port}/mcp`); }); const shutdown = () => { @@ -79,15 +68,22 @@ export async function startServer( process.on("SIGTERM", shutdown); } +/** + * Starts an MCP server with stdio transport. + * + * @param createServer - Factory function that creates a new McpServer instance. + */ +export async function startStdioServer( + createServer: () => McpServer, +): Promise { + await createServer().connect(new StdioServerTransport()); +} + async function main() { if (process.argv.includes("--stdio")) { - await createServer().connect(new StdioServerTransport()); + await startStdioServer(createServer); } else { - const port = parseInt(process.env.PORT ?? "3105", 10); - await startServer(createServer, { - port, - name: "Customer Segmentation Server", - }); + await startStreamableHTTPServer(createServer); } } diff --git a/examples/debug-server/main.ts b/examples/debug-server/main.ts index 5aa8ddbc..3c89a975 100644 --- a/examples/debug-server/main.ts +++ b/examples/debug-server/main.ts @@ -4,10 +4,6 @@ * Or: node dist/index.js [--stdio] */ -/** - * Shared utilities for running MCP servers with Streamable HTTP transport. - */ - import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; @@ -16,22 +12,15 @@ import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; -export interface ServerOptions { - port: number; - name?: string; -} - /** * Starts an MCP server with Streamable HTTP transport in stateless mode. * * @param createServer - Factory function that creates a new McpServer instance per request. - * @param options - Server configuration options. */ -export async function startServer( +export async function startStreamableHTTPServer( createServer: () => McpServer, - options: ServerOptions, ): Promise { - const { port, name = "MCP Server" } = options; + const port = parseInt(process.env.PORT ?? "3001", 10); const app = createMcpExpressApp({ host: "0.0.0.0" }); app.use(cors()); @@ -67,7 +56,7 @@ export async function startServer( console.error("Failed to start server:", err); process.exit(1); } - console.log(`${name} listening on http://localhost:${port}/mcp`); + console.log(`MCP server listening on http://localhost:${port}/mcp`); }); const shutdown = () => { @@ -79,12 +68,22 @@ export async function startServer( process.on("SIGTERM", shutdown); } +/** + * Starts an MCP server with stdio transport. + * + * @param createServer - Factory function that creates a new McpServer instance. + */ +export async function startStdioServer( + createServer: () => McpServer, +): Promise { + await createServer().connect(new StdioServerTransport()); +} + async function main() { if (process.argv.includes("--stdio")) { - await createServer().connect(new StdioServerTransport()); + await startStdioServer(createServer); } else { - const port = parseInt(process.env.PORT ?? "3110", 10); - await startServer(createServer, { port, name: "Debug Server" }); + await startStreamableHTTPServer(createServer); } } diff --git a/examples/integration-server/main.ts b/examples/integration-server/main.ts index 5340e8a8..c45787e2 100644 --- a/examples/integration-server/main.ts +++ b/examples/integration-server/main.ts @@ -4,10 +4,6 @@ * Or: node dist/index.js [--stdio] */ -/** - * Shared utilities for running MCP servers with Streamable HTTP transport. - */ - import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; @@ -16,22 +12,15 @@ import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; -export interface ServerOptions { - port: number; - name?: string; -} - /** * Starts an MCP server with Streamable HTTP transport in stateless mode. * * @param createServer - Factory function that creates a new McpServer instance per request. - * @param options - Server configuration options. */ -export async function startServer( +export async function startStreamableHTTPServer( createServer: () => McpServer, - options: ServerOptions, ): Promise { - const { port, name = "MCP Server" } = options; + const port = parseInt(process.env.PORT ?? "3001", 10); const app = createMcpExpressApp({ host: "0.0.0.0" }); app.use(cors()); @@ -67,7 +56,7 @@ export async function startServer( console.error("Failed to start server:", err); process.exit(1); } - console.log(`${name} listening on http://localhost:${port}/mcp`); + console.log(`MCP server listening on http://localhost:${port}/mcp`); }); const shutdown = () => { @@ -79,12 +68,22 @@ export async function startServer( process.on("SIGTERM", shutdown); } +/** + * Starts an MCP server with stdio transport. + * + * @param createServer - Factory function that creates a new McpServer instance. + */ +export async function startStdioServer( + createServer: () => McpServer, +): Promise { + await createServer().connect(new StdioServerTransport()); +} + async function main() { if (process.argv.includes("--stdio")) { - await createServer().connect(new StdioServerTransport()); + await startStdioServer(createServer); } else { - const port = parseInt(process.env.PORT ?? "3001", 10); - await startServer(createServer, { port, name: "Integration Test Server" }); + await startStreamableHTTPServer(createServer); } } diff --git a/examples/map-server/main.ts b/examples/map-server/main.ts index 44956598..f91f57b6 100644 --- a/examples/map-server/main.ts +++ b/examples/map-server/main.ts @@ -4,10 +4,6 @@ * Or: node dist/index.js [--stdio] */ -/** - * Shared utilities for running MCP servers with Streamable HTTP transport. - */ - import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; @@ -16,22 +12,15 @@ import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; -export interface ServerOptions { - port: number; - name?: string; -} - /** * Starts an MCP server with Streamable HTTP transport in stateless mode. * * @param createServer - Factory function that creates a new McpServer instance per request. - * @param options - Server configuration options. */ -export async function startServer( +export async function startStreamableHTTPServer( createServer: () => McpServer, - options: ServerOptions, ): Promise { - const { port, name = "MCP Server" } = options; + const port = parseInt(process.env.PORT ?? "3001", 10); const app = createMcpExpressApp({ host: "0.0.0.0" }); app.use(cors()); @@ -63,7 +52,7 @@ export async function startServer( }); const httpServer = app.listen(port, () => { - console.log(`${name} listening on http://localhost:${port}/mcp`); + console.log(`MCP server listening on http://localhost:${port}/mcp`); }); const shutdown = () => { @@ -75,12 +64,22 @@ export async function startServer( process.on("SIGTERM", shutdown); } +/** + * Starts an MCP server with stdio transport. + * + * @param createServer - Factory function that creates a new McpServer instance. + */ +export async function startStdioServer( + createServer: () => McpServer, +): Promise { + await createServer().connect(new StdioServerTransport()); +} + async function main() { if (process.argv.includes("--stdio")) { - await createServer().connect(new StdioServerTransport()); + await startStdioServer(createServer); } else { - const port = parseInt(process.env.PORT ?? "3001", 10); - await startServer(createServer, { port, name: "CesiumJS Map Server" }); + await startStreamableHTTPServer(createServer); } } diff --git a/examples/pdf-server/main.ts b/examples/pdf-server/main.ts index 2b0e3ff9..310a3127 100644 --- a/examples/pdf-server/main.ts +++ b/examples/pdf-server/main.ts @@ -23,19 +23,13 @@ import { DEFAULT_PDF, } from "./server.js"; -export interface ServerOptions { - port: number; - name?: string; -} - /** * Starts an MCP server with Streamable HTTP transport in stateless mode. */ -export async function startServer( +export async function startStreamableHTTPServer( createServer: () => McpServer, - options: ServerOptions, ): Promise { - const { port, name = "MCP Server" } = options; + const port = parseInt(process.env.PORT ?? "3001", 10); const app = createMcpExpressApp({ host: "0.0.0.0" }); app.use(cors()); @@ -71,7 +65,7 @@ export async function startServer( console.error("Failed to start server:", err); process.exit(1); } - console.log(`${name} listening on http://localhost:${port}/mcp`); + console.log(`MCP server listening on http://localhost:${port}/mcp`); }); const shutdown = () => { @@ -83,6 +77,17 @@ export async function startServer( process.on("SIGTERM", shutdown); } +/** + * Starts an MCP server with stdio transport. + * + * @param createServer - Factory function that creates a new McpServer instance. + */ +export async function startStdioServer( + createServer: () => McpServer, +): Promise { + await createServer().connect(new StdioServerTransport()); +} + function parseArgs(): { urls: string[]; stdio: boolean } { const args = process.argv.slice(2); const urls: string[] = []; @@ -132,10 +137,9 @@ async function main() { ); if (stdio) { - await createServer().connect(new StdioServerTransport()); + await startStdioServer(createServer); } else { - const port = parseInt(process.env.PORT ?? "3120", 10); - await startServer(createServer, { port, name: "PDF Server" }); + await startStreamableHTTPServer(createServer); } } diff --git a/examples/qr-server/server.py b/examples/qr-server/server.py index 5595ef3d..02384c7b 100755 --- a/examples/qr-server/server.py +++ b/examples/qr-server/server.py @@ -25,7 +25,7 @@ VIEW_URI = "ui://qr-server/view.html" HOST = os.environ.get("HOST", "0.0.0.0") # 0.0.0.0 for Docker compatibility -PORT = int(os.environ.get("PORT", "3108")) +PORT = int(os.environ.get("PORT", "3001")) mcp = FastMCP("QR Code Server") diff --git a/examples/say-server/server.py b/examples/say-server/server.py index bc1313e0..64b7417a 100755 --- a/examples/say-server/server.py +++ b/examples/say-server/server.py @@ -58,7 +58,7 @@ VIEW_URI = "ui://say-demo/view.html" HOST = os.environ.get("HOST", "0.0.0.0") -PORT = int(os.environ.get("PORT", "3109")) +PORT = int(os.environ.get("PORT", "3001")) # Speaker icon as SVG data URI SPEAKER_ICON = Icon( diff --git a/examples/scenario-modeler-server/main.ts b/examples/scenario-modeler-server/main.ts index b7cc7d00..dbd3ab6a 100644 --- a/examples/scenario-modeler-server/main.ts +++ b/examples/scenario-modeler-server/main.ts @@ -4,10 +4,6 @@ * Or: node dist/index.js [--stdio] */ -/** - * Shared utilities for running MCP servers with Streamable HTTP transport. - */ - import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; @@ -16,22 +12,15 @@ import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; -export interface ServerOptions { - port: number; - name?: string; -} - /** * Starts an MCP server with Streamable HTTP transport in stateless mode. * * @param createServer - Factory function that creates a new McpServer instance per request. - * @param options - Server configuration options. */ -export async function startServer( +export async function startStreamableHTTPServer( createServer: () => McpServer, - options: ServerOptions, ): Promise { - const { port, name = "MCP Server" } = options; + const port = parseInt(process.env.PORT ?? "3001", 10); const app = createMcpExpressApp({ host: "0.0.0.0" }); app.use(cors()); @@ -67,7 +56,7 @@ export async function startServer( console.error("Failed to start server:", err); process.exit(1); } - console.log(`${name} listening on http://localhost:${port}/mcp`); + console.log(`MCP server listening on http://localhost:${port}/mcp`); }); const shutdown = () => { @@ -79,12 +68,22 @@ export async function startServer( process.on("SIGTERM", shutdown); } +/** + * Starts an MCP server with stdio transport. + * + * @param createServer - Factory function that creates a new McpServer instance. + */ +export async function startStdioServer( + createServer: () => McpServer, +): Promise { + await createServer().connect(new StdioServerTransport()); +} + async function main() { if (process.argv.includes("--stdio")) { - await createServer().connect(new StdioServerTransport()); + await startStdioServer(createServer); } else { - const port = parseInt(process.env.PORT ?? "3106", 10); - await startServer(createServer, { port, name: "SaaS Scenario Modeler" }); + await startStreamableHTTPServer(createServer); } } diff --git a/examples/shadertoy-server/main.ts b/examples/shadertoy-server/main.ts index 717f5e64..ac8f6ad8 100644 --- a/examples/shadertoy-server/main.ts +++ b/examples/shadertoy-server/main.ts @@ -4,10 +4,6 @@ * Or: node dist/index.js [--stdio] */ -/** - * Shared utilities for running MCP servers with Streamable HTTP transport. - */ - import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; @@ -16,22 +12,15 @@ import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; -export interface ServerOptions { - port: number; - name?: string; -} - /** * Starts an MCP server with Streamable HTTP transport in stateless mode. * * @param createServer - Factory function that creates a new McpServer instance per request. - * @param options - Server configuration options. */ -export async function startServer( +export async function startStreamableHTTPServer( createServer: () => McpServer, - options: ServerOptions, ): Promise { - const { port, name = "MCP Server" } = options; + const port = parseInt(process.env.PORT ?? "3001", 10); const app = createMcpExpressApp({ host: "0.0.0.0" }); app.use(cors()); @@ -67,7 +56,7 @@ export async function startServer( console.error("Failed to start server:", err); process.exit(1); } - console.log(`${name} listening on http://localhost:${port}/mcp`); + console.log(`MCP server listening on http://localhost:${port}/mcp`); }); const shutdown = () => { @@ -79,12 +68,22 @@ export async function startServer( process.on("SIGTERM", shutdown); } +/** + * Starts an MCP server with stdio transport. + * + * @param createServer - Factory function that creates a new McpServer instance. + */ +export async function startStdioServer( + createServer: () => McpServer, +): Promise { + await createServer().connect(new StdioServerTransport()); +} + async function main() { if (process.argv.includes("--stdio")) { - await createServer().connect(new StdioServerTransport()); + await startStdioServer(createServer); } else { - const port = parseInt(process.env.PORT ?? "3001", 10); - await startServer(createServer, { port, name: "ShaderToy Server" }); + await startStreamableHTTPServer(createServer); } } diff --git a/examples/sheet-music-server/main.ts b/examples/sheet-music-server/main.ts index 4b206e72..8b143a7a 100644 --- a/examples/sheet-music-server/main.ts +++ b/examples/sheet-music-server/main.ts @@ -4,10 +4,6 @@ * Or: node dist/index.js [--stdio] */ -/** - * Shared utilities for running MCP servers with Streamable HTTP transport. - */ - import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; @@ -16,22 +12,15 @@ import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; -export interface ServerOptions { - port: number; - name?: string; -} - /** * Starts an MCP server with Streamable HTTP transport in stateless mode. * * @param createServer - Factory function that creates a new McpServer instance per request. - * @param options - Server configuration options. */ -export async function startServer( +export async function startStreamableHTTPServer( createServer: () => McpServer, - options: ServerOptions, ): Promise { - const { port, name = "MCP Server" } = options; + const port = parseInt(process.env.PORT ?? "3001", 10); const app = createMcpExpressApp({ host: "0.0.0.0" }); app.use(cors()); @@ -67,7 +56,7 @@ export async function startServer( console.error("Failed to start server:", err); process.exit(1); } - console.log(`${name} listening on http://localhost:${port}/mcp`); + console.log(`MCP server listening on http://localhost:${port}/mcp`); }); const shutdown = () => { @@ -79,12 +68,22 @@ export async function startServer( process.on("SIGTERM", shutdown); } +/** + * Starts an MCP server with stdio transport. + * + * @param createServer - Factory function that creates a new McpServer instance. + */ +export async function startStdioServer( + createServer: () => McpServer, +): Promise { + await createServer().connect(new StdioServerTransport()); +} + async function main() { if (process.argv.includes("--stdio")) { - await createServer().connect(new StdioServerTransport()); + await startStdioServer(createServer); } else { - const port = parseInt(process.env.PORT ?? "3001", 10); - await startServer(createServer, { port, name: "Sheet Music Server" }); + await startStreamableHTTPServer(createServer); } } diff --git a/examples/system-monitor-server/main.ts b/examples/system-monitor-server/main.ts index 19624809..365a5604 100644 --- a/examples/system-monitor-server/main.ts +++ b/examples/system-monitor-server/main.ts @@ -4,10 +4,6 @@ * Or: node dist/index.js [--stdio] */ -/** - * Shared utilities for running MCP servers with Streamable HTTP transport. - */ - import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; @@ -16,22 +12,15 @@ import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; -export interface ServerOptions { - port: number; - name?: string; -} - /** * Starts an MCP server with Streamable HTTP transport in stateless mode. * * @param createServer - Factory function that creates a new McpServer instance per request. - * @param options - Server configuration options. */ -export async function startServer( +export async function startStreamableHTTPServer( createServer: () => McpServer, - options: ServerOptions, ): Promise { - const { port, name = "MCP Server" } = options; + const port = parseInt(process.env.PORT ?? "3001", 10); const app = createMcpExpressApp({ host: "0.0.0.0" }); app.use(cors()); @@ -67,7 +56,7 @@ export async function startServer( console.error("Failed to start server:", err); process.exit(1); } - console.log(`${name} listening on http://localhost:${port}/mcp`); + console.log(`MCP server listening on http://localhost:${port}/mcp`); }); const shutdown = () => { @@ -79,12 +68,22 @@ export async function startServer( process.on("SIGTERM", shutdown); } +/** + * Starts an MCP server with stdio transport. + * + * @param createServer - Factory function that creates a new McpServer instance. + */ +export async function startStdioServer( + createServer: () => McpServer, +): Promise { + await createServer().connect(new StdioServerTransport()); +} + async function main() { if (process.argv.includes("--stdio")) { - await createServer().connect(new StdioServerTransport()); + await startStdioServer(createServer); } else { - const port = parseInt(process.env.PORT ?? "3107", 10); - await startServer(createServer, { port, name: "System Monitor Server" }); + await startStreamableHTTPServer(createServer); } } diff --git a/examples/threejs-server/main.ts b/examples/threejs-server/main.ts index 1f779a97..13a2084a 100644 --- a/examples/threejs-server/main.ts +++ b/examples/threejs-server/main.ts @@ -4,10 +4,6 @@ * Or: node dist/index.js [--stdio] */ -/** - * Shared utilities for running MCP servers with Streamable HTTP transport. - */ - import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; @@ -16,22 +12,15 @@ import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; -export interface ServerOptions { - port: number; - name?: string; -} - /** * Starts an MCP server with Streamable HTTP transport in stateless mode. * * @param createServer - Factory function that creates a new McpServer instance per request. - * @param options - Server configuration options. */ -export async function startServer( +export async function startStreamableHTTPServer( createServer: () => McpServer, - options: ServerOptions, ): Promise { - const { port, name = "MCP Server" } = options; + const port = parseInt(process.env.PORT ?? "3001", 10); const app = createMcpExpressApp({ host: "0.0.0.0" }); app.use(cors()); @@ -67,7 +56,7 @@ export async function startServer( console.error("Failed to start server:", err); process.exit(1); } - console.log(`${name} listening on http://localhost:${port}/mcp`); + console.log(`MCP server listening on http://localhost:${port}/mcp`); }); const shutdown = () => { @@ -79,12 +68,22 @@ export async function startServer( process.on("SIGTERM", shutdown); } +/** + * Starts an MCP server with stdio transport. + * + * @param createServer - Factory function that creates a new McpServer instance. + */ +export async function startStdioServer( + createServer: () => McpServer, +): Promise { + await createServer().connect(new StdioServerTransport()); +} + async function main() { if (process.argv.includes("--stdio")) { - await createServer().connect(new StdioServerTransport()); + await startStdioServer(createServer); } else { - const port = parseInt(process.env.PORT ?? "3108", 10); - await startServer(createServer, { port, name: "Three.js Server" }); + await startStreamableHTTPServer(createServer); } } diff --git a/examples/transcript-server/main.ts b/examples/transcript-server/main.ts index 2a927041..68db8dc7 100644 --- a/examples/transcript-server/main.ts +++ b/examples/transcript-server/main.ts @@ -4,10 +4,6 @@ * Or: node dist/index.js [--stdio] */ -/** - * Shared utilities for running MCP servers with Streamable HTTP transport. - */ - import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; @@ -16,19 +12,13 @@ import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; -export interface ServerOptions { - port: number; - name?: string; -} - /** * Starts an MCP server with Streamable HTTP transport in stateless mode. */ -export async function startServer( +export async function startStreamableHTTPServer( createServer: () => McpServer, - options: ServerOptions, ): Promise { - const { port, name = "MCP Server" } = options; + const port = parseInt(process.env.PORT ?? "3001", 10); const app = createMcpExpressApp({ host: "0.0.0.0" }); app.use(cors()); @@ -64,7 +54,7 @@ export async function startServer( console.error("Failed to start server:", err); process.exit(1); } - console.log(`${name} listening on http://localhost:${port}/mcp`); + console.log(`MCP server listening on http://localhost:${port}/mcp`); }); const shutdown = () => { @@ -76,12 +66,22 @@ export async function startServer( process.on("SIGTERM", shutdown); } +/** + * Starts an MCP server with stdio transport. + * + * @param createServer - Factory function that creates a new McpServer instance. + */ +export async function startStdioServer( + createServer: () => McpServer, +): Promise { + await createServer().connect(new StdioServerTransport()); +} + async function main() { if (process.argv.includes("--stdio")) { - await createServer().connect(new StdioServerTransport()); + await startStdioServer(createServer); } else { - const port = parseInt(process.env.PORT ?? "3109", 10); - await startServer(createServer, { port, name: "Transcript Server" }); + await startStreamableHTTPServer(createServer); } } diff --git a/examples/video-resource-server/main.ts b/examples/video-resource-server/main.ts index ff304639..082fa5ab 100644 --- a/examples/video-resource-server/main.ts +++ b/examples/video-resource-server/main.ts @@ -4,10 +4,6 @@ * Or: node dist/index.js [--stdio] */ -/** - * Shared utilities for running MCP servers with Streamable HTTP transport. - */ - import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; @@ -16,22 +12,15 @@ import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; -export interface ServerOptions { - port: number; - name?: string; -} - /** * Starts an MCP server with Streamable HTTP transport in stateless mode. * * @param createServer - Factory function that creates a new McpServer instance per request. - * @param options - Server configuration options. */ -export async function startServer( +export async function startStreamableHTTPServer( createServer: () => McpServer, - options: ServerOptions, ): Promise { - const { port, name = "MCP Server" } = options; + const port = parseInt(process.env.PORT ?? "3001", 10); const app = createMcpExpressApp({ host: "0.0.0.0" }); app.use(cors()); @@ -67,7 +56,7 @@ export async function startServer( console.error("Failed to start server:", err); process.exit(1); } - console.log(`${name} listening on http://localhost:${port}/mcp`); + console.log(`MCP server listening on http://localhost:${port}/mcp`); }); const shutdown = () => { @@ -79,12 +68,22 @@ export async function startServer( process.on("SIGTERM", shutdown); } +/** + * Starts an MCP server with stdio transport. + * + * @param createServer - Factory function that creates a new McpServer instance. + */ +export async function startStdioServer( + createServer: () => McpServer, +): Promise { + await createServer().connect(new StdioServerTransport()); +} + async function main() { if (process.argv.includes("--stdio")) { - await createServer().connect(new StdioServerTransport()); + await startStdioServer(createServer); } else { - const port = parseInt(process.env.PORT ?? "3001", 10); - await startServer(createServer, { port, name: "Video Resource Server" }); + await startStreamableHTTPServer(createServer); } } diff --git a/examples/wiki-explorer-server/main.ts b/examples/wiki-explorer-server/main.ts index d83ac427..cc7b3a22 100644 --- a/examples/wiki-explorer-server/main.ts +++ b/examples/wiki-explorer-server/main.ts @@ -4,10 +4,6 @@ * Or: node dist/index.js [--stdio] */ -/** - * Shared utilities for running MCP servers with Streamable HTTP transport. - */ - import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; @@ -16,22 +12,15 @@ import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; -export interface ServerOptions { - port: number; - name?: string; -} - /** * Starts an MCP server with Streamable HTTP transport in stateless mode. * * @param createServer - Factory function that creates a new McpServer instance per request. - * @param options - Server configuration options. */ -export async function startServer( +export async function startStreamableHTTPServer( createServer: () => McpServer, - options: ServerOptions, ): Promise { - const { port, name = "MCP Server" } = options; + const port = parseInt(process.env.PORT ?? "3001", 10); const app = createMcpExpressApp({ host: "0.0.0.0" }); app.use(cors()); @@ -67,7 +56,7 @@ export async function startServer( console.error("Failed to start server:", err); process.exit(1); } - console.log(`${name} listening on http://localhost:${port}/mcp`); + console.log(`MCP server listening on http://localhost:${port}/mcp`); }); const shutdown = () => { @@ -79,12 +68,22 @@ export async function startServer( process.on("SIGTERM", shutdown); } +/** + * Starts an MCP server with stdio transport. + * + * @param createServer - Factory function that creates a new McpServer instance. + */ +export async function startStdioServer( + createServer: () => McpServer, +): Promise { + await createServer().connect(new StdioServerTransport()); +} + async function main() { if (process.argv.includes("--stdio")) { - await createServer().connect(new StdioServerTransport()); + await startStdioServer(createServer); } else { - const port = parseInt(process.env.PORT ?? "3109", 10); - await startServer(createServer, { port, name: "Wiki Explorer" }); + await startStreamableHTTPServer(createServer); } } From 56a2e79bbaf1a54ebca7c7cc5e7cf6cb15affb42 Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Fri, 23 Jan 2026 12:47:30 -0600 Subject: [PATCH 2/3] Support full-file inclusion in synced code snippets Previously, the script only supported extracting regions from `.ts/.tsx` files using the `#regionName` syntax. Now it also supports including entire files without a region specifier, enabling sync of any file type (JSON, YAML, shell scripts, etc.) into documentation. Changes: - Make `#regionName` optional in the `source=""` attribute - Accept any fence language (not just `ts`/`tsx`) - Simplify cache to flat map with composite keys - Add validation error for region extraction on non-TypeScript files Co-Authored-By: Claude Opus 4.5 --- AGENTS.md | 2 +- scripts/sync-snippets.ts | 128 ++++++++++++++++++++------------------- 2 files changed, 66 insertions(+), 64 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ae8903a4..9bbd2f05 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -80,7 +80,7 @@ View (App) <--PostMessageTransport--> Host (AppBridge) <--MCP Client--> MCP Serv ## Documentation -JSDoc `@example` tags should pull type-checked code from companion `.examples.ts` files (e.g., `app.ts` → `app.examples.ts`). Use ` ```ts source="./file.examples.ts#regionName" ` fences referencing `//#region regionName` blocks, then run `npm run sync:snippets`. Region names follow `exportedName_variant` or `ClassName_methodName_variant` pattern (e.g., `useApp_basicUsage`, `App_hostCapabilities_checkAfterConnection`). +JSDoc `@example` tags should pull type-checked code from companion `.examples.ts` files (e.g., `app.ts` → `app.examples.ts`). Use ` ```ts source="./file.examples.ts#regionName" ` fences referencing `//#region regionName` blocks; region names follow `exportedName_variant` or `ClassName_methodName_variant` pattern (e.g., `useApp_basicUsage`, `App_hostCapabilities_checkAfterConnection`). For whole-file inclusion (any file type), omit the `#regionName`. Run `npm run sync:snippets` to sync. Standalone docs in `docs/` (listed in `typedoc.config.mjs` `projectDocuments`) can also have type-checked companion `.ts`/`.tsx` files using the same pattern. diff --git a/scripts/sync-snippets.ts b/scripts/sync-snippets.ts index 3933727a..4e9cfa8d 100644 --- a/scripts/sync-snippets.ts +++ b/scripts/sync-snippets.ts @@ -1,17 +1,29 @@ /** * Code Snippet Sync Script * - * This script syncs code snippets from `.examples.ts/.examples.tsx` files - * into JSDoc comments containing labeled code fences. + * This script syncs code snippets into JSDoc comments and markdown files + * containing labeled code fences. * - * The script replaces the content inside code fences that have a path#region - * reference in their info string. + * ## Supported Source Files + * + * - **Full-file inclusion**: Any file type (e.g., `.json`, `.yaml`, `.sh`, `.ts`) + * - **Region extraction**: Only `.ts` and `.tsx` files (using `//#region` markers) * * ## Code Fence Format * + * Full-file inclusion (any file type): + * + * ``````typescript + * ```json source="./config.json" + * // entire file content is synced here + * ``` + * `````` + * + * Region extraction (.ts/.tsx only): + * * ``````typescript * ```ts source="./path.examples.ts#regionName" - * // code is synced here + * // region content is synced here * ``` * `````` * @@ -55,10 +67,10 @@ interface LabeledCodeFence { displayName?: string; /** Relative path to the example file (e.g., "./app.examples.ts") */ examplePath: string; - /** Region name (e.g., "App_basicUsage") */ - regionName: string; - /** Language from the code fence (ts or tsx) */ - language: "ts" | "tsx"; + /** Region name (e.g., "App_basicUsage"), or undefined for whole file */ + regionName?: string; + /** Language from the code fence (e.g., "ts", "json", "yaml") */ + language: string; /** Character index of the opening fence line start */ openingFenceStart: number; /** Character index after the opening fence line (after newline) */ @@ -69,22 +81,12 @@ interface LabeledCodeFence { linePrefix: string; } -/** - * Represents extracted region content from an example file. - */ -interface RegionContent { - /** The dedented code content */ - code: string; - /** Language for code fence (ts or tsx) */ - language: "ts" | "tsx"; -} - /** * Cache for example file regions to avoid re-reading files. - * Key: absolute example file path - * Value: Map + * Key: `${absoluteExamplePath}#${regionName}` (empty regionName for whole file) + * Value: extracted code string */ -type RegionCache = Map>; +type RegionCache = Map; /** * Processing result for a source file. @@ -97,18 +99,20 @@ interface FileProcessingResult { } // JSDoc patterns - for code fences inside JSDoc comments with " * " prefix -// Matches: ``` [displayName] source="#" +// Matches: ``` [displayName] source="" or source="#" // Example: " * ```ts my-app.ts source="./app.examples.ts#App_basicUsage"" // Example: " * ```ts source="./app.examples.ts#App_basicUsage"" +// Example: " * ```ts source="./complete-example.ts"" (whole file) const JSDOC_LABELED_FENCE_PATTERN = - /^(\s*\*\s*)```(ts|tsx)(?:\s+(\S+))?\s+source="([^"#]+)#([^"]+)"/; + /^(\s*\*\s*)```(\w+)(?:\s+(\S+))?\s+source="([^"#]+)(?:#([^"]+))?"/; const JSDOC_CLOSING_FENCE_PATTERN = /^(\s*\*\s*)```\s*$/; // Markdown patterns - for plain code fences in markdown files (no prefix) -// Matches: ``` [displayName] source="#" +// Matches: ``` [displayName] source="" or source="#" // Example: ```tsx source="./patterns.tsx#chunkedDataServer" +// Example: ```tsx source="./complete-example.tsx" (whole file) const MARKDOWN_LABELED_FENCE_PATTERN = - /^```(ts|tsx)(?:\s+(\S+))?\s+source="([^"#]+)#([^"]+)"/; + /^```(\w+)(?:\s+(\S+))?\s+source="([^"#]+)(?:#([^"]+))?"/; const MARKDOWN_CLOSING_FENCE_PATTERN = /^```\s*$/; /** @@ -184,7 +188,7 @@ function findLabeledCodeFences( displayName, examplePath, regionName, - language: language as "ts" | "tsx", + language, openingFenceStart, openingFenceEnd, closingFenceStart, @@ -242,6 +246,14 @@ function extractRegion( regionName: string, examplePath: string, ): string { + // Region extraction only supported for .ts/.tsx files (uses //#region syntax) + if (!examplePath.endsWith(".ts") && !examplePath.endsWith(".tsx")) { + throw new Error( + `Region extraction (#${regionName}) is only supported for .ts/.tsx files. ` + + `Use full-file inclusion (without #regionName) for: ${examplePath}`, + ); + } + const regionStart = `//#region ${regionName}`; const regionEnd = `//#endregion ${regionName}`; @@ -281,53 +293,46 @@ function extractRegion( * Get or load a region from the cache. * @param sourceFilePath The source file requesting the region * @param examplePath The relative path to the example file - * @param regionName The region name to extract + * @param regionName The region name to extract, or undefined for whole file * @param cache The region cache - * @returns The region content + * @returns The extracted code string */ function getOrLoadRegion( sourceFilePath: string, examplePath: string, - regionName: string, + regionName: string | undefined, cache: RegionCache, -): RegionContent { +): string { // Resolve the example path relative to the source file const sourceDir = dirname(sourceFilePath); const absoluteExamplePath = resolve(sourceDir, examplePath); - // Check cache first - let fileCache = cache.get(absoluteExamplePath); - if (fileCache) { - const cached = fileCache.get(regionName); - if (cached) { - return cached; - } - } + // File content is always cached with key ending in "#" (empty region) + const fileKey = `${absoluteExamplePath}#`; + let fileContent = cache.get(fileKey); - // Load the example file - let exampleContent: string; - try { - exampleContent = readFileSync(absoluteExamplePath, "utf-8"); - } catch { - throw new Error(`Example file not found: ${absoluteExamplePath}`); + if (fileContent === undefined) { + try { + fileContent = readFileSync(absoluteExamplePath, "utf-8").trim(); + } catch { + throw new Error(`Example file not found: ${absoluteExamplePath}`); + } + cache.set(fileKey, fileContent); } - // Initialize file cache if needed - if (!fileCache) { - fileCache = new Map(); - cache.set(absoluteExamplePath, fileCache); + // If no region name, return whole file + if (!regionName) { + return fileContent; } - // Determine language from file extension - const language: "ts" | "tsx" = absoluteExamplePath.endsWith(".tsx") - ? "tsx" - : "ts"; + // Extract region from cached file content, cache the result + const regionKey = `${absoluteExamplePath}#${regionName}`; + let regionContent = cache.get(regionKey); - // Extract the region - const code = extractRegion(exampleContent, regionName, examplePath); - - const regionContent: RegionContent = { code, language }; - fileCache.set(regionName, regionContent); + if (regionContent === undefined) { + regionContent = extractRegion(fileContent, regionName, examplePath); + cache.set(regionKey, regionContent); + } return regionContent; } @@ -393,17 +398,14 @@ function processFile( const fence = fences[i]; try { - const regionContent = getOrLoadRegion( + const code = getOrLoadRegion( filePath, fence.examplePath, fence.regionName, cache, ); - const formattedCode = formatCodeLines( - regionContent.code, - fence.linePrefix, - ); + const formattedCode = formatCodeLines(code, fence.linePrefix); // Replace content between opening fence end and closing fence start content = From 2109dccadb539bbb3fede948e1d18e9cf2a8d55f Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Fri, 23 Jan 2026 18:03:33 -0600 Subject: [PATCH 3/3] Rewrite Quickstart guide with type-checked code examples Rewrites the Quickstart tutorial to provide a more focused, step-by-step introduction to building MCP Apps. The guide now uses synced code fences that pull from `examples/quickstart/`, ensuring all code shown in the documentation is type-checked and tested. Key improvements: - Clearer project setup instructions with explicit commands - Streamlined explanations of the tool + resource registration pattern - Added screenshot showing the completed app - E2E test coverage validates the example works end-to-end Co-Authored-By: Claude Opus 4.5 --- .prettierignore | 1 + docs/quickstart-success.png | Bin 0 -> 59778 bytes docs/quickstart.md | 435 ++++++++++++------ examples/basic-host/src/index.module.css | 4 +- examples/basic-server-preact/server.ts | 2 +- examples/basic-server-react/server.ts | 2 +- examples/basic-server-solid/server.ts | 2 +- examples/basic-server-svelte/server.ts | 2 +- examples/basic-server-vanillajs/server.ts | 2 +- examples/basic-server-vue/server.ts | 2 +- examples/quickstart/.gitignore | 2 + examples/quickstart/README.md | 3 + examples/quickstart/main.ts | 87 ++++ examples/quickstart/mcp-app.html | 14 + examples/quickstart/package.json | 34 ++ examples/quickstart/server.ts | 60 +++ examples/quickstart/src/mcp-app.ts | 26 ++ examples/quickstart/tsconfig.json | 19 + examples/quickstart/tsconfig.server.json | 19 + examples/quickstart/vite.config.ts | 24 + package-lock.json | 62 +++ tests/e2e/servers.spec.ts | 6 + .../servers.spec.ts-snapshots/quickstart.png | Bin 0 -> 34873 bytes 23 files changed, 672 insertions(+), 136 deletions(-) create mode 100644 docs/quickstart-success.png create mode 100644 examples/quickstart/.gitignore create mode 100644 examples/quickstart/README.md create mode 100644 examples/quickstart/main.ts create mode 100644 examples/quickstart/mcp-app.html create mode 100644 examples/quickstart/package.json create mode 100644 examples/quickstart/server.ts create mode 100644 examples/quickstart/src/mcp-app.ts create mode 100644 examples/quickstart/tsconfig.json create mode 100644 examples/quickstart/tsconfig.server.json create mode 100644 examples/quickstart/vite.config.ts create mode 100644 tests/e2e/servers.spec.ts-snapshots/quickstart.png diff --git a/.prettierignore b/.prettierignore index dd3c59ba..f5edcf9d 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,5 +2,6 @@ examples/basic-host/**/*.ts examples/basic-host/**/*.tsx examples/basic-server-*/**/*.ts examples/basic-server-*/**/*.tsx +examples/quickstart/**/*.ts **/vendor/** SKILL.md diff --git a/docs/quickstart-success.png b/docs/quickstart-success.png new file mode 100644 index 0000000000000000000000000000000000000000..058fb3f2a3cc1149a93f7e36b2de76fe4262a6f9 GIT binary patch literal 59778 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

(_8n}rJmRp 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 Zk5j`(($yLTaqta~CBpVXnVIXI{{yT(mn{GQ literal 0 HcmV?d00001 diff --git a/docs/quickstart.md b/docs/quickstart.md index 9481df31..63e93713 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -4,200 +4,351 @@ title: Quickstart # Build Your First MCP App -This tutorial walks you through building an MCP App—a tool with an interactive UI that renders inside MCP hosts like Claude Desktop. +This tutorial walks you through building an MCP App—a tool with an interactive **View** (a UI that renders inside an iframe) that displays in MCP hosts like Claude Desktop. ## What You'll Build -A simple app that fetches the current server time and displays it in a clickable UI. You'll learn the core pattern: **MCP Apps = Tool + UI Resource**. +A simple app that fetches the current server time and displays it in an interactive View. You'll learn the core pattern: **MCP Apps = Tool + UI Resource**. > [!NOTE] -> The complete example is available at [`examples/basic-server-vanillajs`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vanillajs). +> The complete example is available at [`examples/quickstart`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/quickstart). ## Prerequisites -- Familiarity with MCP concepts, especially [Tools](https://modelcontextprotocol.io/docs/learn/server-concepts#tools) and [Resources](https://modelcontextprotocol.io/docs/learn/server-concepts#resources) -- Familiarity with the [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) -- Node.js 18+ +This tutorial assumes you've built an MCP server before and are comfortable with [Tools](https://modelcontextprotocol.io/docs/learn/server-concepts#tools) and [Resources](https://modelcontextprotocol.io/docs/learn/server-concepts#resources). If not, the [official MCP quickstart](https://modelcontextprotocol.io/docs/develop/build-server) is a good place to start. -> [!TIP] -> New to building MCP servers? Start with the [official MCP quickstart guide](https://modelcontextprotocol.io/docs/develop/build-server) to learn the core concepts first. +We'll use the [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) to build the server. -## 1. Project Setup +You'll also need Node.js 18+. -Create a new directory and initialize: +## 1. Set up the project + +We'll set up a minimal TypeScript project with Vite for bundling. + +Start by creating a project directory: ```bash mkdir my-mcp-app && cd my-mcp-app +``` + +Install the dependencies you'll need: + +```bash npm init -y +npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk express cors +npm install -D typescript vite vite-plugin-singlefile @types/express @types/cors @types/node tsx concurrently cross-env ``` -Install dependencies: +Configure your [`package.json`](https://github.com/modelcontextprotocol/ext-apps/blob/main/examples/quickstart/package.json): ```bash -npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk -npm install -D typescript vite vite-plugin-singlefile express cors @types/express @types/cors tsx +npm pkg set type=module +npm pkg set scripts.build="tsc --noEmit && tsc -p tsconfig.server.json && cross-env INPUT=mcp-app.html vite build" +npm pkg set scripts.start="concurrently 'cross-env NODE_ENV=development INPUT=mcp-app.html vite build --watch' 'tsx watch main.ts'" ``` -Create `tsconfig.json`: +

+Create tsconfig.json: -```json + +```json source="../examples/quickstart/tsconfig.json" { "compilerOptions": { - "target": "ES2022", + "target": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], "module": "ESNext", "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src", "server.ts", "main.ts"] +} +``` + +
+ +
+Create tsconfig.server.json — for compiling server-side code: + + +```json source="../examples/quickstart/tsconfig.server.json" +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "./dist", + "rootDir": ".", "strict": true, "esModuleInterop": true, "skipLibCheck": true, - "outDir": "dist" + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true }, - "include": ["*.ts", "src/**/*.ts"] + "include": ["server.ts", "main.ts"] } ``` -Create `vite.config.ts` — this bundles your UI into a single HTML file: +
-```typescript +
+Create vite.config.ts — bundles UI into a single HTML file: + + +```ts source="../examples/quickstart/vite.config.ts" import { defineConfig } from "vite"; import { viteSingleFile } from "vite-plugin-singlefile"; +const INPUT = process.env.INPUT; +if (!INPUT) { + throw new Error("INPUT environment variable is not set"); +} + +const isDevelopment = process.env.NODE_ENV === "development"; + export default defineConfig({ plugins: [viteSingleFile()], build: { - outDir: "dist", + sourcemap: isDevelopment ? "inline" : undefined, + cssMinify: !isDevelopment, + minify: !isDevelopment, + rollupOptions: { - input: process.env.INPUT, + input: INPUT, }, + outDir: "dist", + emptyOutDir: false, }, }); ``` -Add to your `package.json`: +
-```json -{ - "type": "module", - "scripts": { - "build": "INPUT=mcp-app.html vite build", - "serve": "npx tsx server.ts" - } -} +Your `my-mcp-app` directory should now contain: + +``` +my-mcp-app/ +├── package.json +├── tsconfig.json +├── tsconfig.server.json +└── vite.config.ts ``` -> [!NOTE] -> **Full files:** [`package.json`](https://github.com/modelcontextprotocol/ext-apps/blob/main/examples/basic-server-vanillajs/package.json), [`tsconfig.json`](https://github.com/modelcontextprotocol/ext-apps/blob/main/examples/basic-server-vanillajs/tsconfig.json), [`vite.config.ts`](https://github.com/modelcontextprotocol/ext-apps/blob/main/examples/basic-server-vanillajs/vite.config.ts) +With the project scaffolded, let's write the server code. -## 2. Create the Server +## 2. Register the tool and UI resource MCP Apps use a **two-part registration**: 1. A **tool** that the LLM/host calls -2. A **resource** that serves the UI HTML +2. A **resource** that contains the View HTML -The tool's `_meta` field links them together. +The tool's `_meta` field links them together via the resource's URI. When an MCP Apps-capable host calls the tool, it will also read the resource and render the View. -Create `server.ts`: +Create [`server.ts`](https://github.com/modelcontextprotocol/ext-apps/blob/main/examples/quickstart/server.ts), which registers the tool and its UI resource: -```typescript -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; + +```ts source="../examples/quickstart/server.ts" import { - registerAppTool, registerAppResource, + registerAppTool, RESOURCE_MIME_TYPE, } from "@modelcontextprotocol/ext-apps/server"; -import cors from "cors"; -import express from "express"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import fs from "node:fs/promises"; import path from "node:path"; -const server = new McpServer({ - name: "My MCP App Server", - version: "1.0.0", -}); +const DIST_DIR = path.join(import.meta.dirname, "dist"); -// Two-part registration: tool + resource, tied together by the resource URI. -const resourceUri = "ui://get-time/mcp-app.html"; - -// Register a tool with UI metadata. When the host calls this tool, it reads -// `_meta.ui.resourceUri` to know which resource to fetch and render as an -// interactive UI. -registerAppTool( - server, - "get-time", - { - title: "Get Time", - description: "Returns the current server time.", - inputSchema: {}, - _meta: { ui: { resourceUri } }, - }, - async () => { - const time = new Date().toISOString(); - return { - content: [{ type: "text", text: time }], - }; - }, -); - -// Register the resource, which returns the bundled HTML/JavaScript for the UI. -registerAppResource( - server, - resourceUri, - resourceUri, - { mimeType: RESOURCE_MIME_TYPE }, - async () => { - const html = await fs.readFile( - path.join(import.meta.dirname, "dist", "mcp-app.html"), - "utf-8", - ); - return { - contents: [ - { uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }, - ], - }; - }, -); +/** + * Creates a new MCP server instance with tools and resources registered. + */ +export function createServer(): McpServer { + const server = new McpServer({ + name: "Quickstart MCP App Server", + version: "1.0.0", + }); + + // Two-part registration: tool + resource, tied together by the resource URI. + const resourceUri = "ui://get-time/mcp-app.html"; + + // Register a tool with UI metadata. When the host calls this tool, it reads + // `_meta.ui.resourceUri` to know which resource to fetch and render as an + // interactive UI. + registerAppTool( + server, + "get-time", + { + title: "Get Time", + description: "Returns the current server time.", + inputSchema: {}, + _meta: { ui: { resourceUri } }, // Links this tool to its UI resource + }, + async () => { + const time = new Date().toISOString(); + return { content: [{ type: "text", text: time }] }; + }, + ); + + // Register the resource, which returns the bundled HTML/JavaScript for the UI. + registerAppResource( + server, + resourceUri, + resourceUri, + { mimeType: RESOURCE_MIME_TYPE }, + async () => { + const html = await fs.readFile(path.join(DIST_DIR, "mcp-app.html"), "utf-8"); + + return { + contents: [ + { uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }, + ], + }; + }, + ); + + return server; +} +``` + +
+Create main.ts — the entry point that starts the server: -// Start an Express server that exposes the MCP endpoint. -const expressApp = express(); -expressApp.use(cors()); -expressApp.use(express.json()); + +```ts source="../examples/quickstart/main.ts" +import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import cors from "cors"; +import type { Request, Response } from "express"; +import { createServer } from "./server.js"; + +/** + * Starts an MCP server with Streamable HTTP transport in stateless mode. + * + * @param createServer - Factory function that creates a new McpServer instance per request. + */ +export async function startStreamableHTTPServer( + createServer: () => McpServer, +): Promise { + const port = parseInt(process.env.PORT ?? "3001", 10); + + const app = createMcpExpressApp({ host: "0.0.0.0" }); + app.use(cors()); + + app.all("/mcp", async (req: Request, res: Response) => { + const server = createServer(); + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + }); + + res.on("close", () => { + transport.close().catch(() => {}); + server.close().catch(() => {}); + }); + + try { + 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, + }); + } + } + }); -expressApp.post("/mcp", async (req, res) => { - const transport = new StreamableHTTPServerTransport({ - sessionIdGenerator: undefined, - enableJsonResponse: true, + const httpServer = app.listen(port, (err) => { + if (err) { + console.error("Failed to start server:", err); + process.exit(1); + } + console.log(`MCP server listening on http://localhost:${port}/mcp`); }); - res.on("close", () => transport.close()); - await server.connect(transport); - await transport.handleRequest(req, res, req.body); -}); -expressApp.listen(3001, (err) => { - if (err) { - console.error("Error starting server:", err); - process.exit(1); + const shutdown = () => { + console.log("\nShutting down..."); + httpServer.close(() => process.exit(0)); + }; + + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); +} + +/** + * Starts an MCP server with stdio transport. + * + * @param createServer - Factory function that creates a new McpServer instance. + */ +export async function startStdioServer( + createServer: () => McpServer, +): Promise { + await createServer().connect(new StdioServerTransport()); +} + +async function main() { + if (process.argv.includes("--stdio")) { + await startStdioServer(createServer); + } else { + await startStreamableHTTPServer(createServer); } - console.log("Server listening on http://localhost:3001/mcp"); +} + +main().catch((e) => { + console.error(e); + process.exit(1); }); ``` -> [!NOTE] -> **Full file:** [`server.ts`](https://github.com/modelcontextprotocol/ext-apps/blob/main/examples/basic-server-vanillajs/server.ts) +
+ +Your `my-mcp-app` directory should now contain: + +``` +my-mcp-app/ +├── main.ts +├── package.json +├── server.ts +├── tsconfig.json +├── tsconfig.server.json +└── vite.config.ts +``` -Then, verify your server compiles: +Let's verify everything compiles: ```bash npx tsc --noEmit ``` -No output means success. If you see errors, check for typos in `server.ts`. +No output means success! If you see errors, check for typos in `server.ts` or `main.ts`. + +The server can return the current time when the tool is called. Now let's build the UI to display it. + +## 3. Build the View -## 3. Build the UI +The View consists of an HTML page and a script that connects to the host. -Create `mcp-app.html`: +Create [`mcp-app.html`](https://github.com/modelcontextprotocol/ext-apps/blob/main/examples/quickstart/mcp-app.html), the HTML for your View: -```html + +```html source="../examples/quickstart/mcp-app.html" @@ -214,9 +365,10 @@ Create `mcp-app.html`: ``` -Create `src/mcp-app.ts`: +Create [`src/mcp-app.ts`](https://github.com/modelcontextprotocol/ext-apps/blob/main/examples/quickstart/src/mcp-app.ts), which connects to the host and handles user interactions: -```typescript + +```ts source="../examples/quickstart/src/mcp-app.ts" import { App } from "@modelcontextprotocol/ext-apps"; // Get element references @@ -226,7 +378,8 @@ const getTimeBtn = document.getElementById("get-time-btn")!; // Create app instance const app = new App({ name: "Get Time App", version: "1.0.0" }); -// Register handlers BEFORE connecting +// Handle tool results from the server. Set before `app.connect()` to avoid +// missing the initial tool result. app.ontoolresult = (result) => { const time = result.content?.find((c) => c.type === "text")?.text; serverTimeEl.textContent = time ?? "[ERROR]"; @@ -234,6 +387,7 @@ app.ontoolresult = (result) => { // Wire up button click getTimeBtn.addEventListener("click", async () => { + // `app.callServerTool()` lets the UI request fresh data from the server const result = await app.callServerTool({ name: "get-time", arguments: {} }); const time = result.content?.find((c) => c.type === "text")?.text; serverTimeEl.textContent = time ?? "[ERROR]"; @@ -243,30 +397,50 @@ getTimeBtn.addEventListener("click", async () => { app.connect(); ``` -> [!NOTE] -> **Full files:** [`mcp-app.html`](https://github.com/modelcontextprotocol/ext-apps/blob/main/examples/basic-server-vanillajs/mcp-app.html), [`src/mcp-app.ts`](https://github.com/modelcontextprotocol/ext-apps/blob/main/examples/basic-server-vanillajs/src/mcp-app.ts) +Your `my-mcp-app` directory should now contain: -Build the UI: +``` +my-mcp-app/ +├── main.ts +├── mcp-app.html +├── package.json +├── server.ts +├── src/ +│ └── mcp-app.ts +├── tsconfig.json +├── tsconfig.server.json +└── vite.config.ts +``` + +Now let's build the bundled View: ```bash npm run build ``` -This produces `dist/mcp-app.html` which contains your bundled app: +This produces `dist/mcp-app.html`: ```console -$ ls dist/mcp-app.html -dist/mcp-app.html +$ ls dist/ +mcp-app.html ``` -## 4. Test It +The View will connect to the host, receive the tool result, and display it. Let's see it in action! + +## 4. See it in action -You'll need two terminals. +You'll need two terminal windows. -**Terminal 1** — Build and start your server: +**Terminal 1** — Start your server (with watch mode): ```bash -npm run build && npm run serve +npm start +``` + +You should see: + +```console +MCP server listening on http://localhost:3001/mcp ``` **Terminal 2** — Run the test host (from the [ext-apps repo](https://github.com/modelcontextprotocol/ext-apps)): @@ -275,18 +449,23 @@ npm run build && npm run serve git clone https://github.com/modelcontextprotocol/ext-apps.git cd ext-apps/examples/basic-host npm install -npm run start +npm start ``` Open http://localhost:8080 in your browser: 1. Select **get-time** from the "Tool Name" dropdown 2. Click **Call Tool** -3. Your UI renders in the sandbox below +3. Your View renders in the sandbox below 4. Click **Get Server Time** — the current time appears! +![Your first MCP App](./quickstart-success.png) + +You've built your first MCP App! + ## Next Steps -- **Host communication**: Add [`sendMessage()`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendmessage), [`sendLog()`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendlog), and [`sendOpenLink()`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendopenlink) to interact with the host — see [`src/mcp-app.ts`](https://github.com/modelcontextprotocol/ext-apps/blob/main/examples/basic-server-vanillajs/src/mcp-app.ts) +- **Continue learning**: The [`basic-server-vanillajs`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vanillajs) example builds on this quickstart with host communication, theming, and lifecycle handlers - **React version**: Compare with [`basic-server-react`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-react) for a React-based UI +- **Other frameworks**: See also [Vue](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vue), [Svelte](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-svelte), [Preact](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-preact), and [Solid](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-solid) examples - **API reference**: See the full [API documentation](https://modelcontextprotocol.github.io/ext-apps/api/) diff --git a/examples/basic-host/src/index.module.css b/examples/basic-host/src/index.module.css index eb4d6795..ec18e2bf 100644 --- a/examples/basic-host/src/index.module.css +++ b/examples/basic-host/src/index.module.css @@ -145,11 +145,11 @@ } .appIframePanel { - min-height: 200px; + min-height: 100px; iframe { width: 100%; - height: 600px; + height: 400px; box-sizing: border-box; border: 3px dashed var(--color-border); border-radius: 4px; diff --git a/examples/basic-server-preact/server.ts b/examples/basic-server-preact/server.ts index 2d999645..bf14ea59 100644 --- a/examples/basic-server-preact/server.ts +++ b/examples/basic-server-preact/server.ts @@ -29,7 +29,7 @@ export function createServer(): McpServer { title: "Get Time", description: "Returns the current server time as an ISO 8601 string.", inputSchema: {}, - _meta: { ui: { resourceUri } }, + _meta: { ui: { resourceUri } }, // Links this tool to its UI resource }, async (): Promise => { const time = new Date().toISOString(); diff --git a/examples/basic-server-react/server.ts b/examples/basic-server-react/server.ts index 416b217f..23f6dda5 100644 --- a/examples/basic-server-react/server.ts +++ b/examples/basic-server-react/server.ts @@ -30,7 +30,7 @@ export function createServer(): McpServer { title: "Get Time", description: "Returns the current server time as an ISO 8601 string.", inputSchema: {}, - _meta: { ui: { resourceUri } }, + _meta: { ui: { resourceUri } }, // Links this tool to its UI resource }, async (): Promise => { const time = new Date().toISOString(); diff --git a/examples/basic-server-solid/server.ts b/examples/basic-server-solid/server.ts index 33f76a13..2ed2d935 100644 --- a/examples/basic-server-solid/server.ts +++ b/examples/basic-server-solid/server.ts @@ -29,7 +29,7 @@ export function createServer(): McpServer { title: "Get Time", description: "Returns the current server time as an ISO 8601 string.", inputSchema: {}, - _meta: { ui: { resourceUri } }, + _meta: { ui: { resourceUri } }, // Links this tool to its UI resource }, async (): Promise => { const time = new Date().toISOString(); diff --git a/examples/basic-server-svelte/server.ts b/examples/basic-server-svelte/server.ts index dd2dba66..da603ca8 100644 --- a/examples/basic-server-svelte/server.ts +++ b/examples/basic-server-svelte/server.ts @@ -29,7 +29,7 @@ export function createServer(): McpServer { title: "Get Time", description: "Returns the current server time as an ISO 8601 string.", inputSchema: {}, - _meta: { ui: { resourceUri } }, + _meta: { ui: { resourceUri } }, // Links this tool to its UI resource }, async (): Promise => { const time = new Date().toISOString(); diff --git a/examples/basic-server-vanillajs/server.ts b/examples/basic-server-vanillajs/server.ts index 07e82c20..5b2daf70 100644 --- a/examples/basic-server-vanillajs/server.ts +++ b/examples/basic-server-vanillajs/server.ts @@ -33,7 +33,7 @@ export function createServer(): McpServer { outputSchema: z.object({ time: z.string(), }), - _meta: { ui: { resourceUri } }, + _meta: { ui: { resourceUri } }, // Links this tool to its UI resource }, async (): Promise => { const time = new Date().toISOString(); diff --git a/examples/basic-server-vue/server.ts b/examples/basic-server-vue/server.ts index 73cb54d4..5aa72de0 100644 --- a/examples/basic-server-vue/server.ts +++ b/examples/basic-server-vue/server.ts @@ -29,7 +29,7 @@ export function createServer(): McpServer { title: "Get Time", description: "Returns the current server time as an ISO 8601 string.", inputSchema: {}, - _meta: { ui: { resourceUri } }, + _meta: { ui: { resourceUri } }, // Links this tool to its UI resource }, async (): Promise => { const time = new Date().toISOString(); diff --git a/examples/quickstart/.gitignore b/examples/quickstart/.gitignore new file mode 100644 index 00000000..b9470778 --- /dev/null +++ b/examples/quickstart/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/examples/quickstart/README.md b/examples/quickstart/README.md new file mode 100644 index 00000000..74e070db --- /dev/null +++ b/examples/quickstart/README.md @@ -0,0 +1,3 @@ +# Quickstart Server + +This is the example code for the [Quickstart guide](../../docs/quickstart.md). diff --git a/examples/quickstart/main.ts b/examples/quickstart/main.ts new file mode 100644 index 00000000..06895a49 --- /dev/null +++ b/examples/quickstart/main.ts @@ -0,0 +1,87 @@ +import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import cors from "cors"; +import type { Request, Response } from "express"; +import { createServer } from "./server.js"; + +/** + * Starts an MCP server with Streamable HTTP transport in stateless mode. + * + * @param createServer - Factory function that creates a new McpServer instance per request. + */ +export async function startStreamableHTTPServer( + createServer: () => McpServer, +): Promise { + const port = parseInt(process.env.PORT ?? "3001", 10); + + const app = createMcpExpressApp({ host: "0.0.0.0" }); + app.use(cors()); + + app.all("/mcp", async (req: Request, res: Response) => { + const server = createServer(); + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + }); + + res.on("close", () => { + transport.close().catch(() => {}); + server.close().catch(() => {}); + }); + + try { + 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, + }); + } + } + }); + + const httpServer = app.listen(port, (err) => { + if (err) { + console.error("Failed to start server:", err); + process.exit(1); + } + console.log(`MCP server listening on http://localhost:${port}/mcp`); + }); + + const shutdown = () => { + console.log("\nShutting down..."); + httpServer.close(() => process.exit(0)); + }; + + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); +} + +/** + * Starts an MCP server with stdio transport. + * + * @param createServer - Factory function that creates a new McpServer instance. + */ +export async function startStdioServer( + createServer: () => McpServer, +): Promise { + await createServer().connect(new StdioServerTransport()); +} + +async function main() { + if (process.argv.includes("--stdio")) { + await startStdioServer(createServer); + } else { + await startStreamableHTTPServer(createServer); + } +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/examples/quickstart/mcp-app.html b/examples/quickstart/mcp-app.html new file mode 100644 index 00000000..bf276aa5 --- /dev/null +++ b/examples/quickstart/mcp-app.html @@ -0,0 +1,14 @@ + + + + + Get Time App + + +

+ Server Time: Loading... +

+ + + + diff --git a/examples/quickstart/package.json b/examples/quickstart/package.json new file mode 100644 index 00000000..86933d40 --- /dev/null +++ b/examples/quickstart/package.json @@ -0,0 +1,34 @@ +{ + "name": "@modelcontextprotocol/quickstart", + "version": "0.4.1", + "type": "module", + "private": true, + "description": "Quickstart MCP App Server example", + "repository": { + "type": "git", + "url": "https://github.com/modelcontextprotocol/ext-apps", + "directory": "examples/quickstart" + }, + "license": "MIT", + "scripts": { + "build": "tsc --noEmit && tsc -p tsconfig.server.json && cross-env INPUT=mcp-app.html vite build", + "start": "concurrently 'cross-env NODE_ENV=development INPUT=mcp-app.html vite build --watch' 'tsx watch main.ts'" + }, + "dependencies": { + "@modelcontextprotocol/ext-apps": "^0.4.1", + "@modelcontextprotocol/sdk": "^1.24.0", + "cors": "^2.8.5", + "express": "^5.1.0" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "concurrently": "^9.2.1", + "@types/express": "^5.0.0", + "@types/node": "^22.0.0", + "cross-env": "^10.1.0", + "tsx": "^4.21.0", + "typescript": "^5.9.3", + "vite": "^6.0.0", + "vite-plugin-singlefile": "^2.3.0" + } +} diff --git a/examples/quickstart/server.ts b/examples/quickstart/server.ts new file mode 100644 index 00000000..eb3c3b1a --- /dev/null +++ b/examples/quickstart/server.ts @@ -0,0 +1,60 @@ +import { + registerAppResource, + registerAppTool, + RESOURCE_MIME_TYPE, +} from "@modelcontextprotocol/ext-apps/server"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import fs from "node:fs/promises"; +import path from "node:path"; + +const DIST_DIR = path.join(import.meta.dirname, "dist"); + +/** + * Creates a new MCP server instance with tools and resources registered. + */ +export function createServer(): McpServer { + const server = new McpServer({ + name: "Quickstart MCP App Server", + version: "1.0.0", + }); + + // Two-part registration: tool + resource, tied together by the resource URI. + const resourceUri = "ui://get-time/mcp-app.html"; + + // Register a tool with UI metadata. When the host calls this tool, it reads + // `_meta.ui.resourceUri` to know which resource to fetch and render as an + // interactive UI. + registerAppTool( + server, + "get-time", + { + title: "Get Time", + description: "Returns the current server time.", + inputSchema: {}, + _meta: { ui: { resourceUri } }, // Links this tool to its UI resource + }, + async () => { + const time = new Date().toISOString(); + return { content: [{ type: "text", text: time }] }; + }, + ); + + // Register the resource, which returns the bundled HTML/JavaScript for the UI. + registerAppResource( + server, + resourceUri, + resourceUri, + { mimeType: RESOURCE_MIME_TYPE }, + async () => { + const html = await fs.readFile(path.join(DIST_DIR, "mcp-app.html"), "utf-8"); + + return { + contents: [ + { uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }, + ], + }; + }, + ); + + return server; +} diff --git a/examples/quickstart/src/mcp-app.ts b/examples/quickstart/src/mcp-app.ts new file mode 100644 index 00000000..5780bb70 --- /dev/null +++ b/examples/quickstart/src/mcp-app.ts @@ -0,0 +1,26 @@ +import { App } from "@modelcontextprotocol/ext-apps"; + +// Get element references +const serverTimeEl = document.getElementById("server-time")!; +const getTimeBtn = document.getElementById("get-time-btn")!; + +// Create app instance +const app = new App({ name: "Get Time App", version: "1.0.0" }); + +// Handle tool results from the server. Set before `app.connect()` to avoid +// missing the initial tool result. +app.ontoolresult = (result) => { + const time = result.content?.find((c) => c.type === "text")?.text; + serverTimeEl.textContent = time ?? "[ERROR]"; +}; + +// Wire up button click +getTimeBtn.addEventListener("click", async () => { + // `app.callServerTool()` lets the UI request fresh data from the server + const result = await app.callServerTool({ name: "get-time", arguments: {} }); + const time = result.content?.find((c) => c.type === "text")?.text; + serverTimeEl.textContent = time ?? "[ERROR]"; +}); + +// Connect to host +app.connect(); diff --git a/examples/quickstart/tsconfig.json b/examples/quickstart/tsconfig.json new file mode 100644 index 00000000..6c553b5d --- /dev/null +++ b/examples/quickstart/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src", "server.ts", "main.ts"] +} diff --git a/examples/quickstart/tsconfig.server.json b/examples/quickstart/tsconfig.server.json new file mode 100644 index 00000000..7e65f5f7 --- /dev/null +++ b/examples/quickstart/tsconfig.server.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "./dist", + "rootDir": ".", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["server.ts", "main.ts"] +} diff --git a/examples/quickstart/vite.config.ts b/examples/quickstart/vite.config.ts new file mode 100644 index 00000000..6ff6d997 --- /dev/null +++ b/examples/quickstart/vite.config.ts @@ -0,0 +1,24 @@ +import { defineConfig } from "vite"; +import { viteSingleFile } from "vite-plugin-singlefile"; + +const INPUT = process.env.INPUT; +if (!INPUT) { + throw new Error("INPUT environment variable is not set"); +} + +const isDevelopment = process.env.NODE_ENV === "development"; + +export default defineConfig({ + plugins: [viteSingleFile()], + build: { + sourcemap: isDevelopment ? "inline" : undefined, + cssMinify: !isDevelopment, + minify: !isDevelopment, + + rollupOptions: { + input: INPUT, + }, + outDir: "dist", + emptyOutDir: false, + }, +}); diff --git a/package-lock.json b/package-lock.json index b3d94905..9907c20f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -568,6 +568,45 @@ "@modelcontextprotocol/ext-apps": "^0.4.1" } }, + "examples/quickstart": { + "name": "@modelcontextprotocol/quickstart", + "version": "0.4.1", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/ext-apps": "^0.4.1", + "@modelcontextprotocol/sdk": "^1.24.0", + "cors": "^2.8.5", + "express": "^5.1.0" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.0", + "@types/node": "^22.0.0", + "concurrently": "^9.2.1", + "cross-env": "^10.1.0", + "tsx": "^4.21.0", + "typescript": "^5.9.3", + "vite": "^6.0.0", + "vite-plugin-singlefile": "^2.3.0" + } + }, + "examples/quickstart/node_modules/@types/node": { + "version": "22.19.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz", + "integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "examples/quickstart/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "examples/say-server": { "name": "@modelcontextprotocol/server-say", "version": "0.4.1", @@ -897,6 +936,7 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -2333,11 +2373,16 @@ "resolved": "examples/basic-host", "link": true }, + "node_modules/@modelcontextprotocol/quickstart": { + "resolved": "examples/quickstart", + "link": true + }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.25.3", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.3.tgz", "integrity": "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ==", "license": "MIT", + "peer": true, "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", @@ -3417,6 +3462,7 @@ "integrity": "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", "debug": "^4.4.1", @@ -3623,6 +3669,7 @@ "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -3647,6 +3694,7 @@ "integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -4049,6 +4097,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4390,6 +4439,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5105,6 +5155,7 @@ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "dev": true, "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -5577,6 +5628,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -7126,6 +7178,7 @@ "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.2.tgz", "integrity": "sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -7335,6 +7388,7 @@ "integrity": "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -7456,6 +7510,7 @@ "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.0.tgz", "integrity": "sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw==", "license": "MIT", + "peer": true, "engines": { "node": ">=10" } @@ -7746,6 +7801,7 @@ "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.11.tgz", "integrity": "sha512-WEJtcc5mkh/BnHA6Yrg4whlF8g6QwpmXXRg4P2ztPmcKeHHlH4+djYecBLhSpecZY2RRECXYUwIc/C2r3yzQ4Q==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", @@ -7924,6 +7980,7 @@ "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.48.0.tgz", "integrity": "sha512-+NUe82VoFP1RQViZI/esojx70eazGF4u0O/9ucqZ4rPcOZD+n5EVp17uYsqwdzjUjZyTpGKunHbDziW6AIAVkQ==", "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", @@ -9013,6 +9070,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9106,6 +9164,7 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -9400,6 +9459,7 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.27.tgz", "integrity": "sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==", "license": "MIT", + "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.27", "@vue/compiler-sfc": "3.5.27", @@ -9560,6 +9620,7 @@ "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", "dev": true, "license": "ISC", + "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -9610,6 +9671,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/tests/e2e/servers.spec.ts b/tests/e2e/servers.spec.ts index 0f2d8ec1..420da936 100644 --- a/tests/e2e/servers.spec.ts +++ b/tests/e2e/servers.spec.ts @@ -17,6 +17,7 @@ const DYNAMIC_MASKS: Record = { "cohort-heatmap": ['[class*="heatmapWrapper"]'], // Heatmap grid (random data) "customer-segmentation": [".chart-container"], // Scatter plot (random data) "debug-server": ["#event-log", "#callback-table-body"], // Event log and callback counts (dynamic) + quickstart: ["#server-time"], // Server time display "say-server": [".playBtn", ".playOverlayBtn"], // Play buttons may have different states shadertoy: ["#canvas"], // WebGL shader canvas (animated) "system-monitor": [ @@ -101,6 +102,11 @@ const ALL_SERVERS = [ { key: "map-server", name: "CesiumJS Map Server", dir: "map-server" }, { key: "pdf-server", name: "PDF Server", dir: "pdf-server" }, { key: "qr-server", name: "QR Code Server", dir: "qr-server" }, + { + key: "quickstart", + name: "Quickstart MCP App Server", + dir: "quickstart", + }, { key: "say-server", name: "Say Demo", dir: "say-server" }, { key: "scenario-modeler", diff --git a/tests/e2e/servers.spec.ts-snapshots/quickstart.png b/tests/e2e/servers.spec.ts-snapshots/quickstart.png new file mode 100644 index 0000000000000000000000000000000000000000..18ad40e8cc94e33f459bd688653cc7b0f58bcd35 GIT binary patch literal 34873 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 IRk85<4;HArYybcN literal 0 HcmV?d00001