diff --git a/examples/client/README.md b/examples/client/README.md index 12a2b0d68..df893f76a 100644 --- a/examples/client/README.md +++ b/examples/client/README.md @@ -20,6 +20,8 @@ cd examples/client pnpm tsx src/simpleStreamableHttp.ts ``` +By default, examples that start a local OAuth callback server bind to `localhost`. To bind to a different interface, set `MCP_HOST` (for example `MCP_HOST=127.0.0.1`). + Most clients expect a server to be running. Start one from [`../server/README.md`](../server/README.md) (for example `src/simpleStreamableHttp.ts` in `examples/server`). ## Example index diff --git a/examples/client/src/elicitationUrlExample.ts b/examples/client/src/elicitationUrlExample.ts index f8ce2b1e1..1589844c7 100644 --- a/examples/client/src/elicitationUrlExample.ts +++ b/examples/client/src/elicitationUrlExample.ts @@ -5,7 +5,7 @@ // URL elicitation allows servers to prompt the end-user to open a URL in their browser // to collect sensitive information. -import { exec } from 'node:child_process'; +import { spawn } from 'node:child_process'; import { createServer } from 'node:http'; import { createInterface } from 'node:readline'; @@ -34,7 +34,8 @@ import { InMemoryOAuthClientProvider } from './simpleOAuthClientProvider.js'; // Set up OAuth (required for this example) const OAUTH_CALLBACK_PORT = 8090; // Use different port than auth server (3001) -const OAUTH_CALLBACK_URL = `http://localhost:${OAUTH_CALLBACK_PORT}/callback`; +const OAUTH_CALLBACK_HOST = process.env.MCP_HOST ?? 'localhost'; +const OAUTH_CALLBACK_URL = `http://${OAUTH_CALLBACK_HOST}:${OAUTH_CALLBACK_PORT}/callback`; console.log('Getting OAuth token...'); const clientMetadata: OAuthClientMetadata = { @@ -273,14 +274,27 @@ async function elicitationLoop(): Promise { } async function openBrowser(url: string): Promise { - const command = `open "${url}"`; + const platform = process.platform; + let cmd: string; + let args: string[]; + + if (platform === 'darwin') { + cmd = 'open'; + args = [url]; + } else if (platform === 'win32') { + cmd = 'cmd'; + args = ['/c', 'start', '', url]; + } else { + cmd = 'xdg-open'; + args = [url]; + } - exec(command, error => { - if (error) { - console.error(`Failed to open browser: ${error.message}`); - console.log(`Please manually open: ${url}`); - } + const child = spawn(cmd, args, { stdio: 'ignore', detached: true }); + child.on('error', error => { + console.error(`Failed to open browser: ${error.message}`); + console.log(`Please manually open: ${url}`); }); + child.unref(); } /** @@ -484,8 +498,8 @@ async function waitForOAuthCallback(): Promise { } }); - server.listen(OAUTH_CALLBACK_PORT, () => { - console.log(`OAuth callback server started on http://localhost:${OAUTH_CALLBACK_PORT}`); + server.listen(OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_HOST, () => { + console.log(`OAuth callback server started on ${OAUTH_CALLBACK_URL}`); }); }); } diff --git a/examples/client/src/simpleOAuthClient.ts b/examples/client/src/simpleOAuthClient.ts index 9ebc046b9..66c13fa81 100644 --- a/examples/client/src/simpleOAuthClient.ts +++ b/examples/client/src/simpleOAuthClient.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { exec } from 'node:child_process'; +import { spawn } from 'node:child_process'; import { createServer } from 'node:http'; import { createInterface } from 'node:readline'; import { URL } from 'node:url'; @@ -19,7 +19,8 @@ import { InMemoryOAuthClientProvider } from './simpleOAuthClientProvider.js'; // Configuration const DEFAULT_SERVER_URL = 'http://localhost:3000/mcp'; const CALLBACK_PORT = 8090; // Use different port than auth server (3001) -const CALLBACK_URL = `http://localhost:${CALLBACK_PORT}/callback`; +const CALLBACK_HOST = process.env.MCP_HOST ?? 'localhost'; +const CALLBACK_URL = `http://${CALLBACK_HOST}:${CALLBACK_PORT}/callback`; /** * Interactive MCP client with OAuth authentication @@ -52,14 +53,27 @@ class InteractiveOAuthClient { private async openBrowser(url: string): Promise { console.log(`🌐 Opening browser for authorization: ${url}`); - const command = `open "${url}"`; + const platform = process.platform; + let cmd: string; + let args: string[]; + + if (platform === 'darwin') { + cmd = 'open'; + args = [url]; + } else if (platform === 'win32') { + cmd = 'cmd'; + args = ['/c', 'start', '', url]; + } else { + cmd = 'xdg-open'; + args = [url]; + } - exec(command, error => { - if (error) { - console.error(`Failed to open browser: ${error.message}`); - console.log(`Please manually open: ${url}`); - } + const child = spawn(cmd, args, { stdio: 'ignore', detached: true }); + child.on('error', error => { + console.error(`Failed to open browser: ${error.message}`); + console.log(`Please manually open: ${url}`); }); + child.unref(); } /** * Example OAuth callback handler - in production, use a more robust approach @@ -118,8 +132,8 @@ class InteractiveOAuthClient { } }); - server.listen(CALLBACK_PORT, () => { - console.log(`OAuth callback server started on http://localhost:${CALLBACK_PORT}`); + server.listen(CALLBACK_PORT, CALLBACK_HOST, () => { + console.log(`OAuth callback server started on ${CALLBACK_URL}`); }); }); } diff --git a/examples/server/README.md b/examples/server/README.md index bfa67fd53..d4a016528 100644 --- a/examples/server/README.md +++ b/examples/server/README.md @@ -16,6 +16,10 @@ pnpm install pnpm --filter @modelcontextprotocol/examples-server exec tsx src/simpleStreamableHttp.ts ``` +By default, example servers bind to `localhost`. To bind to a different interface, set `MCP_HOST` (for example `MCP_HOST=0.0.0.0`). + +Some examples also enable demo-only CORS for browser-based clients. By default they only allow loopback origins; to allow a different origin, set `MCP_CORS_ORIGIN_REGEX` (for example `MCP_CORS_ORIGIN_REGEX=^https://chatgpt\\.com$`). + Or, from within this package: ```bash diff --git a/examples/server/src/customProtocolVersion.ts b/examples/server/src/customProtocolVersion.ts index c580432e4..6d9cc3794 100644 --- a/examples/server/src/customProtocolVersion.ts +++ b/examples/server/src/customProtocolVersion.ts @@ -51,15 +51,23 @@ const transport = new NodeStreamableHTTPServerTransport({ await server.connect(transport); // Simple HTTP server +const HOST = process.env.MCP_HOST ?? 'localhost'; const PORT = process.env.MCP_PORT ? Number.parseInt(process.env.MCP_PORT, 10) : 3000; -createServer(async (req, res) => { +const httpServer = createServer(async (req, res) => { if (req.url === '/mcp') { await transport.handleRequest(req, res); } else { res.writeHead(404).end('Not Found'); } -}).listen(PORT, () => { - console.log(`MCP server with custom protocol versions on port ${PORT}`); +}); + +httpServer.listen(PORT, HOST, () => { + console.log(`MCP server with custom protocol versions on http://${HOST}:${PORT}/mcp`); console.log(`Supported versions: ${CUSTOM_VERSIONS.join(', ')}`); }); +httpServer.on('error', error => { + console.error('Failed to start server:', error); + // eslint-disable-next-line unicorn/no-process-exit + process.exit(1); +}); diff --git a/examples/server/src/elicitationFormExample.ts b/examples/server/src/elicitationFormExample.ts index ea06331c5..9c6691d77 100644 --- a/examples/server/src/elicitationFormExample.ts +++ b/examples/server/src/elicitationFormExample.ts @@ -315,9 +315,10 @@ mcpServer.registerTool( ); async function main() { + const HOST = process.env.MCP_HOST ?? 'localhost'; const PORT = process.env.PORT ? Number.parseInt(process.env.PORT, 10) : 3000; - const app = createMcpExpressApp(); + const app = createMcpExpressApp({ host: HOST }); // Map to store transports by session ID const transports: { [sessionId: string]: NodeStreamableHTTPServerTransport } = {}; @@ -430,19 +431,19 @@ async function main() { app.delete('/mcp', mcpDeleteHandler); // Start listening - app.listen(PORT, error => { - if (error) { - console.error('Failed to start server:', error); - // eslint-disable-next-line unicorn/no-process-exit - process.exit(1); - } - console.log(`Form elicitation example server is running on http://localhost:${PORT}/mcp`); + const httpServer = app.listen(PORT, HOST, () => { + console.log(`Form elicitation example server is running on http://${HOST}:${PORT}/mcp`); console.log('Available tools:'); console.log(' - register_user: Collect user registration information'); console.log(' - create_event: Multi-step event creation'); console.log(' - update_shipping_address: Collect and validate address'); console.log('\nConnect your MCP client to this server using the HTTP transport.'); }); + httpServer.on('error', error => { + console.error('Failed to start server:', error); + // eslint-disable-next-line unicorn/no-process-exit + process.exit(1); + }); // Handle server shutdown process.on('SIGINT', async () => { diff --git a/examples/server/src/elicitationUrlExample.ts b/examples/server/src/elicitationUrlExample.ts index c38dd75e8..10aac110f 100644 --- a/examples/server/src/elicitationUrlExample.ts +++ b/examples/server/src/elicitationUrlExample.ts @@ -215,17 +215,38 @@ function completeURLElicitation(elicitationId: string) { elicitation.completeResolver(); } +const MCP_HOST = process.env.MCP_HOST ?? 'localhost'; const MCP_PORT = process.env.MCP_PORT ? Number.parseInt(process.env.MCP_PORT, 10) : 3000; const AUTH_PORT = process.env.MCP_AUTH_PORT ? Number.parseInt(process.env.MCP_AUTH_PORT, 10) : 3001; -const app = createMcpExpressApp(); +const app = createMcpExpressApp({ host: MCP_HOST }); -// Allow CORS all domains, expose the Mcp-Session-Id header +const DEFAULT_CORS_ORIGIN_REGEX = /^https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/; + +let corsOriginRegex = DEFAULT_CORS_ORIGIN_REGEX; +if (process.env.MCP_CORS_ORIGIN_REGEX) { + try { + corsOriginRegex = new RegExp(process.env.MCP_CORS_ORIGIN_REGEX); + } catch (error) { + const msg = + error && typeof error === 'object' && 'message' in error ? String((error as { message: unknown }).message) : String(error); + console.warn(`Invalid MCP_CORS_ORIGIN_REGEX (${process.env.MCP_CORS_ORIGIN_REGEX}): ${msg}`); + corsOriginRegex = DEFAULT_CORS_ORIGIN_REGEX; + } +} + +// CORS: allow only loopback origins by default (typical for local dev / Inspector direct connect). +// If you intentionally expose this demo remotely, set MCP_CORS_ORIGIN_REGEX explicitly. +// Also expose the Mcp-Session-Id header. app.use( cors({ - origin: '*', // Allow all origins + origin: (origin, cb) => { + // Allow non-browser clients (no Origin header). + if (!origin) return cb(null, true); + return cb(null, corsOriginRegex.test(origin)); + }, exposedHeaders: ['Mcp-Session-Id'], - credentials: true // Allow cookies to be sent cross-origin + credentials: true }) ); @@ -703,14 +724,14 @@ const mcpDeleteHandler = async (req: Request, res: Response) => { // Set up DELETE route with auth middleware app.delete('/mcp', authMiddleware, mcpDeleteHandler); -app.listen(MCP_PORT, error => { - if (error) { - console.error('Failed to start server:', error); - // eslint-disable-next-line unicorn/no-process-exit - process.exit(1); - } - console.log(`MCP Streamable HTTP Server listening on port ${MCP_PORT}`); - console.log(` Protected Resource Metadata: http://localhost:${MCP_PORT}/.well-known/oauth-protected-resource/mcp`); +const httpServer = app.listen(MCP_PORT, MCP_HOST, () => { + console.log(`MCP Streamable HTTP Server listening on http://${MCP_HOST}:${MCP_PORT}/mcp`); + console.log(` Protected Resource Metadata: http://${MCP_HOST}:${MCP_PORT}/.well-known/oauth-protected-resource/mcp`); +}); +httpServer.on('error', error => { + console.error('Failed to start server:', error); + // eslint-disable-next-line unicorn/no-process-exit + process.exit(1); }); // Handle server shutdown diff --git a/examples/server/src/honoWebStandardStreamableHttp.ts b/examples/server/src/honoWebStandardStreamableHttp.ts index b15f9885f..b0329b2c2 100644 --- a/examples/server/src/honoWebStandardStreamableHttp.ts +++ b/examples/server/src/honoWebStandardStreamableHttp.ts @@ -41,11 +41,29 @@ const transport = new WebStandardStreamableHTTPServerTransport(); // Create the Hono app const app = new Hono(); -// Enable CORS for all origins +const DEFAULT_CORS_ORIGIN_REGEX = /^https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/; + +let corsOriginRegex = DEFAULT_CORS_ORIGIN_REGEX; +if (process.env.MCP_CORS_ORIGIN_REGEX) { + try { + corsOriginRegex = new RegExp(process.env.MCP_CORS_ORIGIN_REGEX); + } catch (error) { + const msg = + error && typeof error === 'object' && 'message' in error ? String((error as { message: unknown }).message) : String(error); + console.warn(`Invalid MCP_CORS_ORIGIN_REGEX (${process.env.MCP_CORS_ORIGIN_REGEX}): ${msg}`); + corsOriginRegex = DEFAULT_CORS_ORIGIN_REGEX; + } +} + +// CORS: allow only loopback origins by default (typical for local dev / Inspector direct connect). +// If you intentionally expose this demo remotely, set MCP_CORS_ORIGIN_REGEX explicitly. app.use( '*', cors({ - origin: '*', + origin: (origin, _c) => { + if (!origin) return null; + return corsOriginRegex.test(origin) ? origin : null; + }, allowMethods: ['GET', 'POST', 'DELETE', 'OPTIONS'], allowHeaders: ['Content-Type', 'mcp-session-id', 'Last-Event-ID', 'mcp-protocol-version'], exposeHeaders: ['mcp-session-id', 'mcp-protocol-version'] @@ -59,15 +77,17 @@ app.get('/health', c => c.json({ status: 'ok' })); app.all('/mcp', c => transport.handleRequest(c.req.raw)); // Start the server +const HOST = process.env.MCP_HOST ?? 'localhost'; const PORT = process.env.MCP_PORT ? Number.parseInt(process.env.MCP_PORT, 10) : 3000; await server.connect(transport); -console.log(`Starting Hono MCP server on port ${PORT}`); -console.log(`Health check: http://localhost:${PORT}/health`); -console.log(`MCP endpoint: http://localhost:${PORT}/mcp`); +console.log(`Starting Hono MCP server on http://${HOST}:${PORT}`); +console.log(`Health check: http://${HOST}:${PORT}/health`); +console.log(`MCP endpoint: http://${HOST}:${PORT}/mcp`); serve({ fetch: app.fetch, + hostname: HOST, port: PORT }); diff --git a/examples/server/src/jsonResponseStreamableHttp.ts b/examples/server/src/jsonResponseStreamableHttp.ts index 7a3aad67a..3e97b19f7 100644 --- a/examples/server/src/jsonResponseStreamableHttp.ts +++ b/examples/server/src/jsonResponseStreamableHttp.ts @@ -77,7 +77,10 @@ const getServer = () => { return server; }; -const app = createMcpExpressApp(); +const HOST = process.env.MCP_HOST ?? 'localhost'; +const PORT = process.env.MCP_PORT ? Number.parseInt(process.env.MCP_PORT, 10) : 3000; + +const app = createMcpExpressApp({ host: HOST }); // Map to store transports by session ID const transports: { [sessionId: string]: NodeStreamableHTTPServerTransport } = {}; @@ -148,14 +151,13 @@ app.get('/mcp', async (req: Request, res: Response) => { }); // Start the server -const PORT = 3000; -app.listen(PORT, error => { - if (error) { - console.error('Failed to start server:', error); - // eslint-disable-next-line unicorn/no-process-exit - process.exit(1); - } - console.log(`MCP Streamable HTTP Server listening on port ${PORT}`); +const server = app.listen(PORT, HOST, () => { + console.log(`MCP Streamable HTTP Server listening on http://${HOST}:${PORT}/mcp`); +}); +server.on('error', error => { + console.error('Failed to start server:', error); + // eslint-disable-next-line unicorn/no-process-exit + process.exit(1); }); // Handle server shutdown diff --git a/examples/server/src/simpleStatelessStreamableHttp.ts b/examples/server/src/simpleStatelessStreamableHttp.ts index 2b4f0363d..924269bad 100644 --- a/examples/server/src/simpleStatelessStreamableHttp.ts +++ b/examples/server/src/simpleStatelessStreamableHttp.ts @@ -94,7 +94,10 @@ const getServer = () => { return server; }; -const app = createMcpExpressApp(); +const HOST = process.env.MCP_HOST ?? 'localhost'; +const PORT = process.env.MCP_PORT ? Number.parseInt(process.env.MCP_PORT, 10) : 3000; + +const app = createMcpExpressApp({ host: HOST }); app.post('/mcp', async (req: Request, res: Response) => { const server = getServer(); @@ -153,14 +156,13 @@ app.delete('/mcp', async (req: Request, res: Response) => { }); // Start the server -const PORT = 3000; -app.listen(PORT, error => { - if (error) { - console.error('Failed to start server:', error); - // eslint-disable-next-line unicorn/no-process-exit - process.exit(1); - } - console.log(`MCP Stateless Streamable HTTP Server listening on port ${PORT}`); +const server = app.listen(PORT, HOST, () => { + console.log(`MCP Stateless Streamable HTTP Server listening on http://${HOST}:${PORT}/mcp`); +}); +server.on('error', error => { + console.error('Failed to start server:', error); + // eslint-disable-next-line unicorn/no-process-exit + process.exit(1); }); // Handle server shutdown diff --git a/examples/server/src/simpleStreamableHttp.ts b/examples/server/src/simpleStreamableHttp.ts index dc86b17bd..792be0b36 100644 --- a/examples/server/src/simpleStreamableHttp.ts +++ b/examples/server/src/simpleStreamableHttp.ts @@ -497,18 +497,37 @@ const getServer = () => { return server; }; +const MCP_HOST = process.env.MCP_HOST ?? 'localhost'; const MCP_PORT = process.env.MCP_PORT ? Number.parseInt(process.env.MCP_PORT, 10) : 3000; const AUTH_PORT = process.env.MCP_AUTH_PORT ? Number.parseInt(process.env.MCP_AUTH_PORT, 10) : 3001; -const app = createMcpExpressApp(); +const app = createMcpExpressApp({ host: MCP_HOST }); -// Enable CORS for browser-based clients (demo only) -// This allows cross-origin requests and exposes WWW-Authenticate header for OAuth -// WARNING: This configuration is for demo purposes only. In production, you should restrict this to specific origins and configure CORS yourself. +const DEFAULT_CORS_ORIGIN_REGEX = /^https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/; + +let corsOriginRegex = DEFAULT_CORS_ORIGIN_REGEX; +if (process.env.MCP_CORS_ORIGIN_REGEX) { + try { + corsOriginRegex = new RegExp(process.env.MCP_CORS_ORIGIN_REGEX); + } catch (error) { + const msg = + error && typeof error === 'object' && 'message' in error ? String((error as { message: unknown }).message) : String(error); + console.warn(`Invalid MCP_CORS_ORIGIN_REGEX (${process.env.MCP_CORS_ORIGIN_REGEX}): ${msg}`); + corsOriginRegex = DEFAULT_CORS_ORIGIN_REGEX; + } +} + +// CORS: allow only loopback origins by default (typical for local dev / Inspector direct connect). +// If you intentionally expose this demo remotely, set MCP_CORS_ORIGIN_REGEX explicitly. +// This also exposes WWW-Authenticate for OAuth flows. app.use( cors({ exposedHeaders: ['WWW-Authenticate', 'Mcp-Session-Id', 'Last-Event-Id', 'Mcp-Protocol-Version'], - origin: '*' // WARNING: This allows all origins to access the MCP server. In production, you should restrict this to specific origins. + origin: (origin, cb) => { + // Allow non-browser clients (no Origin header). + if (!origin) return cb(null, true); + return cb(null, corsOriginRegex.test(origin)); + } }) ); @@ -681,17 +700,17 @@ if (useOAuth && authMiddleware) { app.delete('/mcp', mcpDeleteHandler); } -app.listen(MCP_PORT, error => { - if (error) { - console.error('Failed to start server:', error); - // eslint-disable-next-line unicorn/no-process-exit - process.exit(1); - } - console.log(`MCP Streamable HTTP Server listening on port ${MCP_PORT}`); +const httpServer = app.listen(MCP_PORT, MCP_HOST, () => { + console.log(`MCP Streamable HTTP Server listening on http://${MCP_HOST}:${MCP_PORT}/mcp`); if (useOAuth) { - console.log(` Protected Resource Metadata: http://localhost:${MCP_PORT}/.well-known/oauth-protected-resource/mcp`); + console.log(` Protected Resource Metadata: http://${MCP_HOST}:${MCP_PORT}/.well-known/oauth-protected-resource/mcp`); } }); +httpServer.on('error', error => { + console.error('Failed to start server:', error); + // eslint-disable-next-line unicorn/no-process-exit + process.exit(1); +}); // Handle server shutdown process.on('SIGINT', async () => { diff --git a/examples/server/src/simpleTaskInteractive.ts b/examples/server/src/simpleTaskInteractive.ts index 9092926d9..5de1d2c7d 100644 --- a/examples/server/src/simpleTaskInteractive.ts +++ b/examples/server/src/simpleTaskInteractive.ts @@ -443,6 +443,7 @@ class TaskSession { // Server Setup // ============================================================================ +const HOST = process.env.MCP_HOST ?? 'localhost'; const PORT = process.env.PORT ? Number.parseInt(process.env.PORT, 10) : 8000; // Create shared stores @@ -630,7 +631,7 @@ const createServer = (): Server => { // Express App Setup // ============================================================================ -const app = createMcpExpressApp(); +const app = createMcpExpressApp({ host: HOST }); // Map to store transports by session ID const transports: { [sessionId: string]: NodeStreamableHTTPServerTransport } = {}; @@ -718,12 +719,17 @@ app.delete('/mcp', async (req: Request, res: Response) => { }); // Start server -app.listen(PORT, () => { - console.log(`Starting server on http://localhost:${PORT}/mcp`); +const httpServer = app.listen(PORT, HOST, () => { + console.log(`Starting server on http://${HOST}:${PORT}/mcp`); console.log('\nAvailable tools:'); console.log(' - confirm_delete: Demonstrates elicitation (asks user y/n)'); console.log(' - write_haiku: Demonstrates sampling (requests LLM completion)'); }); +httpServer.on('error', error => { + console.error('Failed to start server:', error); + // eslint-disable-next-line unicorn/no-process-exit + process.exit(1); +}); // Handle shutdown process.on('SIGINT', async () => { diff --git a/examples/server/src/ssePollingExample.ts b/examples/server/src/ssePollingExample.ts index 3897b392e..98cdd509a 100644 --- a/examples/server/src/ssePollingExample.ts +++ b/examples/server/src/ssePollingExample.ts @@ -83,8 +83,33 @@ server.registerTool( ); // Set up Express app -const app = createMcpExpressApp(); -app.use(cors()); +const HOST = process.env.MCP_HOST ?? 'localhost'; +const PORT = process.env.MCP_PORT ? Number.parseInt(process.env.MCP_PORT, 10) : 3001; + +const app = createMcpExpressApp({ host: HOST }); +const DEFAULT_CORS_ORIGIN_REGEX = /^https?:\/\/(?:localhost|127\.0\.0\.1)(?::\d+)?$/; + +let corsOriginRegex = DEFAULT_CORS_ORIGIN_REGEX; +if (process.env.MCP_CORS_ORIGIN_REGEX) { + try { + corsOriginRegex = new RegExp(process.env.MCP_CORS_ORIGIN_REGEX); + } catch (error) { + const msg = + error && typeof error === 'object' && 'message' in error ? String((error as { message: unknown }).message) : String(error); + console.warn(`Invalid MCP_CORS_ORIGIN_REGEX (${process.env.MCP_CORS_ORIGIN_REGEX}): ${msg}`); + corsOriginRegex = DEFAULT_CORS_ORIGIN_REGEX; + } +} + +app.use( + cors({ + origin: (origin, cb) => { + // Allow non-browser clients (no Origin header). + if (!origin) return cb(null, true); + return cb(null, corsOriginRegex.test(origin)); + } + }) +); // Create event store for resumability const eventStore = new InMemoryEventStore(); @@ -118,9 +143,8 @@ app.all('/mcp', async (req: Request, res: Response) => { }); // Start the server -const PORT = 3001; -app.listen(PORT, () => { - console.log(`SSE Polling Example Server running on http://localhost:${PORT}/mcp`); +const httpServer = app.listen(PORT, HOST, () => { + console.log(`SSE Polling Example Server running on http://${HOST}:${PORT}/mcp`); console.log(''); console.log('This server demonstrates SEP-1699 SSE polling:'); console.log('- retryInterval: 2000ms (client waits 2s before reconnecting)'); @@ -128,3 +152,8 @@ app.listen(PORT, () => { console.log(''); console.log('Try calling the "long-task" tool to see server-initiated disconnect in action.'); }); +httpServer.on('error', error => { + console.error('Failed to start server:', error); + // eslint-disable-next-line unicorn/no-process-exit + process.exit(1); +}); diff --git a/examples/server/src/standaloneSseWithGetStreamableHttp.ts b/examples/server/src/standaloneSseWithGetStreamableHttp.ts index 1fd351ef1..5b02de623 100644 --- a/examples/server/src/standaloneSseWithGetStreamableHttp.ts +++ b/examples/server/src/standaloneSseWithGetStreamableHttp.ts @@ -36,7 +36,10 @@ const resourceChangeInterval = setInterval(() => { addResource(name, `Content for ${name}`); }, 5000); // Change resources every 5 seconds for testing -const app = createMcpExpressApp(); +const HOST = process.env.MCP_HOST ?? 'localhost'; +const PORT = process.env.MCP_PORT ? Number.parseInt(process.env.MCP_PORT, 10) : 3000; + +const app = createMcpExpressApp({ host: HOST }); app.post('/mcp', async (req: Request, res: Response) => { console.log('Received MCP request:', req.body); @@ -110,14 +113,13 @@ app.get('/mcp', async (req: Request, res: Response) => { }); // Start the server -const PORT = 3000; -app.listen(PORT, error => { - if (error) { - console.error('Failed to start server:', error); - // eslint-disable-next-line unicorn/no-process-exit - process.exit(1); - } - console.log(`Server listening on port ${PORT}`); +const httpServer = app.listen(PORT, HOST, () => { + console.log(`Server listening on http://${HOST}:${PORT}/mcp`); +}); +httpServer.on('error', error => { + console.error('Failed to start server:', error); + // eslint-disable-next-line unicorn/no-process-exit + process.exit(1); }); // Handle server shutdown diff --git a/examples/shared/src/authServer.ts b/examples/shared/src/authServer.ts index e967b23d9..daffc985a 100644 --- a/examples/shared/src/authServer.ts +++ b/examples/shared/src/authServer.ts @@ -38,6 +38,30 @@ export interface SetupAuthServerOptions { let globalAuth: DemoAuth | null = null; let demoUserCreated = false; +const DEFAULT_CORS_ORIGIN_REGEX = /^https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/; + +function buildCorsMiddleware(): ReturnType { + let corsOriginRegex = DEFAULT_CORS_ORIGIN_REGEX; + if (process.env.MCP_CORS_ORIGIN_REGEX) { + try { + corsOriginRegex = new RegExp(process.env.MCP_CORS_ORIGIN_REGEX); + } catch (error) { + const msg = + error && typeof error === 'object' && 'message' in error ? String((error as { message: unknown }).message) : String(error); + console.warn(`Invalid MCP_CORS_ORIGIN_REGEX (${process.env.MCP_CORS_ORIGIN_REGEX}): ${msg}`); + corsOriginRegex = DEFAULT_CORS_ORIGIN_REGEX; + } + } + + return cors({ + origin: (origin, cb) => { + // Allow non-browser clients (no Origin header). + if (!origin) return cb(null, true); + return cb(null, corsOriginRegex.test(origin)); + } + }); +} + /** * Gets the global auth instance (must call setupAuthServer first) */ @@ -102,13 +126,11 @@ export function setupAuthServer(options: SetupAuthServerOptions): void { // Create Express app for auth server const authApp = express(); - // Enable CORS for all origins (demo only) - must be before other middleware - // WARNING: This configuration is for demo purposes only. In production, you should restrict this to specific origins and configure CORS yourself. - authApp.use( - cors({ - origin: '*' // WARNING: This allows all origins to access the auth server. In production, you should restrict this to specific origins. - }) - ); + // CORS: allow only loopback origins by default (typical for local dev / Inspector direct connect). + // If you intentionally expose this demo remotely, set MCP_CORS_ORIGIN_REGEX explicitly. + // Must be before other middleware. + const corsMw = buildCorsMiddleware(); + authApp.use(corsMw); // Create better-auth handler // toNodeHandler bypasses Express methods @@ -163,12 +185,13 @@ export function setupAuthServer(options: SetupAuthServerOptions): void { // OAuth metadata endpoints using better-auth's built-in handlers // Add explicit OPTIONS handler for CORS preflight - authApp.options('/.well-known/oauth-authorization-server', cors()); - authApp.get('/.well-known/oauth-authorization-server', cors(), toNodeHandler(oAuthDiscoveryMetadata(auth))); + authApp.options('/.well-known/oauth-authorization-server', corsMw); + authApp.get('/.well-known/oauth-authorization-server', corsMw, toNodeHandler(oAuthDiscoveryMetadata(auth))); // Body parsers for non-better-auth routes (like /sign-in) - authApp.use(express.json()); - authApp.use(express.urlencoded({ extended: true })); + const maxBodyBytes = 100 * 1024; // Make the default explicit to avoid accidental large-body DoS. + authApp.use(express.json({ limit: maxBodyBytes })); + authApp.use(express.urlencoded({ extended: true, limit: maxBodyBytes })); // Auto-login page that creates a real better-auth session // This simulates a user logging in and approving the OAuth request @@ -240,17 +263,18 @@ export function setupAuthServer(options: SetupAuthServerOptions): void { // Start the auth server const authPort = Number.parseInt(authServerUrl.port, 10); - authApp.listen(authPort, (error?: Error) => { - if (error) { - console.error('Failed to start auth server:', error); - // eslint-disable-next-line unicorn/no-process-exit - process.exit(1); - } + const authHost = authServerUrl.hostname || 'localhost'; + const server = authApp.listen(authPort, authHost, () => { console.log(`OAuth Authorization Server listening on port ${authPort}`); console.log(` Authorization: ${authServerUrl}api/auth/mcp/authorize`); console.log(` Token: ${authServerUrl}api/auth/mcp/token`); console.log(` Metadata: ${authServerUrl}.well-known/oauth-authorization-server`); }); + server.on('error', error => { + console.error('Failed to start auth server:', error); + // eslint-disable-next-line unicorn/no-process-exit + process.exit(1); + }); } /** @@ -271,14 +295,15 @@ export function setupAuthServer(options: SetupAuthServerOptions): void { export function createProtectedResourceMetadataRouter(resourcePath = '/mcp'): Router { const auth = getAuth(); const router = express.Router(); + const corsMw = buildCorsMiddleware(); // Construct the metadata path per RFC 9728 Section 3 const metadataPath = `/.well-known/oauth-protected-resource${resourcePath}`; // Enable CORS for browser-based clients to discover the auth server // Add explicit OPTIONS handler for CORS preflight - router.options(metadataPath, cors()); - router.get(metadataPath, cors(), toNodeHandler(oAuthProtectedResourceMetadata(auth))); + router.options(metadataPath, corsMw); + router.get(metadataPath, corsMw, toNodeHandler(oAuthProtectedResourceMetadata(auth))); return router; }