| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656 |
- "use strict";
- // Run with: npx tsx src/examples/server/elicitationUrlExample.ts
- //
- // This example demonstrates how to use URL elicitation to securely collect
- // *sensitive* user input in a remote (HTTP) server.
- // URL elicitation allows servers to prompt the end-user to open a URL in their browser
- // to collect sensitive information.
- // Note: See also elicitationFormExample.ts for an example of using form (not URL) elicitation
- // to collect *non-sensitive* user input with a structured schema.
- var __importDefault = (this && this.__importDefault) || function (mod) {
- return (mod && mod.__esModule) ? mod : { "default": mod };
- };
- Object.defineProperty(exports, "__esModule", { value: true });
- const express_1 = __importDefault(require("express"));
- const node_crypto_1 = require("node:crypto");
- const zod_1 = require("zod");
- const mcp_js_1 = require("../../server/mcp.js");
- const express_js_1 = require("../../server/express.js");
- const streamableHttp_js_1 = require("../../server/streamableHttp.js");
- const router_js_1 = require("../../server/auth/router.js");
- const bearerAuth_js_1 = require("../../server/auth/middleware/bearerAuth.js");
- const types_js_1 = require("../../types.js");
- const inMemoryEventStore_js_1 = require("../shared/inMemoryEventStore.js");
- const demoInMemoryOAuthProvider_js_1 = require("./demoInMemoryOAuthProvider.js");
- const auth_utils_js_1 = require("../../shared/auth-utils.js");
- const cors_1 = __importDefault(require("cors"));
- // Create an MCP server with implementation details
- const getServer = () => {
- const mcpServer = new mcp_js_1.McpServer({
- name: 'url-elicitation-http-server',
- version: '1.0.0'
- }, {
- capabilities: { logging: {} }
- });
- mcpServer.registerTool('payment-confirm', {
- description: 'A tool that confirms a payment directly with a user',
- inputSchema: {
- cartId: zod_1.z.string().describe('The ID of the cart to confirm')
- }
- }, async ({ cartId }, extra) => {
- /*
- In a real world scenario, there would be some logic here to check if the user has the provided cartId.
- For the purposes of this example, we'll throw an error (-> elicits the client to open a URL to confirm payment)
- */
- const sessionId = extra.sessionId;
- if (!sessionId) {
- throw new Error('Expected a Session ID');
- }
- // Create and track the elicitation
- const elicitationId = generateTrackedElicitation(sessionId, elicitationId => mcpServer.server.createElicitationCompletionNotifier(elicitationId));
- throw new types_js_1.UrlElicitationRequiredError([
- {
- mode: 'url',
- message: 'This tool requires a payment confirmation. Open the link to confirm payment!',
- url: `http://localhost:${MCP_PORT}/confirm-payment?session=${sessionId}&elicitation=${elicitationId}&cartId=${encodeURIComponent(cartId)}`,
- elicitationId
- }
- ]);
- });
- mcpServer.registerTool('third-party-auth', {
- description: 'A demo tool that requires third-party OAuth credentials',
- inputSchema: {
- param1: zod_1.z.string().describe('First parameter')
- }
- }, async (_, extra) => {
- /*
- In a real world scenario, there would be some logic here to check if we already have a valid access token for the user.
- Auth info (with a subject or `sub` claim) can be typically be found in `extra.authInfo`.
- If we do, we can just return the result of the tool call.
- If we don't, we can throw an ElicitationRequiredError to request the user to authenticate.
- For the purposes of this example, we'll throw an error (-> elicits the client to open a URL to authenticate).
- */
- const sessionId = extra.sessionId;
- if (!sessionId) {
- throw new Error('Expected a Session ID');
- }
- // Create and track the elicitation
- const elicitationId = generateTrackedElicitation(sessionId, elicitationId => mcpServer.server.createElicitationCompletionNotifier(elicitationId));
- // Simulate OAuth callback and token exchange after 5 seconds
- // In a real app, this would be called from your OAuth callback handler
- setTimeout(() => {
- console.log(`Simulating OAuth token received for elicitation ${elicitationId}`);
- completeURLElicitation(elicitationId);
- }, 5000);
- throw new types_js_1.UrlElicitationRequiredError([
- {
- mode: 'url',
- message: 'This tool requires access to your example.com account. Open the link to authenticate!',
- url: 'https://www.example.com/oauth/authorize',
- elicitationId
- }
- ]);
- });
- return mcpServer;
- };
- const elicitationsMap = new Map();
- // Clean up old elicitations after 1 hour to prevent memory leaks
- const ELICITATION_TTL_MS = 60 * 60 * 1000; // 1 hour
- const CLEANUP_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes
- function cleanupOldElicitations() {
- const now = new Date();
- for (const [id, metadata] of elicitationsMap.entries()) {
- if (now.getTime() - metadata.createdAt.getTime() > ELICITATION_TTL_MS) {
- elicitationsMap.delete(id);
- console.log(`Cleaned up expired elicitation: ${id}`);
- }
- }
- }
- setInterval(cleanupOldElicitations, CLEANUP_INTERVAL_MS);
- /**
- * Elicitation IDs must be unique strings within the MCP session
- * UUIDs are used in this example for simplicity
- */
- function generateElicitationId() {
- return (0, node_crypto_1.randomUUID)();
- }
- /**
- * Helper function to create and track a new elicitation.
- */
- function generateTrackedElicitation(sessionId, createCompletionNotifier) {
- const elicitationId = generateElicitationId();
- // Create a Promise and its resolver for tracking completion
- let completeResolver;
- const completedPromise = new Promise(resolve => {
- completeResolver = resolve;
- });
- const completionNotifier = createCompletionNotifier ? createCompletionNotifier(elicitationId) : undefined;
- // Store the elicitation in our map
- elicitationsMap.set(elicitationId, {
- status: 'pending',
- completedPromise,
- completeResolver: completeResolver,
- createdAt: new Date(),
- sessionId,
- completionNotifier
- });
- return elicitationId;
- }
- /**
- * Helper function to complete an elicitation.
- */
- function completeURLElicitation(elicitationId) {
- const elicitation = elicitationsMap.get(elicitationId);
- if (!elicitation) {
- console.warn(`Attempted to complete unknown elicitation: ${elicitationId}`);
- return;
- }
- if (elicitation.status === 'complete') {
- console.warn(`Elicitation already complete: ${elicitationId}`);
- return;
- }
- // Update metadata
- elicitation.status = 'complete';
- // Send completion notification to the client
- if (elicitation.completionNotifier) {
- console.log(`Sending notifications/elicitation/complete notification for elicitation ${elicitationId}`);
- elicitation.completionNotifier().catch(error => {
- console.error(`Failed to send completion notification for elicitation ${elicitationId}:`, error);
- });
- }
- // Resolve the promise to unblock any waiting code
- elicitation.completeResolver();
- }
- const MCP_PORT = process.env.MCP_PORT ? parseInt(process.env.MCP_PORT, 10) : 3000;
- const AUTH_PORT = process.env.MCP_AUTH_PORT ? parseInt(process.env.MCP_AUTH_PORT, 10) : 3001;
- const app = (0, express_js_1.createMcpExpressApp)();
- // Allow CORS all domains, expose the Mcp-Session-Id header
- app.use((0, cors_1.default)({
- origin: '*', // Allow all origins
- exposedHeaders: ['Mcp-Session-Id'],
- credentials: true // Allow cookies to be sent cross-origin
- }));
- // Set up OAuth (required for this example)
- let authMiddleware = null;
- // Create auth middleware for MCP endpoints
- const mcpServerUrl = new URL(`http://localhost:${MCP_PORT}/mcp`);
- const authServerUrl = new URL(`http://localhost:${AUTH_PORT}`);
- const oauthMetadata = (0, demoInMemoryOAuthProvider_js_1.setupAuthServer)({ authServerUrl, mcpServerUrl, strictResource: true });
- const tokenVerifier = {
- verifyAccessToken: async (token) => {
- const endpoint = oauthMetadata.introspection_endpoint;
- if (!endpoint) {
- throw new Error('No token verification endpoint available in metadata');
- }
- const response = await fetch(endpoint, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded'
- },
- body: new URLSearchParams({
- token: token
- }).toString()
- });
- if (!response.ok) {
- const text = await response.text().catch(() => null);
- throw new Error(`Invalid or expired token: ${text}`);
- }
- const data = await response.json();
- if (!data.aud) {
- throw new Error(`Resource Indicator (RFC8707) missing`);
- }
- if (!(0, auth_utils_js_1.checkResourceAllowed)({ requestedResource: data.aud, configuredResource: mcpServerUrl })) {
- throw new Error(`Expected resource indicator ${mcpServerUrl}, got: ${data.aud}`);
- }
- // Convert the response to AuthInfo format
- return {
- token,
- clientId: data.client_id,
- scopes: data.scope ? data.scope.split(' ') : [],
- expiresAt: data.exp
- };
- }
- };
- // Add metadata routes to the main MCP server
- app.use((0, router_js_1.mcpAuthMetadataRouter)({
- oauthMetadata,
- resourceServerUrl: mcpServerUrl,
- scopesSupported: ['mcp:tools'],
- resourceName: 'MCP Demo Server'
- }));
- authMiddleware = (0, bearerAuth_js_1.requireBearerAuth)({
- verifier: tokenVerifier,
- requiredScopes: [],
- resourceMetadataUrl: (0, router_js_1.getOAuthProtectedResourceMetadataUrl)(mcpServerUrl)
- });
- /**
- * API Key Form Handling
- *
- * Many servers today require an API key to operate, but there's no scalable way to do this dynamically for remote servers within MCP protocol.
- * URL-mode elicitation enables the server to host a simple form and get the secret data securely from the user without involving the LLM or client.
- **/
- async function sendApiKeyElicitation(sessionId, sender, createCompletionNotifier) {
- if (!sessionId) {
- console.error('No session ID provided');
- throw new Error('Expected a Session ID to track elicitation');
- }
- console.log('🔑 URL elicitation demo: Requesting API key from client...');
- const elicitationId = generateTrackedElicitation(sessionId, createCompletionNotifier);
- try {
- const result = await sender({
- mode: 'url',
- message: 'Please provide your API key to authenticate with this server',
- // Host the form on the same server. In a real app, you might coordinate passing these state variables differently.
- url: `http://localhost:${MCP_PORT}/api-key-form?session=${sessionId}&elicitation=${elicitationId}`,
- elicitationId
- });
- switch (result.action) {
- case 'accept':
- console.log('🔑 URL elicitation demo: Client accepted the API key elicitation (now pending form submission)');
- // Wait for the API key to be submitted via the form
- // The form submission will complete the elicitation
- break;
- default:
- console.log('🔑 URL elicitation demo: Client declined to provide an API key');
- // In a real app, this might close the connection, but for the demo, we'll continue
- break;
- }
- }
- catch (error) {
- console.error('Error during API key elicitation:', error);
- }
- }
- // API Key Form endpoint - serves a simple HTML form
- app.get('/api-key-form', (req, res) => {
- const mcpSessionId = req.query.session;
- const elicitationId = req.query.elicitation;
- if (!mcpSessionId || !elicitationId) {
- res.status(400).send('<h1>Error</h1><p>Missing required parameters</p>');
- return;
- }
- // Check for user session cookie
- // In production, this is often handled by some user auth middleware to ensure the user has a valid session
- // This session is different from the MCP session.
- // This userSession is the cookie that the MCP Server's Authorization Server sets for the user when they log in.
- const userSession = getUserSessionCookie(req.headers.cookie);
- if (!userSession) {
- res.status(401).send('<h1>Error</h1><p>Unauthorized - please reconnect to login again</p>');
- return;
- }
- // Serve a simple HTML form
- res.send(`
- <!DOCTYPE html>
- <html>
- <head>
- <title>Submit Your API Key</title>
- <style>
- body { font-family: sans-serif; max-width: 400px; margin: 50px auto; padding: 20px; }
- input[type="text"] { width: 100%; padding: 8px; margin: 10px 0; box-sizing: border-box; }
- button { background: #007bff; color: white; padding: 10px 20px; border: none; cursor: pointer; }
- button:hover { background: #0056b3; }
- .user { background: #d1ecf1; padding: 8px; margin-bottom: 10px; }
- .info { color: #666; font-size: 0.9em; margin-top: 20px; }
- </style>
- </head>
- <body>
- <h1>API Key Required</h1>
- <div class="user">✓ Logged in as: <strong>${userSession.name}</strong></div>
- <form method="POST" action="/api-key-form">
- <input type="hidden" name="session" value="${mcpSessionId}" />
- <input type="hidden" name="elicitation" value="${elicitationId}" />
- <label>API Key:<br>
- <input type="text" name="apiKey" required placeholder="Enter your API key" />
- </label>
- <button type="submit">Submit</button>
- </form>
- <div class="info">This is a demo showing how a server can securely elicit sensitive data from a user using a URL.</div>
- </body>
- </html>
- `);
- });
- // Handle API key form submission
- app.post('/api-key-form', express_1.default.urlencoded(), (req, res) => {
- const { session: sessionId, apiKey, elicitation: elicitationId } = req.body;
- if (!sessionId || !apiKey || !elicitationId) {
- res.status(400).send('<h1>Error</h1><p>Missing required parameters</p>');
- return;
- }
- // Check for user session cookie here too
- const userSession = getUserSessionCookie(req.headers.cookie);
- if (!userSession) {
- res.status(401).send('<h1>Error</h1><p>Unauthorized - please reconnect to login again</p>');
- return;
- }
- // A real app might store this API key to be used later for the user.
- console.log(`🔑 Received API key \x1b[32m${apiKey}\x1b[0m for session ${sessionId}`);
- // If we have an elicitationId, complete the elicitation
- completeURLElicitation(elicitationId);
- // Send a success response
- res.send(`
- <!DOCTYPE html>
- <html>
- <head>
- <title>Success</title>
- <style>
- body { font-family: sans-serif; max-width: 400px; margin: 50px auto; padding: 20px; text-align: center; }
- .success { background: #d4edda; color: #155724; padding: 20px; margin: 20px 0; }
- </style>
- </head>
- <body>
- <div class="success">
- <h1>Success ✓</h1>
- <p>API key received.</p>
- </div>
- <p>You can close this window and return to your MCP client.</p>
- </body>
- </html>
- `);
- });
- // Helper to get the user session from the demo_session cookie
- function getUserSessionCookie(cookieHeader) {
- if (!cookieHeader)
- return null;
- const cookies = cookieHeader.split(';');
- for (const cookie of cookies) {
- const [name, value] = cookie.trim().split('=');
- if (name === 'demo_session' && value) {
- try {
- return JSON.parse(decodeURIComponent(value));
- }
- catch (error) {
- console.error('Failed to parse demo_session cookie:', error);
- return null;
- }
- }
- }
- return null;
- }
- /**
- * Payment Confirmation Form Handling
- *
- * This demonstrates how a server can use URL-mode elicitation to get user confirmation
- * for sensitive operations like payment processing.
- **/
- // Payment Confirmation Form endpoint - serves a simple HTML form
- app.get('/confirm-payment', (req, res) => {
- const mcpSessionId = req.query.session;
- const elicitationId = req.query.elicitation;
- const cartId = req.query.cartId;
- if (!mcpSessionId || !elicitationId) {
- res.status(400).send('<h1>Error</h1><p>Missing required parameters</p>');
- return;
- }
- // Check for user session cookie
- // In production, this is often handled by some user auth middleware to ensure the user has a valid session
- // This session is different from the MCP session.
- // This userSession is the cookie that the MCP Server's Authorization Server sets for the user when they log in.
- const userSession = getUserSessionCookie(req.headers.cookie);
- if (!userSession) {
- res.status(401).send('<h1>Error</h1><p>Unauthorized - please reconnect to login again</p>');
- return;
- }
- // Serve a simple HTML form
- res.send(`
- <!DOCTYPE html>
- <html>
- <head>
- <title>Confirm Payment</title>
- <style>
- body { font-family: sans-serif; max-width: 400px; margin: 50px auto; padding: 20px; }
- button { background: #28a745; color: white; padding: 12px 24px; border: none; cursor: pointer; font-size: 16px; width: 100%; margin: 10px 0; }
- button:hover { background: #218838; }
- button.cancel { background: #6c757d; }
- button.cancel:hover { background: #5a6268; }
- .user { background: #d1ecf1; padding: 8px; margin-bottom: 10px; }
- .cart-info { background: #f8f9fa; padding: 12px; margin: 15px 0; border-left: 4px solid #007bff; }
- .info { color: #666; font-size: 0.9em; margin-top: 20px; }
- .warning { background: #fff3cd; color: #856404; padding: 12px; margin: 15px 0; border-left: 4px solid #ffc107; }
- </style>
- </head>
- <body>
- <h1>Confirm Payment</h1>
- <div class="user">✓ Logged in as: <strong>${userSession.name}</strong></div>
- ${cartId ? `<div class="cart-info"><strong>Cart ID:</strong> ${cartId}</div>` : ''}
- <div class="warning">
- <strong>⚠️ Please review your order before confirming.</strong>
- </div>
- <form method="POST" action="/confirm-payment">
- <input type="hidden" name="session" value="${mcpSessionId}" />
- <input type="hidden" name="elicitation" value="${elicitationId}" />
- ${cartId ? `<input type="hidden" name="cartId" value="${cartId}" />` : ''}
- <button type="submit" name="action" value="confirm">Confirm Payment</button>
- <button type="submit" name="action" value="cancel" class="cancel">Cancel</button>
- </form>
- <div class="info">This is a demo showing how a server can securely get user confirmation for sensitive operations using URL-mode elicitation.</div>
- </body>
- </html>
- `);
- });
- // Handle Payment Confirmation form submission
- app.post('/confirm-payment', express_1.default.urlencoded(), (req, res) => {
- const { session: sessionId, elicitation: elicitationId, cartId, action } = req.body;
- if (!sessionId || !elicitationId) {
- res.status(400).send('<h1>Error</h1><p>Missing required parameters</p>');
- return;
- }
- // Check for user session cookie here too
- const userSession = getUserSessionCookie(req.headers.cookie);
- if (!userSession) {
- res.status(401).send('<h1>Error</h1><p>Unauthorized - please reconnect to login again</p>');
- return;
- }
- if (action === 'confirm') {
- // A real app would process the payment here
- console.log(`💳 Payment confirmed for cart ${cartId || 'unknown'} by user ${userSession.name} (session ${sessionId})`);
- // Complete the elicitation
- completeURLElicitation(elicitationId);
- // Send a success response
- res.send(`
- <!DOCTYPE html>
- <html>
- <head>
- <title>Payment Confirmed</title>
- <style>
- body { font-family: sans-serif; max-width: 400px; margin: 50px auto; padding: 20px; text-align: center; }
- .success { background: #d4edda; color: #155724; padding: 20px; margin: 20px 0; }
- </style>
- </head>
- <body>
- <div class="success">
- <h1>Payment Confirmed ✓</h1>
- <p>Your payment has been successfully processed.</p>
- ${cartId ? `<p><strong>Cart ID:</strong> ${cartId}</p>` : ''}
- </div>
- <p>You can close this window and return to your MCP client.</p>
- </body>
- </html>
- `);
- }
- else if (action === 'cancel') {
- console.log(`💳 Payment cancelled for cart ${cartId || 'unknown'} by user ${userSession.name} (session ${sessionId})`);
- // The client will still receive a notifications/elicitation/complete notification,
- // which indicates that the out-of-band interaction is complete (but not necessarily successful)
- completeURLElicitation(elicitationId);
- res.send(`
- <!DOCTYPE html>
- <html>
- <head>
- <title>Payment Cancelled</title>
- <style>
- body { font-family: sans-serif; max-width: 400px; margin: 50px auto; padding: 20px; text-align: center; }
- .info { background: #d1ecf1; color: #0c5460; padding: 20px; margin: 20px 0; }
- </style>
- </head>
- <body>
- <div class="info">
- <h1>Payment Cancelled</h1>
- <p>Your payment has been cancelled.</p>
- </div>
- <p>You can close this window and return to your MCP client.</p>
- </body>
- </html>
- `);
- }
- else {
- res.status(400).send('<h1>Error</h1><p>Invalid action</p>');
- }
- });
- // Map to store transports by session ID
- const transports = {};
- const sessionsNeedingElicitation = {};
- // MCP POST endpoint
- const mcpPostHandler = async (req, res) => {
- const sessionId = req.headers['mcp-session-id'];
- console.debug(`Received MCP POST for session: ${sessionId || 'unknown'}`);
- try {
- let transport;
- if (sessionId && transports[sessionId]) {
- // Reuse existing transport
- transport = transports[sessionId];
- }
- else if (!sessionId && (0, types_js_1.isInitializeRequest)(req.body)) {
- const server = getServer();
- // New initialization request
- const eventStore = new inMemoryEventStore_js_1.InMemoryEventStore();
- transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
- sessionIdGenerator: () => (0, node_crypto_1.randomUUID)(),
- eventStore, // Enable resumability
- onsessioninitialized: sessionId => {
- // Store the transport by session ID when session is initialized
- // This avoids race conditions where requests might come in before the session is stored
- console.log(`Session initialized with ID: ${sessionId}`);
- transports[sessionId] = transport;
- sessionsNeedingElicitation[sessionId] = {
- elicitationSender: params => server.server.elicitInput(params),
- createCompletionNotifier: elicitationId => server.server.createElicitationCompletionNotifier(elicitationId)
- };
- }
- });
- // Set up onclose handler to clean up transport when closed
- transport.onclose = () => {
- const sid = transport.sessionId;
- if (sid && transports[sid]) {
- console.log(`Transport closed for session ${sid}, removing from transports map`);
- delete transports[sid];
- delete sessionsNeedingElicitation[sid];
- }
- };
- // Connect the transport to the MCP server BEFORE handling the request
- // so responses can flow back through the same transport
- await server.connect(transport);
- await transport.handleRequest(req, res, req.body);
- return; // Already handled
- }
- else {
- // Invalid request - no session ID or not initialization request
- res.status(400).json({
- jsonrpc: '2.0',
- error: {
- code: -32000,
- message: 'Bad Request: No valid session ID provided'
- },
- id: null
- });
- return;
- }
- // Handle the request with existing transport - no need to reconnect
- // The existing transport is already connected to the server
- await transport.handleRequest(req, res, req.body);
- }
- catch (error) {
- console.error('Error handling MCP request:', error);
- if (!res.headersSent) {
- res.status(500).json({
- jsonrpc: '2.0',
- error: {
- code: -32603,
- message: 'Internal server error'
- },
- id: null
- });
- }
- }
- };
- // Set up routes with auth middleware
- app.post('/mcp', authMiddleware, mcpPostHandler);
- // Handle GET requests for SSE streams (using built-in support from StreamableHTTP)
- const mcpGetHandler = async (req, res) => {
- const sessionId = req.headers['mcp-session-id'];
- if (!sessionId || !transports[sessionId]) {
- res.status(400).send('Invalid or missing session ID');
- return;
- }
- // Check for Last-Event-ID header for resumability
- const lastEventId = req.headers['last-event-id'];
- if (lastEventId) {
- console.log(`Client reconnecting with Last-Event-ID: ${lastEventId}`);
- }
- else {
- console.log(`Establishing new SSE stream for session ${sessionId}`);
- }
- const transport = transports[sessionId];
- await transport.handleRequest(req, res);
- if (sessionsNeedingElicitation[sessionId]) {
- const { elicitationSender, createCompletionNotifier } = sessionsNeedingElicitation[sessionId];
- // Send an elicitation request to the client in the background
- sendApiKeyElicitation(sessionId, elicitationSender, createCompletionNotifier)
- .then(() => {
- // Only delete on successful send for this demo
- delete sessionsNeedingElicitation[sessionId];
- console.log(`🔑 URL elicitation demo: Finished sending API key elicitation request for session ${sessionId}`);
- })
- .catch(error => {
- console.error('Error sending API key elicitation:', error);
- // Keep in map to potentially retry on next reconnect
- });
- }
- };
- // Set up GET route with conditional auth middleware
- app.get('/mcp', authMiddleware, mcpGetHandler);
- // Handle DELETE requests for session termination (according to MCP spec)
- const mcpDeleteHandler = async (req, res) => {
- const sessionId = req.headers['mcp-session-id'];
- if (!sessionId || !transports[sessionId]) {
- res.status(400).send('Invalid or missing session ID');
- return;
- }
- console.log(`Received session termination request for session ${sessionId}`);
- try {
- const transport = transports[sessionId];
- await transport.handleRequest(req, res);
- }
- catch (error) {
- console.error('Error handling session termination:', error);
- if (!res.headersSent) {
- res.status(500).send('Error processing session termination');
- }
- }
- };
- // 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);
- process.exit(1);
- }
- console.log(`MCP Streamable HTTP Server listening on port ${MCP_PORT}`);
- });
- // Handle server shutdown
- process.on('SIGINT', async () => {
- console.log('Shutting down server...');
- // Close all active transports to properly clean up resources
- for (const sessionId in transports) {
- try {
- console.log(`Closing transport for session ${sessionId}`);
- await transports[sessionId].close();
- delete transports[sessionId];
- delete sessionsNeedingElicitation[sessionId];
- }
- catch (error) {
- console.error(`Error closing transport for session ${sessionId}:`, error);
- }
- }
- console.log('Server shutdown complete');
- process.exit(0);
- });
- //# sourceMappingURL=elicitationUrlExample.js.map
|