simpleTaskInteractive.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600
  1. "use strict";
  2. /**
  3. * Simple interactive task server demonstrating elicitation and sampling.
  4. *
  5. * This server demonstrates the task message queue pattern from the MCP Tasks spec:
  6. * - confirm_delete: Uses elicitation to ask the user for confirmation
  7. * - write_haiku: Uses sampling to request an LLM to generate content
  8. *
  9. * Both tools use the "call-now, fetch-later" pattern where the initial call
  10. * creates a task, and the result is fetched via tasks/result endpoint.
  11. */
  12. Object.defineProperty(exports, "__esModule", { value: true });
  13. const node_crypto_1 = require("node:crypto");
  14. const index_js_1 = require("../../server/index.js");
  15. const express_js_1 = require("../../server/express.js");
  16. const streamableHttp_js_1 = require("../../server/streamableHttp.js");
  17. const types_js_1 = require("../../types.js");
  18. const interfaces_js_1 = require("../../experimental/tasks/interfaces.js");
  19. const in_memory_js_1 = require("../../experimental/tasks/stores/in-memory.js");
  20. // ============================================================================
  21. // Resolver - Promise-like for passing results between async operations
  22. // ============================================================================
  23. class Resolver {
  24. constructor() {
  25. this._done = false;
  26. this._promise = new Promise((resolve, reject) => {
  27. this._resolve = resolve;
  28. this._reject = reject;
  29. });
  30. }
  31. setResult(value) {
  32. if (this._done)
  33. return;
  34. this._done = true;
  35. this._resolve(value);
  36. }
  37. setException(error) {
  38. if (this._done)
  39. return;
  40. this._done = true;
  41. this._reject(error);
  42. }
  43. wait() {
  44. return this._promise;
  45. }
  46. done() {
  47. return this._done;
  48. }
  49. }
  50. class TaskMessageQueueWithResolvers {
  51. constructor() {
  52. this.queues = new Map();
  53. this.waitResolvers = new Map();
  54. }
  55. getQueue(taskId) {
  56. let queue = this.queues.get(taskId);
  57. if (!queue) {
  58. queue = [];
  59. this.queues.set(taskId, queue);
  60. }
  61. return queue;
  62. }
  63. async enqueue(taskId, message, _sessionId, maxSize) {
  64. const queue = this.getQueue(taskId);
  65. if (maxSize !== undefined && queue.length >= maxSize) {
  66. throw new Error(`Task message queue overflow: queue size (${queue.length}) exceeds maximum (${maxSize})`);
  67. }
  68. queue.push(message);
  69. // Notify any waiters
  70. this.notifyWaiters(taskId);
  71. }
  72. async enqueueWithResolver(taskId, message, resolver, originalRequestId) {
  73. const queue = this.getQueue(taskId);
  74. const queuedMessage = {
  75. type: 'request',
  76. message,
  77. timestamp: Date.now(),
  78. resolver,
  79. originalRequestId
  80. };
  81. queue.push(queuedMessage);
  82. this.notifyWaiters(taskId);
  83. }
  84. async dequeue(taskId, _sessionId) {
  85. const queue = this.getQueue(taskId);
  86. return queue.shift();
  87. }
  88. async dequeueAll(taskId, _sessionId) {
  89. const queue = this.queues.get(taskId) ?? [];
  90. this.queues.delete(taskId);
  91. return queue;
  92. }
  93. async waitForMessage(taskId) {
  94. // Check if there are already messages
  95. const queue = this.getQueue(taskId);
  96. if (queue.length > 0)
  97. return;
  98. // Wait for a message to be added
  99. return new Promise(resolve => {
  100. let waiters = this.waitResolvers.get(taskId);
  101. if (!waiters) {
  102. waiters = [];
  103. this.waitResolvers.set(taskId, waiters);
  104. }
  105. waiters.push(resolve);
  106. });
  107. }
  108. notifyWaiters(taskId) {
  109. const waiters = this.waitResolvers.get(taskId);
  110. if (waiters) {
  111. this.waitResolvers.delete(taskId);
  112. for (const resolve of waiters) {
  113. resolve();
  114. }
  115. }
  116. }
  117. cleanup() {
  118. this.queues.clear();
  119. this.waitResolvers.clear();
  120. }
  121. }
  122. // ============================================================================
  123. // Extended task store with wait functionality
  124. // ============================================================================
  125. class TaskStoreWithNotifications extends in_memory_js_1.InMemoryTaskStore {
  126. constructor() {
  127. super(...arguments);
  128. this.updateResolvers = new Map();
  129. }
  130. async updateTaskStatus(taskId, status, statusMessage, sessionId) {
  131. await super.updateTaskStatus(taskId, status, statusMessage, sessionId);
  132. this.notifyUpdate(taskId);
  133. }
  134. async storeTaskResult(taskId, status, result, sessionId) {
  135. await super.storeTaskResult(taskId, status, result, sessionId);
  136. this.notifyUpdate(taskId);
  137. }
  138. async waitForUpdate(taskId) {
  139. return new Promise(resolve => {
  140. let waiters = this.updateResolvers.get(taskId);
  141. if (!waiters) {
  142. waiters = [];
  143. this.updateResolvers.set(taskId, waiters);
  144. }
  145. waiters.push(resolve);
  146. });
  147. }
  148. notifyUpdate(taskId) {
  149. const waiters = this.updateResolvers.get(taskId);
  150. if (waiters) {
  151. this.updateResolvers.delete(taskId);
  152. for (const resolve of waiters) {
  153. resolve();
  154. }
  155. }
  156. }
  157. }
  158. // ============================================================================
  159. // Task Result Handler - delivers queued messages and routes responses
  160. // ============================================================================
  161. class TaskResultHandler {
  162. constructor(store, queue) {
  163. this.store = store;
  164. this.queue = queue;
  165. this.pendingRequests = new Map();
  166. }
  167. async handle(taskId, server, _sessionId) {
  168. while (true) {
  169. // Get fresh task state
  170. const task = await this.store.getTask(taskId);
  171. if (!task) {
  172. throw new Error(`Task not found: ${taskId}`);
  173. }
  174. // Dequeue and send all pending messages
  175. await this.deliverQueuedMessages(taskId, server, _sessionId);
  176. // If task is terminal, return result
  177. if ((0, interfaces_js_1.isTerminal)(task.status)) {
  178. const result = await this.store.getTaskResult(taskId);
  179. // Add related-task metadata per spec
  180. return {
  181. ...result,
  182. _meta: {
  183. ...(result._meta || {}),
  184. [types_js_1.RELATED_TASK_META_KEY]: { taskId }
  185. }
  186. };
  187. }
  188. // Wait for task update or new message
  189. await this.waitForUpdate(taskId);
  190. }
  191. }
  192. async deliverQueuedMessages(taskId, server, _sessionId) {
  193. while (true) {
  194. const message = await this.queue.dequeue(taskId);
  195. if (!message)
  196. break;
  197. console.log(`[Server] Delivering queued ${message.type} message for task ${taskId}`);
  198. if (message.type === 'request') {
  199. const reqMessage = message;
  200. // Send the request via the server
  201. // Store the resolver so we can route the response back
  202. if (reqMessage.resolver && reqMessage.originalRequestId) {
  203. this.pendingRequests.set(reqMessage.originalRequestId, reqMessage.resolver);
  204. }
  205. // Send the message - for elicitation/sampling, we use the server's methods
  206. // But since we're in tasks/result context, we need to send via transport
  207. // This is simplified - in production you'd use proper message routing
  208. try {
  209. const request = reqMessage.message;
  210. let response;
  211. if (request.method === 'elicitation/create') {
  212. // Send elicitation request to client
  213. const params = request.params;
  214. response = await server.elicitInput(params);
  215. }
  216. else if (request.method === 'sampling/createMessage') {
  217. // Send sampling request to client
  218. const params = request.params;
  219. response = await server.createMessage(params);
  220. }
  221. else {
  222. throw new Error(`Unknown request method: ${request.method}`);
  223. }
  224. // Route response back to resolver
  225. if (reqMessage.resolver) {
  226. reqMessage.resolver.setResult(response);
  227. }
  228. }
  229. catch (error) {
  230. if (reqMessage.resolver) {
  231. reqMessage.resolver.setException(error instanceof Error ? error : new Error(String(error)));
  232. }
  233. }
  234. }
  235. // For notifications, we'd send them too but this example focuses on requests
  236. }
  237. }
  238. async waitForUpdate(taskId) {
  239. // Race between store update and queue message
  240. await Promise.race([this.store.waitForUpdate(taskId), this.queue.waitForMessage(taskId)]);
  241. }
  242. routeResponse(requestId, response) {
  243. const resolver = this.pendingRequests.get(requestId);
  244. if (resolver && !resolver.done()) {
  245. this.pendingRequests.delete(requestId);
  246. resolver.setResult(response);
  247. return true;
  248. }
  249. return false;
  250. }
  251. routeError(requestId, error) {
  252. const resolver = this.pendingRequests.get(requestId);
  253. if (resolver && !resolver.done()) {
  254. this.pendingRequests.delete(requestId);
  255. resolver.setException(error);
  256. return true;
  257. }
  258. return false;
  259. }
  260. }
  261. // ============================================================================
  262. // Task Session - wraps server to enqueue requests during task execution
  263. // ============================================================================
  264. class TaskSession {
  265. constructor(server, taskId, store, queue) {
  266. this.server = server;
  267. this.taskId = taskId;
  268. this.store = store;
  269. this.queue = queue;
  270. this.requestCounter = 0;
  271. }
  272. nextRequestId() {
  273. return `task-${this.taskId}-${++this.requestCounter}`;
  274. }
  275. async elicit(message, requestedSchema) {
  276. // Update task status to input_required
  277. await this.store.updateTaskStatus(this.taskId, 'input_required');
  278. const requestId = this.nextRequestId();
  279. // Build the elicitation request with related-task metadata
  280. const params = {
  281. message,
  282. requestedSchema,
  283. mode: 'form',
  284. _meta: {
  285. [types_js_1.RELATED_TASK_META_KEY]: { taskId: this.taskId }
  286. }
  287. };
  288. const jsonrpcRequest = {
  289. jsonrpc: '2.0',
  290. id: requestId,
  291. method: 'elicitation/create',
  292. params
  293. };
  294. // Create resolver to wait for response
  295. const resolver = new Resolver();
  296. // Enqueue the request
  297. await this.queue.enqueueWithResolver(this.taskId, jsonrpcRequest, resolver, requestId);
  298. try {
  299. // Wait for response
  300. const response = await resolver.wait();
  301. // Update status back to working
  302. await this.store.updateTaskStatus(this.taskId, 'working');
  303. return response;
  304. }
  305. catch (error) {
  306. await this.store.updateTaskStatus(this.taskId, 'working');
  307. throw error;
  308. }
  309. }
  310. async createMessage(messages, maxTokens) {
  311. // Update task status to input_required
  312. await this.store.updateTaskStatus(this.taskId, 'input_required');
  313. const requestId = this.nextRequestId();
  314. // Build the sampling request with related-task metadata
  315. const params = {
  316. messages,
  317. maxTokens,
  318. _meta: {
  319. [types_js_1.RELATED_TASK_META_KEY]: { taskId: this.taskId }
  320. }
  321. };
  322. const jsonrpcRequest = {
  323. jsonrpc: '2.0',
  324. id: requestId,
  325. method: 'sampling/createMessage',
  326. params
  327. };
  328. // Create resolver to wait for response
  329. const resolver = new Resolver();
  330. // Enqueue the request
  331. await this.queue.enqueueWithResolver(this.taskId, jsonrpcRequest, resolver, requestId);
  332. try {
  333. // Wait for response
  334. const response = await resolver.wait();
  335. // Update status back to working
  336. await this.store.updateTaskStatus(this.taskId, 'working');
  337. return response;
  338. }
  339. catch (error) {
  340. await this.store.updateTaskStatus(this.taskId, 'working');
  341. throw error;
  342. }
  343. }
  344. }
  345. // ============================================================================
  346. // Server Setup
  347. // ============================================================================
  348. const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 8000;
  349. // Create shared stores
  350. const taskStore = new TaskStoreWithNotifications();
  351. const messageQueue = new TaskMessageQueueWithResolvers();
  352. const taskResultHandler = new TaskResultHandler(taskStore, messageQueue);
  353. // Track active task executions
  354. const activeTaskExecutions = new Map();
  355. // Create the server
  356. const createServer = () => {
  357. const server = new index_js_1.Server({ name: 'simple-task-interactive', version: '1.0.0' }, {
  358. capabilities: {
  359. tools: {},
  360. tasks: {
  361. requests: {
  362. tools: { call: {} }
  363. }
  364. }
  365. }
  366. });
  367. // Register tools
  368. server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
  369. return {
  370. tools: [
  371. {
  372. name: 'confirm_delete',
  373. description: 'Asks for confirmation before deleting (demonstrates elicitation)',
  374. inputSchema: {
  375. type: 'object',
  376. properties: {
  377. filename: { type: 'string' }
  378. }
  379. },
  380. execution: { taskSupport: 'required' }
  381. },
  382. {
  383. name: 'write_haiku',
  384. description: 'Asks LLM to write a haiku (demonstrates sampling)',
  385. inputSchema: {
  386. type: 'object',
  387. properties: {
  388. topic: { type: 'string' }
  389. }
  390. },
  391. execution: { taskSupport: 'required' }
  392. }
  393. ]
  394. };
  395. });
  396. // Handle tool calls
  397. server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request, extra) => {
  398. const { name, arguments: args } = request.params;
  399. const taskParams = (request.params._meta?.task || request.params.task);
  400. // Validate task mode - these tools require tasks
  401. if (!taskParams) {
  402. throw new Error(`Tool ${name} requires task mode`);
  403. }
  404. // Create task
  405. const taskOptions = {
  406. ttl: taskParams.ttl,
  407. pollInterval: taskParams.pollInterval ?? 1000
  408. };
  409. const task = await taskStore.createTask(taskOptions, extra.requestId, request, extra.sessionId);
  410. console.log(`\n[Server] ${name} called, task created: ${task.taskId}`);
  411. // Start background task execution
  412. const taskExecution = (async () => {
  413. try {
  414. const taskSession = new TaskSession(server, task.taskId, taskStore, messageQueue);
  415. if (name === 'confirm_delete') {
  416. const filename = args?.filename ?? 'unknown.txt';
  417. console.log(`[Server] confirm_delete: asking about '${filename}'`);
  418. console.log('[Server] Sending elicitation request to client...');
  419. const result = await taskSession.elicit(`Are you sure you want to delete '${filename}'?`, {
  420. type: 'object',
  421. properties: {
  422. confirm: { type: 'boolean' }
  423. },
  424. required: ['confirm']
  425. });
  426. console.log(`[Server] Received elicitation response: action=${result.action}, content=${JSON.stringify(result.content)}`);
  427. let text;
  428. if (result.action === 'accept' && result.content) {
  429. const confirmed = result.content.confirm;
  430. text = confirmed ? `Deleted '${filename}'` : 'Deletion cancelled';
  431. }
  432. else {
  433. text = 'Deletion cancelled';
  434. }
  435. console.log(`[Server] Completing task with result: ${text}`);
  436. await taskStore.storeTaskResult(task.taskId, 'completed', {
  437. content: [{ type: 'text', text }]
  438. });
  439. }
  440. else if (name === 'write_haiku') {
  441. const topic = args?.topic ?? 'nature';
  442. console.log(`[Server] write_haiku: topic '${topic}'`);
  443. console.log('[Server] Sending sampling request to client...');
  444. const result = await taskSession.createMessage([
  445. {
  446. role: 'user',
  447. content: { type: 'text', text: `Write a haiku about ${topic}` }
  448. }
  449. ], 50);
  450. let haiku = 'No response';
  451. if (result.content && 'text' in result.content) {
  452. haiku = result.content.text;
  453. }
  454. console.log(`[Server] Received sampling response: ${haiku.substring(0, 50)}...`);
  455. console.log('[Server] Completing task with haiku');
  456. await taskStore.storeTaskResult(task.taskId, 'completed', {
  457. content: [{ type: 'text', text: `Haiku:\n${haiku}` }]
  458. });
  459. }
  460. }
  461. catch (error) {
  462. console.error(`[Server] Task ${task.taskId} failed:`, error);
  463. await taskStore.storeTaskResult(task.taskId, 'failed', {
  464. content: [{ type: 'text', text: `Error: ${error}` }],
  465. isError: true
  466. });
  467. }
  468. finally {
  469. activeTaskExecutions.delete(task.taskId);
  470. }
  471. })();
  472. activeTaskExecutions.set(task.taskId, {
  473. promise: taskExecution,
  474. server,
  475. sessionId: extra.sessionId ?? ''
  476. });
  477. return { task };
  478. });
  479. // Handle tasks/get
  480. server.setRequestHandler(types_js_1.GetTaskRequestSchema, async (request) => {
  481. const { taskId } = request.params;
  482. const task = await taskStore.getTask(taskId);
  483. if (!task) {
  484. throw new Error(`Task ${taskId} not found`);
  485. }
  486. return task;
  487. });
  488. // Handle tasks/result
  489. server.setRequestHandler(types_js_1.GetTaskPayloadRequestSchema, async (request, extra) => {
  490. const { taskId } = request.params;
  491. console.log(`[Server] tasks/result called for task ${taskId}`);
  492. return taskResultHandler.handle(taskId, server, extra.sessionId ?? '');
  493. });
  494. return server;
  495. };
  496. // ============================================================================
  497. // Express App Setup
  498. // ============================================================================
  499. const app = (0, express_js_1.createMcpExpressApp)();
  500. // Map to store transports by session ID
  501. const transports = {};
  502. // Helper to check if request is initialize
  503. const isInitializeRequest = (body) => {
  504. return typeof body === 'object' && body !== null && 'method' in body && body.method === 'initialize';
  505. };
  506. // MCP POST endpoint
  507. app.post('/mcp', async (req, res) => {
  508. const sessionId = req.headers['mcp-session-id'];
  509. try {
  510. let transport;
  511. if (sessionId && transports[sessionId]) {
  512. transport = transports[sessionId];
  513. }
  514. else if (!sessionId && isInitializeRequest(req.body)) {
  515. transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
  516. sessionIdGenerator: () => (0, node_crypto_1.randomUUID)(),
  517. onsessioninitialized: sid => {
  518. console.log(`Session initialized: ${sid}`);
  519. transports[sid] = transport;
  520. }
  521. });
  522. transport.onclose = () => {
  523. const sid = transport.sessionId;
  524. if (sid && transports[sid]) {
  525. console.log(`Transport closed for session ${sid}`);
  526. delete transports[sid];
  527. }
  528. };
  529. const server = createServer();
  530. await server.connect(transport);
  531. await transport.handleRequest(req, res, req.body);
  532. return;
  533. }
  534. else {
  535. res.status(400).json({
  536. jsonrpc: '2.0',
  537. error: { code: -32000, message: 'Bad Request: No valid session ID' },
  538. id: null
  539. });
  540. return;
  541. }
  542. await transport.handleRequest(req, res, req.body);
  543. }
  544. catch (error) {
  545. console.error('Error handling MCP request:', error);
  546. if (!res.headersSent) {
  547. res.status(500).json({
  548. jsonrpc: '2.0',
  549. error: { code: -32603, message: 'Internal server error' },
  550. id: null
  551. });
  552. }
  553. }
  554. });
  555. // Handle GET requests for SSE streams
  556. app.get('/mcp', async (req, res) => {
  557. const sessionId = req.headers['mcp-session-id'];
  558. if (!sessionId || !transports[sessionId]) {
  559. res.status(400).send('Invalid or missing session ID');
  560. return;
  561. }
  562. const transport = transports[sessionId];
  563. await transport.handleRequest(req, res);
  564. });
  565. // Handle DELETE requests for session termination
  566. app.delete('/mcp', async (req, res) => {
  567. const sessionId = req.headers['mcp-session-id'];
  568. if (!sessionId || !transports[sessionId]) {
  569. res.status(400).send('Invalid or missing session ID');
  570. return;
  571. }
  572. console.log(`Session termination request: ${sessionId}`);
  573. const transport = transports[sessionId];
  574. await transport.handleRequest(req, res);
  575. });
  576. // Start server
  577. app.listen(PORT, () => {
  578. console.log(`Starting server on http://localhost:${PORT}/mcp`);
  579. console.log('\nAvailable tools:');
  580. console.log(' - confirm_delete: Demonstrates elicitation (asks user y/n)');
  581. console.log(' - write_haiku: Demonstrates sampling (requests LLM completion)');
  582. });
  583. // Handle shutdown
  584. process.on('SIGINT', async () => {
  585. console.log('\nShutting down server...');
  586. for (const sessionId of Object.keys(transports)) {
  587. try {
  588. await transports[sessionId].close();
  589. delete transports[sessionId];
  590. }
  591. catch (error) {
  592. console.error(`Error closing session ${sessionId}:`, error);
  593. }
  594. }
  595. taskStore.cleanup();
  596. messageQueue.cleanup();
  597. console.log('Server shutdown complete');
  598. process.exit(0);
  599. });
  600. //# sourceMappingURL=simpleTaskInteractive.js.map