simpleStreamableHttp.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725
  1. import { randomUUID } from 'node:crypto';
  2. import * as z from 'zod/v4';
  3. import { McpServer } from '../../server/mcp.js';
  4. import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js';
  5. import { getOAuthProtectedResourceMetadataUrl, mcpAuthMetadataRouter } from '../../server/auth/router.js';
  6. import { requireBearerAuth } from '../../server/auth/middleware/bearerAuth.js';
  7. import { createMcpExpressApp } from '../../server/express.js';
  8. import { ElicitResultSchema, isInitializeRequest } from '../../types.js';
  9. import { InMemoryEventStore } from '../shared/inMemoryEventStore.js';
  10. import { InMemoryTaskStore, InMemoryTaskMessageQueue } from '../../experimental/tasks/stores/in-memory.js';
  11. import { setupAuthServer } from './demoInMemoryOAuthProvider.js';
  12. import { checkResourceAllowed } from '../../shared/auth-utils.js';
  13. // Check for OAuth flag
  14. const useOAuth = process.argv.includes('--oauth');
  15. const strictOAuth = process.argv.includes('--oauth-strict');
  16. // Create shared task store for demonstration
  17. const taskStore = new InMemoryTaskStore();
  18. // Create an MCP server with implementation details
  19. const getServer = () => {
  20. const server = new McpServer({
  21. name: 'simple-streamable-http-server',
  22. version: '1.0.0',
  23. icons: [{ src: './mcp.svg', sizes: ['512x512'], mimeType: 'image/svg+xml' }],
  24. websiteUrl: 'https://github.com/modelcontextprotocol/typescript-sdk'
  25. }, {
  26. capabilities: { logging: {}, tasks: { requests: { tools: { call: {} } } } },
  27. taskStore, // Enable task support
  28. taskMessageQueue: new InMemoryTaskMessageQueue()
  29. });
  30. // Register a simple tool that returns a greeting
  31. server.registerTool('greet', {
  32. title: 'Greeting Tool', // Display name for UI
  33. description: 'A simple greeting tool',
  34. inputSchema: {
  35. name: z.string().describe('Name to greet')
  36. }
  37. }, async ({ name }) => {
  38. return {
  39. content: [
  40. {
  41. type: 'text',
  42. text: `Hello, ${name}!`
  43. }
  44. ]
  45. };
  46. });
  47. // Register a tool that sends multiple greetings with notifications (with annotations)
  48. server.registerTool('multi-greet', {
  49. description: 'A tool that sends different greetings with delays between them',
  50. inputSchema: {
  51. name: z.string().describe('Name to greet')
  52. },
  53. annotations: {
  54. title: 'Multiple Greeting Tool',
  55. readOnlyHint: true,
  56. openWorldHint: false
  57. }
  58. }, async ({ name }, extra) => {
  59. const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
  60. await server.sendLoggingMessage({
  61. level: 'debug',
  62. data: `Starting multi-greet for ${name}`
  63. }, extra.sessionId);
  64. await sleep(1000); // Wait 1 second before first greeting
  65. await server.sendLoggingMessage({
  66. level: 'info',
  67. data: `Sending first greeting to ${name}`
  68. }, extra.sessionId);
  69. await sleep(1000); // Wait another second before second greeting
  70. await server.sendLoggingMessage({
  71. level: 'info',
  72. data: `Sending second greeting to ${name}`
  73. }, extra.sessionId);
  74. return {
  75. content: [
  76. {
  77. type: 'text',
  78. text: `Good morning, ${name}!`
  79. }
  80. ]
  81. };
  82. });
  83. // Register a tool that demonstrates form elicitation (user input collection with a schema)
  84. // This creates a closure that captures the server instance
  85. server.registerTool('collect-user-info', {
  86. description: 'A tool that collects user information through form elicitation',
  87. inputSchema: {
  88. infoType: z.enum(['contact', 'preferences', 'feedback']).describe('Type of information to collect')
  89. }
  90. }, async ({ infoType }, extra) => {
  91. let message;
  92. let requestedSchema;
  93. switch (infoType) {
  94. case 'contact':
  95. message = 'Please provide your contact information';
  96. requestedSchema = {
  97. type: 'object',
  98. properties: {
  99. name: {
  100. type: 'string',
  101. title: 'Full Name',
  102. description: 'Your full name'
  103. },
  104. email: {
  105. type: 'string',
  106. title: 'Email Address',
  107. description: 'Your email address',
  108. format: 'email'
  109. },
  110. phone: {
  111. type: 'string',
  112. title: 'Phone Number',
  113. description: 'Your phone number (optional)'
  114. }
  115. },
  116. required: ['name', 'email']
  117. };
  118. break;
  119. case 'preferences':
  120. message = 'Please set your preferences';
  121. requestedSchema = {
  122. type: 'object',
  123. properties: {
  124. theme: {
  125. type: 'string',
  126. title: 'Theme',
  127. description: 'Choose your preferred theme',
  128. enum: ['light', 'dark', 'auto'],
  129. enumNames: ['Light', 'Dark', 'Auto']
  130. },
  131. notifications: {
  132. type: 'boolean',
  133. title: 'Enable Notifications',
  134. description: 'Would you like to receive notifications?',
  135. default: true
  136. },
  137. frequency: {
  138. type: 'string',
  139. title: 'Notification Frequency',
  140. description: 'How often would you like notifications?',
  141. enum: ['daily', 'weekly', 'monthly'],
  142. enumNames: ['Daily', 'Weekly', 'Monthly']
  143. }
  144. },
  145. required: ['theme']
  146. };
  147. break;
  148. case 'feedback':
  149. message = 'Please provide your feedback';
  150. requestedSchema = {
  151. type: 'object',
  152. properties: {
  153. rating: {
  154. type: 'integer',
  155. title: 'Rating',
  156. description: 'Rate your experience (1-5)',
  157. minimum: 1,
  158. maximum: 5
  159. },
  160. comments: {
  161. type: 'string',
  162. title: 'Comments',
  163. description: 'Additional comments (optional)',
  164. maxLength: 500
  165. },
  166. recommend: {
  167. type: 'boolean',
  168. title: 'Would you recommend this?',
  169. description: 'Would you recommend this to others?'
  170. }
  171. },
  172. required: ['rating', 'recommend']
  173. };
  174. break;
  175. default:
  176. throw new Error(`Unknown info type: ${infoType}`);
  177. }
  178. try {
  179. // Use sendRequest through the extra parameter to elicit input
  180. const result = await extra.sendRequest({
  181. method: 'elicitation/create',
  182. params: {
  183. mode: 'form',
  184. message,
  185. requestedSchema
  186. }
  187. }, ElicitResultSchema);
  188. if (result.action === 'accept') {
  189. return {
  190. content: [
  191. {
  192. type: 'text',
  193. text: `Thank you! Collected ${infoType} information: ${JSON.stringify(result.content, null, 2)}`
  194. }
  195. ]
  196. };
  197. }
  198. else if (result.action === 'decline') {
  199. return {
  200. content: [
  201. {
  202. type: 'text',
  203. text: `No information was collected. User declined ${infoType} information request.`
  204. }
  205. ]
  206. };
  207. }
  208. else {
  209. return {
  210. content: [
  211. {
  212. type: 'text',
  213. text: `Information collection was cancelled by the user.`
  214. }
  215. ]
  216. };
  217. }
  218. }
  219. catch (error) {
  220. return {
  221. content: [
  222. {
  223. type: 'text',
  224. text: `Error collecting ${infoType} information: ${error}`
  225. }
  226. ]
  227. };
  228. }
  229. });
  230. // Register a tool that demonstrates bidirectional task support:
  231. // Server creates a task, then elicits input from client using elicitInputStream
  232. // Using the experimental tasks API - WARNING: may change without notice
  233. server.experimental.tasks.registerToolTask('collect-user-info-task', {
  234. title: 'Collect Info with Task',
  235. description: 'Collects user info via elicitation with task support using elicitInputStream',
  236. inputSchema: {
  237. infoType: z.enum(['contact', 'preferences']).describe('Type of information to collect').default('contact')
  238. }
  239. }, {
  240. async createTask({ infoType }, { taskStore: createTaskStore, taskRequestedTtl }) {
  241. // Create the server-side task
  242. const task = await createTaskStore.createTask({
  243. ttl: taskRequestedTtl
  244. });
  245. // Perform async work that makes a nested elicitation request using elicitInputStream
  246. (async () => {
  247. try {
  248. const message = infoType === 'contact' ? 'Please provide your contact information' : 'Please set your preferences';
  249. // Define schemas with proper typing for PrimitiveSchemaDefinition
  250. const contactSchema = {
  251. type: 'object',
  252. properties: {
  253. name: { type: 'string', title: 'Full Name', description: 'Your full name' },
  254. email: { type: 'string', title: 'Email', description: 'Your email address' }
  255. },
  256. required: ['name', 'email']
  257. };
  258. const preferencesSchema = {
  259. type: 'object',
  260. properties: {
  261. theme: { type: 'string', title: 'Theme', enum: ['light', 'dark', 'auto'] },
  262. notifications: { type: 'boolean', title: 'Enable Notifications', default: true }
  263. },
  264. required: ['theme']
  265. };
  266. const requestedSchema = infoType === 'contact' ? contactSchema : preferencesSchema;
  267. // Use elicitInputStream to elicit input from client
  268. // This demonstrates the streaming elicitation API
  269. // Access via server.server to get the underlying Server instance
  270. const stream = server.server.experimental.tasks.elicitInputStream({
  271. mode: 'form',
  272. message,
  273. requestedSchema
  274. });
  275. let elicitResult;
  276. for await (const msg of stream) {
  277. if (msg.type === 'result') {
  278. elicitResult = msg.result;
  279. }
  280. else if (msg.type === 'error') {
  281. throw msg.error;
  282. }
  283. }
  284. if (!elicitResult) {
  285. throw new Error('No result received from elicitation');
  286. }
  287. let resultText;
  288. if (elicitResult.action === 'accept') {
  289. resultText = `Collected ${infoType} info: ${JSON.stringify(elicitResult.content, null, 2)}`;
  290. }
  291. else if (elicitResult.action === 'decline') {
  292. resultText = `User declined to provide ${infoType} information`;
  293. }
  294. else {
  295. resultText = 'User cancelled the request';
  296. }
  297. await taskStore.storeTaskResult(task.taskId, 'completed', {
  298. content: [{ type: 'text', text: resultText }]
  299. });
  300. }
  301. catch (error) {
  302. console.error('Error in collect-user-info-task:', error);
  303. await taskStore.storeTaskResult(task.taskId, 'failed', {
  304. content: [{ type: 'text', text: `Error: ${error}` }],
  305. isError: true
  306. });
  307. }
  308. })();
  309. return { task };
  310. },
  311. async getTask(_args, { taskId, taskStore: getTaskStore }) {
  312. return await getTaskStore.getTask(taskId);
  313. },
  314. async getTaskResult(_args, { taskId, taskStore: getResultTaskStore }) {
  315. const result = await getResultTaskStore.getTaskResult(taskId);
  316. return result;
  317. }
  318. });
  319. // Register a simple prompt with title
  320. server.registerPrompt('greeting-template', {
  321. title: 'Greeting Template', // Display name for UI
  322. description: 'A simple greeting prompt template',
  323. argsSchema: {
  324. name: z.string().describe('Name to include in greeting')
  325. }
  326. }, async ({ name }) => {
  327. return {
  328. messages: [
  329. {
  330. role: 'user',
  331. content: {
  332. type: 'text',
  333. text: `Please greet ${name} in a friendly manner.`
  334. }
  335. }
  336. ]
  337. };
  338. });
  339. // Register a tool specifically for testing resumability
  340. server.registerTool('start-notification-stream', {
  341. description: 'Starts sending periodic notifications for testing resumability',
  342. inputSchema: {
  343. interval: z.number().describe('Interval in milliseconds between notifications').default(100),
  344. count: z.number().describe('Number of notifications to send (0 for 100)').default(50)
  345. }
  346. }, async ({ interval, count }, extra) => {
  347. const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
  348. let counter = 0;
  349. while (count === 0 || counter < count) {
  350. counter++;
  351. try {
  352. await server.sendLoggingMessage({
  353. level: 'info',
  354. data: `Periodic notification #${counter} at ${new Date().toISOString()}`
  355. }, extra.sessionId);
  356. }
  357. catch (error) {
  358. console.error('Error sending notification:', error);
  359. }
  360. // Wait for the specified interval
  361. await sleep(interval);
  362. }
  363. return {
  364. content: [
  365. {
  366. type: 'text',
  367. text: `Started sending periodic notifications every ${interval}ms`
  368. }
  369. ]
  370. };
  371. });
  372. // Create a simple resource at a fixed URI
  373. server.registerResource('greeting-resource', 'https://example.com/greetings/default', {
  374. title: 'Default Greeting', // Display name for UI
  375. description: 'A simple greeting resource',
  376. mimeType: 'text/plain'
  377. }, async () => {
  378. return {
  379. contents: [
  380. {
  381. uri: 'https://example.com/greetings/default',
  382. text: 'Hello, world!'
  383. }
  384. ]
  385. };
  386. });
  387. // Create additional resources for ResourceLink demonstration
  388. server.registerResource('example-file-1', 'file:///example/file1.txt', {
  389. title: 'Example File 1',
  390. description: 'First example file for ResourceLink demonstration',
  391. mimeType: 'text/plain'
  392. }, async () => {
  393. return {
  394. contents: [
  395. {
  396. uri: 'file:///example/file1.txt',
  397. text: 'This is the content of file 1'
  398. }
  399. ]
  400. };
  401. });
  402. server.registerResource('example-file-2', 'file:///example/file2.txt', {
  403. title: 'Example File 2',
  404. description: 'Second example file for ResourceLink demonstration',
  405. mimeType: 'text/plain'
  406. }, async () => {
  407. return {
  408. contents: [
  409. {
  410. uri: 'file:///example/file2.txt',
  411. text: 'This is the content of file 2'
  412. }
  413. ]
  414. };
  415. });
  416. // Register a tool that returns ResourceLinks
  417. server.registerTool('list-files', {
  418. title: 'List Files with ResourceLinks',
  419. description: 'Returns a list of files as ResourceLinks without embedding their content',
  420. inputSchema: {
  421. includeDescriptions: z.boolean().optional().describe('Whether to include descriptions in the resource links')
  422. }
  423. }, async ({ includeDescriptions = true }) => {
  424. const resourceLinks = [
  425. {
  426. type: 'resource_link',
  427. uri: 'https://example.com/greetings/default',
  428. name: 'Default Greeting',
  429. mimeType: 'text/plain',
  430. ...(includeDescriptions && { description: 'A simple greeting resource' })
  431. },
  432. {
  433. type: 'resource_link',
  434. uri: 'file:///example/file1.txt',
  435. name: 'Example File 1',
  436. mimeType: 'text/plain',
  437. ...(includeDescriptions && { description: 'First example file for ResourceLink demonstration' })
  438. },
  439. {
  440. type: 'resource_link',
  441. uri: 'file:///example/file2.txt',
  442. name: 'Example File 2',
  443. mimeType: 'text/plain',
  444. ...(includeDescriptions && { description: 'Second example file for ResourceLink demonstration' })
  445. }
  446. ];
  447. return {
  448. content: [
  449. {
  450. type: 'text',
  451. text: 'Here are the available files as resource links:'
  452. },
  453. ...resourceLinks,
  454. {
  455. type: 'text',
  456. text: '\nYou can read any of these resources using their URI.'
  457. }
  458. ]
  459. };
  460. });
  461. // Register a long-running tool that demonstrates task execution
  462. // Using the experimental tasks API - WARNING: may change without notice
  463. server.experimental.tasks.registerToolTask('delay', {
  464. title: 'Delay',
  465. description: 'A simple tool that delays for a specified duration, useful for testing task execution',
  466. inputSchema: {
  467. duration: z.number().describe('Duration in milliseconds').default(5000)
  468. }
  469. }, {
  470. async createTask({ duration }, { taskStore, taskRequestedTtl }) {
  471. // Create the task
  472. const task = await taskStore.createTask({
  473. ttl: taskRequestedTtl
  474. });
  475. // Simulate out-of-band work
  476. (async () => {
  477. await new Promise(resolve => setTimeout(resolve, duration));
  478. await taskStore.storeTaskResult(task.taskId, 'completed', {
  479. content: [
  480. {
  481. type: 'text',
  482. text: `Completed ${duration}ms delay`
  483. }
  484. ]
  485. });
  486. })();
  487. // Return CreateTaskResult with the created task
  488. return {
  489. task
  490. };
  491. },
  492. async getTask(_args, { taskId, taskStore }) {
  493. return await taskStore.getTask(taskId);
  494. },
  495. async getTaskResult(_args, { taskId, taskStore }) {
  496. const result = await taskStore.getTaskResult(taskId);
  497. return result;
  498. }
  499. });
  500. return server;
  501. };
  502. const MCP_PORT = process.env.MCP_PORT ? parseInt(process.env.MCP_PORT, 10) : 3000;
  503. const AUTH_PORT = process.env.MCP_AUTH_PORT ? parseInt(process.env.MCP_AUTH_PORT, 10) : 3001;
  504. const app = createMcpExpressApp();
  505. // Set up OAuth if enabled
  506. let authMiddleware = null;
  507. if (useOAuth) {
  508. // Create auth middleware for MCP endpoints
  509. const mcpServerUrl = new URL(`http://localhost:${MCP_PORT}/mcp`);
  510. const authServerUrl = new URL(`http://localhost:${AUTH_PORT}`);
  511. const oauthMetadata = setupAuthServer({ authServerUrl, mcpServerUrl, strictResource: strictOAuth });
  512. const tokenVerifier = {
  513. verifyAccessToken: async (token) => {
  514. const endpoint = oauthMetadata.introspection_endpoint;
  515. if (!endpoint) {
  516. throw new Error('No token verification endpoint available in metadata');
  517. }
  518. const response = await fetch(endpoint, {
  519. method: 'POST',
  520. headers: {
  521. 'Content-Type': 'application/x-www-form-urlencoded'
  522. },
  523. body: new URLSearchParams({
  524. token: token
  525. }).toString()
  526. });
  527. if (!response.ok) {
  528. const text = await response.text().catch(() => null);
  529. throw new Error(`Invalid or expired token: ${text}`);
  530. }
  531. const data = await response.json();
  532. if (strictOAuth) {
  533. if (!data.aud) {
  534. throw new Error(`Resource Indicator (RFC8707) missing`);
  535. }
  536. if (!checkResourceAllowed({ requestedResource: data.aud, configuredResource: mcpServerUrl })) {
  537. throw new Error(`Expected resource indicator ${mcpServerUrl}, got: ${data.aud}`);
  538. }
  539. }
  540. // Convert the response to AuthInfo format
  541. return {
  542. token,
  543. clientId: data.client_id,
  544. scopes: data.scope ? data.scope.split(' ') : [],
  545. expiresAt: data.exp
  546. };
  547. }
  548. };
  549. // Add metadata routes to the main MCP server
  550. app.use(mcpAuthMetadataRouter({
  551. oauthMetadata,
  552. resourceServerUrl: mcpServerUrl,
  553. scopesSupported: ['mcp:tools'],
  554. resourceName: 'MCP Demo Server'
  555. }));
  556. authMiddleware = requireBearerAuth({
  557. verifier: tokenVerifier,
  558. requiredScopes: [],
  559. resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl)
  560. });
  561. }
  562. // Map to store transports by session ID
  563. const transports = {};
  564. // MCP POST endpoint with optional auth
  565. const mcpPostHandler = async (req, res) => {
  566. const sessionId = req.headers['mcp-session-id'];
  567. if (sessionId) {
  568. console.log(`Received MCP request for session: ${sessionId}`);
  569. }
  570. else {
  571. console.log('Request body:', req.body);
  572. }
  573. if (useOAuth && req.auth) {
  574. console.log('Authenticated user:', req.auth);
  575. }
  576. try {
  577. let transport;
  578. if (sessionId && transports[sessionId]) {
  579. // Reuse existing transport
  580. transport = transports[sessionId];
  581. }
  582. else if (!sessionId && isInitializeRequest(req.body)) {
  583. // New initialization request
  584. const eventStore = new InMemoryEventStore();
  585. transport = new StreamableHTTPServerTransport({
  586. sessionIdGenerator: () => randomUUID(),
  587. eventStore, // Enable resumability
  588. onsessioninitialized: sessionId => {
  589. // Store the transport by session ID when session is initialized
  590. // This avoids race conditions where requests might come in before the session is stored
  591. console.log(`Session initialized with ID: ${sessionId}`);
  592. transports[sessionId] = transport;
  593. }
  594. });
  595. // Set up onclose handler to clean up transport when closed
  596. transport.onclose = () => {
  597. const sid = transport.sessionId;
  598. if (sid && transports[sid]) {
  599. console.log(`Transport closed for session ${sid}, removing from transports map`);
  600. delete transports[sid];
  601. }
  602. };
  603. // Connect the transport to the MCP server BEFORE handling the request
  604. // so responses can flow back through the same transport
  605. const server = getServer();
  606. await server.connect(transport);
  607. await transport.handleRequest(req, res, req.body);
  608. return; // Already handled
  609. }
  610. else {
  611. // Invalid request - no session ID or not initialization request
  612. res.status(400).json({
  613. jsonrpc: '2.0',
  614. error: {
  615. code: -32000,
  616. message: 'Bad Request: No valid session ID provided'
  617. },
  618. id: null
  619. });
  620. return;
  621. }
  622. // Handle the request with existing transport - no need to reconnect
  623. // The existing transport is already connected to the server
  624. await transport.handleRequest(req, res, req.body);
  625. }
  626. catch (error) {
  627. console.error('Error handling MCP request:', error);
  628. if (!res.headersSent) {
  629. res.status(500).json({
  630. jsonrpc: '2.0',
  631. error: {
  632. code: -32603,
  633. message: 'Internal server error'
  634. },
  635. id: null
  636. });
  637. }
  638. }
  639. };
  640. // Set up routes with conditional auth middleware
  641. if (useOAuth && authMiddleware) {
  642. app.post('/mcp', authMiddleware, mcpPostHandler);
  643. }
  644. else {
  645. app.post('/mcp', mcpPostHandler);
  646. }
  647. // Handle GET requests for SSE streams (using built-in support from StreamableHTTP)
  648. const mcpGetHandler = async (req, res) => {
  649. const sessionId = req.headers['mcp-session-id'];
  650. if (!sessionId || !transports[sessionId]) {
  651. res.status(400).send('Invalid or missing session ID');
  652. return;
  653. }
  654. if (useOAuth && req.auth) {
  655. console.log('Authenticated SSE connection from user:', req.auth);
  656. }
  657. // Check for Last-Event-ID header for resumability
  658. const lastEventId = req.headers['last-event-id'];
  659. if (lastEventId) {
  660. console.log(`Client reconnecting with Last-Event-ID: ${lastEventId}`);
  661. }
  662. else {
  663. console.log(`Establishing new SSE stream for session ${sessionId}`);
  664. }
  665. const transport = transports[sessionId];
  666. await transport.handleRequest(req, res);
  667. };
  668. // Set up GET route with conditional auth middleware
  669. if (useOAuth && authMiddleware) {
  670. app.get('/mcp', authMiddleware, mcpGetHandler);
  671. }
  672. else {
  673. app.get('/mcp', mcpGetHandler);
  674. }
  675. // Handle DELETE requests for session termination (according to MCP spec)
  676. const mcpDeleteHandler = async (req, res) => {
  677. const sessionId = req.headers['mcp-session-id'];
  678. if (!sessionId || !transports[sessionId]) {
  679. res.status(400).send('Invalid or missing session ID');
  680. return;
  681. }
  682. console.log(`Received session termination request for session ${sessionId}`);
  683. try {
  684. const transport = transports[sessionId];
  685. await transport.handleRequest(req, res);
  686. }
  687. catch (error) {
  688. console.error('Error handling session termination:', error);
  689. if (!res.headersSent) {
  690. res.status(500).send('Error processing session termination');
  691. }
  692. }
  693. };
  694. // Set up DELETE route with conditional auth middleware
  695. if (useOAuth && authMiddleware) {
  696. app.delete('/mcp', authMiddleware, mcpDeleteHandler);
  697. }
  698. else {
  699. app.delete('/mcp', mcpDeleteHandler);
  700. }
  701. app.listen(MCP_PORT, error => {
  702. if (error) {
  703. console.error('Failed to start server:', error);
  704. process.exit(1);
  705. }
  706. console.log(`MCP Streamable HTTP Server listening on port ${MCP_PORT}`);
  707. });
  708. // Handle server shutdown
  709. process.on('SIGINT', async () => {
  710. console.log('Shutting down server...');
  711. // Close all active transports to properly clean up resources
  712. for (const sessionId in transports) {
  713. try {
  714. console.log(`Closing transport for session ${sessionId}`);
  715. await transports[sessionId].close();
  716. delete transports[sessionId];
  717. }
  718. catch (error) {
  719. console.error(`Error closing transport for session ${sessionId}:`, error);
  720. }
  721. }
  722. console.log('Server shutdown complete');
  723. process.exit(0);
  724. });
  725. //# sourceMappingURL=simpleStreamableHttp.js.map