Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions examples/client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 24 additions & 10 deletions examples/client/src/elicitationUrlExample.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -273,14 +274,27 @@ async function elicitationLoop(): Promise<void> {
}

async function openBrowser(url: string): Promise<void> {
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();
}

/**
Expand Down Expand Up @@ -484,8 +498,8 @@ async function waitForOAuthCallback(): Promise<string> {
}
});

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}`);
});
});
}
Expand Down
34 changes: 24 additions & 10 deletions examples/client/src/simpleOAuthClient.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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
Expand Down Expand Up @@ -52,14 +53,27 @@ class InteractiveOAuthClient {
private async openBrowser(url: string): Promise<void> {
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
Expand Down Expand Up @@ -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}`);
});
});
}
Expand Down
4 changes: 4 additions & 0 deletions examples/server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 11 additions & 3 deletions examples/server/src/customProtocolVersion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
17 changes: 9 additions & 8 deletions examples/server/src/elicitationFormExample.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } = {};
Expand Down Expand Up @@ -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 () => {
Expand Down
45 changes: 33 additions & 12 deletions examples/server/src/elicitationUrlExample.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
);

Expand Down Expand Up @@ -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
Expand Down
30 changes: 25 additions & 5 deletions examples/server/src/honoWebStandardStreamableHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand All @@ -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
});
20 changes: 11 additions & 9 deletions examples/server/src/jsonResponseStreamableHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } = {};
Expand Down Expand Up @@ -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
Expand Down
20 changes: 11 additions & 9 deletions examples/server/src/simpleStatelessStreamableHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading