simpleStreamableHttp.js 30 KB

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