elicitationUrlExample.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680
  1. "use strict";
  2. // Run with: npx tsx src/examples/client/elicitationUrlExample.ts
  3. //
  4. // This example demonstrates how to use URL elicitation to securely
  5. // collect user input in a remote (HTTP) server.
  6. // URL elicitation allows servers to prompt the end-user to open a URL in their browser
  7. // to collect sensitive information.
  8. Object.defineProperty(exports, "__esModule", { value: true });
  9. const index_js_1 = require("../../client/index.js");
  10. const streamableHttp_js_1 = require("../../client/streamableHttp.js");
  11. const node_readline_1 = require("node:readline");
  12. const types_js_1 = require("../../types.js");
  13. const metadataUtils_js_1 = require("../../shared/metadataUtils.js");
  14. const simpleOAuthClientProvider_js_1 = require("./simpleOAuthClientProvider.js");
  15. const auth_js_1 = require("../../client/auth.js");
  16. const node_http_1 = require("node:http");
  17. // Set up OAuth (required for this example)
  18. const OAUTH_CALLBACK_PORT = 8090; // Use different port than auth server (3001)
  19. const OAUTH_CALLBACK_URL = `http://localhost:${OAUTH_CALLBACK_PORT}/callback`;
  20. let oauthProvider = undefined;
  21. console.log('Getting OAuth token...');
  22. const clientMetadata = {
  23. client_name: 'Elicitation MCP Client',
  24. redirect_uris: [OAUTH_CALLBACK_URL],
  25. grant_types: ['authorization_code', 'refresh_token'],
  26. response_types: ['code'],
  27. token_endpoint_auth_method: 'client_secret_post',
  28. scope: 'mcp:tools'
  29. };
  30. oauthProvider = new simpleOAuthClientProvider_js_1.InMemoryOAuthClientProvider(OAUTH_CALLBACK_URL, clientMetadata, (redirectUrl) => {
  31. console.log(`\n🔗 Please open this URL in your browser to authorize:\n ${redirectUrl.toString()}`);
  32. });
  33. // Create readline interface for user input
  34. const readline = (0, node_readline_1.createInterface)({
  35. input: process.stdin,
  36. output: process.stdout
  37. });
  38. let abortCommand = new AbortController();
  39. // Global client and transport for interactive commands
  40. let client = null;
  41. let transport = null;
  42. let serverUrl = 'http://localhost:3000/mcp';
  43. let sessionId = undefined;
  44. let isProcessingCommand = false;
  45. let isProcessingElicitations = false;
  46. const elicitationQueue = [];
  47. let elicitationQueueSignal = null;
  48. let elicitationsCompleteSignal = null;
  49. // Map to track pending URL elicitations waiting for completion notifications
  50. const pendingURLElicitations = new Map();
  51. async function main() {
  52. console.log('MCP Interactive Client');
  53. console.log('=====================');
  54. // Connect to server immediately with default settings
  55. await connect();
  56. // Start the elicitation loop in the background
  57. elicitationLoop().catch(error => {
  58. console.error('Unexpected error in elicitation loop:', error);
  59. process.exit(1);
  60. });
  61. // Short delay allowing the server to send any SSE elicitations on connection
  62. await new Promise(resolve => setTimeout(resolve, 200));
  63. // Wait until we are done processing any initial elicitations
  64. await waitForElicitationsToComplete();
  65. // Print help and start the command loop
  66. printHelp();
  67. await commandLoop();
  68. }
  69. async function waitForElicitationsToComplete() {
  70. // Wait until the queue is empty and nothing is being processed
  71. while (elicitationQueue.length > 0 || isProcessingElicitations) {
  72. await new Promise(resolve => setTimeout(resolve, 100));
  73. }
  74. }
  75. function printHelp() {
  76. console.log('\nAvailable commands:');
  77. console.log(' connect [url] - Connect to MCP server (default: http://localhost:3000/mcp)');
  78. console.log(' disconnect - Disconnect from server');
  79. console.log(' terminate-session - Terminate the current session');
  80. console.log(' reconnect - Reconnect to the server');
  81. console.log(' list-tools - List available tools');
  82. console.log(' call-tool <name> [args] - Call a tool with optional JSON arguments');
  83. console.log(' payment-confirm - Test URL elicitation via error response with payment-confirm tool');
  84. console.log(' third-party-auth - Test tool that requires third-party OAuth credentials');
  85. console.log(' help - Show this help');
  86. console.log(' quit - Exit the program');
  87. }
  88. async function commandLoop() {
  89. await new Promise(resolve => {
  90. if (!isProcessingElicitations) {
  91. resolve();
  92. }
  93. else {
  94. elicitationsCompleteSignal = resolve;
  95. }
  96. });
  97. readline.question('\n> ', { signal: abortCommand.signal }, async (input) => {
  98. isProcessingCommand = true;
  99. const args = input.trim().split(/\s+/);
  100. const command = args[0]?.toLowerCase();
  101. try {
  102. switch (command) {
  103. case 'connect':
  104. await connect(args[1]);
  105. break;
  106. case 'disconnect':
  107. await disconnect();
  108. break;
  109. case 'terminate-session':
  110. await terminateSession();
  111. break;
  112. case 'reconnect':
  113. await reconnect();
  114. break;
  115. case 'list-tools':
  116. await listTools();
  117. break;
  118. case 'call-tool':
  119. if (args.length < 2) {
  120. console.log('Usage: call-tool <name> [args]');
  121. }
  122. else {
  123. const toolName = args[1];
  124. let toolArgs = {};
  125. if (args.length > 2) {
  126. try {
  127. toolArgs = JSON.parse(args.slice(2).join(' '));
  128. }
  129. catch {
  130. console.log('Invalid JSON arguments. Using empty args.');
  131. }
  132. }
  133. await callTool(toolName, toolArgs);
  134. }
  135. break;
  136. case 'payment-confirm':
  137. await callPaymentConfirmTool();
  138. break;
  139. case 'third-party-auth':
  140. await callThirdPartyAuthTool();
  141. break;
  142. case 'help':
  143. printHelp();
  144. break;
  145. case 'quit':
  146. case 'exit':
  147. await cleanup();
  148. return;
  149. default:
  150. if (command) {
  151. console.log(`Unknown command: ${command}`);
  152. }
  153. break;
  154. }
  155. }
  156. catch (error) {
  157. console.error(`Error executing command: ${error}`);
  158. }
  159. finally {
  160. isProcessingCommand = false;
  161. }
  162. // Process another command after we've processed the this one
  163. await commandLoop();
  164. });
  165. }
  166. async function elicitationLoop() {
  167. while (true) {
  168. // Wait until we have elicitations to process
  169. await new Promise(resolve => {
  170. if (elicitationQueue.length > 0) {
  171. resolve();
  172. }
  173. else {
  174. elicitationQueueSignal = resolve;
  175. }
  176. });
  177. isProcessingElicitations = true;
  178. abortCommand.abort(); // Abort the command loop if it's running
  179. // Process all queued elicitations
  180. while (elicitationQueue.length > 0) {
  181. const queued = elicitationQueue.shift();
  182. console.log(`📤 Processing queued elicitation (${elicitationQueue.length} remaining)`);
  183. try {
  184. const result = await handleElicitationRequest(queued.request);
  185. queued.resolve(result);
  186. }
  187. catch (error) {
  188. queued.reject(error instanceof Error ? error : new Error(String(error)));
  189. }
  190. }
  191. console.log('✅ All queued elicitations processed. Resuming command loop...\n');
  192. isProcessingElicitations = false;
  193. // Reset the abort controller for the next command loop
  194. abortCommand = new AbortController();
  195. // Resume the command loop
  196. if (elicitationsCompleteSignal) {
  197. elicitationsCompleteSignal();
  198. elicitationsCompleteSignal = null;
  199. }
  200. }
  201. }
  202. /**
  203. * Enqueues an elicitation request and returns the result.
  204. *
  205. * This function is used so that our CLI (which can only handle one input request at a time)
  206. * can handle elicitation requests and the command loop.
  207. *
  208. * @param request - The elicitation request to be handled
  209. * @returns The elicitation result
  210. */
  211. async function elicitationRequestHandler(request) {
  212. // If we are processing a command, handle this elicitation immediately
  213. if (isProcessingCommand) {
  214. console.log('📋 Processing elicitation immediately (during command execution)');
  215. return await handleElicitationRequest(request);
  216. }
  217. // Otherwise, queue the request to be handled by the elicitation loop
  218. console.log(`📥 Queueing elicitation request (queue size will be: ${elicitationQueue.length + 1})`);
  219. return new Promise((resolve, reject) => {
  220. elicitationQueue.push({
  221. request,
  222. resolve,
  223. reject
  224. });
  225. // Signal the elicitation loop that there's work to do
  226. if (elicitationQueueSignal) {
  227. elicitationQueueSignal();
  228. elicitationQueueSignal = null;
  229. }
  230. });
  231. }
  232. /**
  233. * Handles an elicitation request.
  234. *
  235. * This function is used to handle the elicitation request and return the result.
  236. *
  237. * @param request - The elicitation request to be handled
  238. * @returns The elicitation result
  239. */
  240. async function handleElicitationRequest(request) {
  241. const mode = request.params.mode;
  242. console.log('\n🔔 Elicitation Request Received:');
  243. console.log(`Mode: ${mode}`);
  244. if (mode === 'url') {
  245. return {
  246. action: await handleURLElicitation(request.params)
  247. };
  248. }
  249. else {
  250. // Should not happen because the client declares its capabilities to the server,
  251. // but being defensive is a good practice:
  252. throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, `Unsupported elicitation mode: ${mode}`);
  253. }
  254. }
  255. /**
  256. * Handles a URL elicitation by opening the URL in the browser.
  257. *
  258. * Note: This is a shared code for both request handlers and error handlers.
  259. * As a result of sharing schema, there is no big forking of logic for the client.
  260. *
  261. * @param params - The URL elicitation request parameters
  262. * @returns The action to take (accept, cancel, or decline)
  263. */
  264. async function handleURLElicitation(params) {
  265. const url = params.url;
  266. const elicitationId = params.elicitationId;
  267. const message = params.message;
  268. console.log(`🆔 Elicitation ID: ${elicitationId}`); // Print for illustration
  269. // Parse URL to show domain for security
  270. let domain = 'unknown domain';
  271. try {
  272. const parsedUrl = new URL(url);
  273. domain = parsedUrl.hostname;
  274. }
  275. catch {
  276. console.error('Invalid URL provided by server');
  277. return 'decline';
  278. }
  279. // Example security warning to help prevent phishing attacks
  280. console.log('\n⚠️ \x1b[33mSECURITY WARNING\x1b[0m ⚠️');
  281. console.log('\x1b[33mThe server is requesting you to open an external URL.\x1b[0m');
  282. console.log('\x1b[33mOnly proceed if you trust this server and understand why it needs this.\x1b[0m\n');
  283. console.log(`🌐 Target domain: \x1b[36m${domain}\x1b[0m`);
  284. console.log(`🔗 Full URL: \x1b[36m${url}\x1b[0m`);
  285. console.log(`\nℹ️ Server's reason:\n\n\x1b[36m${message}\x1b[0m\n`);
  286. // 1. Ask for user consent to open the URL
  287. const consent = await new Promise(resolve => {
  288. readline.question('\nDo you want to open this URL in your browser? (y/n): ', input => {
  289. resolve(input.trim().toLowerCase());
  290. });
  291. });
  292. // 2. If user did not consent, return appropriate result
  293. if (consent === 'no' || consent === 'n') {
  294. console.log('❌ URL navigation declined.');
  295. return 'decline';
  296. }
  297. else if (consent !== 'yes' && consent !== 'y') {
  298. console.log('🚫 Invalid response. Cancelling elicitation.');
  299. return 'cancel';
  300. }
  301. // 3. Wait for completion notification in the background
  302. const completionPromise = new Promise((resolve, reject) => {
  303. const timeout = setTimeout(() => {
  304. pendingURLElicitations.delete(elicitationId);
  305. console.log(`\x1b[31m❌ Elicitation ${elicitationId} timed out waiting for completion.\x1b[0m`);
  306. reject(new Error('Elicitation completion timeout'));
  307. }, 5 * 60 * 1000); // 5 minute timeout
  308. pendingURLElicitations.set(elicitationId, {
  309. resolve: () => {
  310. clearTimeout(timeout);
  311. resolve();
  312. },
  313. reject,
  314. timeout
  315. });
  316. });
  317. completionPromise.catch(error => {
  318. console.error('Background completion wait failed:', error);
  319. });
  320. // 4. Direct user to open the URL in their browser
  321. console.log(`\n🔗 Please open this URL in your browser:\n ${url}`);
  322. console.log('\n⏳ Waiting for you to complete the interaction in your browser...');
  323. console.log(' The server will send a notification once you complete the action.');
  324. // 5. Acknowledge the user accepted the elicitation
  325. return 'accept';
  326. }
  327. /**
  328. * Example OAuth callback handler - in production, use a more robust approach
  329. * for handling callbacks and storing tokens
  330. */
  331. /**
  332. * Starts a temporary HTTP server to receive the OAuth callback
  333. */
  334. async function waitForOAuthCallback() {
  335. return new Promise((resolve, reject) => {
  336. const server = (0, node_http_1.createServer)((req, res) => {
  337. // Ignore favicon requests
  338. if (req.url === '/favicon.ico') {
  339. res.writeHead(404);
  340. res.end();
  341. return;
  342. }
  343. console.log(`📥 Received callback: ${req.url}`);
  344. const parsedUrl = new URL(req.url || '', 'http://localhost');
  345. const code = parsedUrl.searchParams.get('code');
  346. const error = parsedUrl.searchParams.get('error');
  347. if (code) {
  348. console.log(`✅ Authorization code received: ${code?.substring(0, 10)}...`);
  349. res.writeHead(200, { 'Content-Type': 'text/html' });
  350. res.end(`
  351. <html>
  352. <body>
  353. <h1>Authorization Successful!</h1>
  354. <p>This simulates successful authorization of the MCP client, which now has an access token for the MCP server.</p>
  355. <p>This window will close automatically in 10 seconds.</p>
  356. <script>setTimeout(() => window.close(), 10000);</script>
  357. </body>
  358. </html>
  359. `);
  360. resolve(code);
  361. setTimeout(() => server.close(), 15000);
  362. }
  363. else if (error) {
  364. console.log(`❌ Authorization error: ${error}`);
  365. res.writeHead(400, { 'Content-Type': 'text/html' });
  366. res.end(`
  367. <html>
  368. <body>
  369. <h1>Authorization Failed</h1>
  370. <p>Error: ${error}</p>
  371. </body>
  372. </html>
  373. `);
  374. reject(new Error(`OAuth authorization failed: ${error}`));
  375. }
  376. else {
  377. console.log(`❌ No authorization code or error in callback`);
  378. res.writeHead(400);
  379. res.end('Bad request');
  380. reject(new Error('No authorization code provided'));
  381. }
  382. });
  383. server.listen(OAUTH_CALLBACK_PORT, () => {
  384. console.log(`OAuth callback server started on http://localhost:${OAUTH_CALLBACK_PORT}`);
  385. });
  386. });
  387. }
  388. /**
  389. * Attempts to connect to the MCP server with OAuth authentication.
  390. * Handles OAuth flow recursively if authorization is required.
  391. */
  392. async function attemptConnection(oauthProvider) {
  393. console.log('🚢 Creating transport with OAuth provider...');
  394. const baseUrl = new URL(serverUrl);
  395. transport = new streamableHttp_js_1.StreamableHTTPClientTransport(baseUrl, {
  396. sessionId: sessionId,
  397. authProvider: oauthProvider
  398. });
  399. console.log('🚢 Transport created');
  400. try {
  401. console.log('🔌 Attempting connection (this will trigger OAuth redirect if needed)...');
  402. await client.connect(transport);
  403. sessionId = transport.sessionId;
  404. console.log('Transport created with session ID:', sessionId);
  405. console.log('✅ Connected successfully');
  406. }
  407. catch (error) {
  408. if (error instanceof auth_js_1.UnauthorizedError) {
  409. console.log('🔐 OAuth required - waiting for authorization...');
  410. const callbackPromise = waitForOAuthCallback();
  411. const authCode = await callbackPromise;
  412. await transport.finishAuth(authCode);
  413. console.log('🔐 Authorization code received:', authCode);
  414. console.log('🔌 Reconnecting with authenticated transport...');
  415. // Recursively retry connection after OAuth completion
  416. await attemptConnection(oauthProvider);
  417. }
  418. else {
  419. console.error('❌ Connection failed with non-auth error:', error);
  420. throw error;
  421. }
  422. }
  423. }
  424. async function connect(url) {
  425. if (client) {
  426. console.log('Already connected. Disconnect first.');
  427. return;
  428. }
  429. if (url) {
  430. serverUrl = url;
  431. }
  432. console.log(`🔗 Attempting to connect to ${serverUrl}...`);
  433. // Create a new client with elicitation capability
  434. console.log('👤 Creating MCP client...');
  435. client = new index_js_1.Client({
  436. name: 'example-client',
  437. version: '1.0.0'
  438. }, {
  439. capabilities: {
  440. elicitation: {
  441. // Only URL elicitation is supported in this demo
  442. // (see server/elicitationExample.ts for a demo of form mode elicitation)
  443. url: {}
  444. }
  445. }
  446. });
  447. console.log('👤 Client created');
  448. // Set up elicitation request handler with proper validation
  449. client.setRequestHandler(types_js_1.ElicitRequestSchema, elicitationRequestHandler);
  450. // Set up notification handler for elicitation completion
  451. client.setNotificationHandler(types_js_1.ElicitationCompleteNotificationSchema, notification => {
  452. const { elicitationId } = notification.params;
  453. const pending = pendingURLElicitations.get(elicitationId);
  454. if (pending) {
  455. clearTimeout(pending.timeout);
  456. pendingURLElicitations.delete(elicitationId);
  457. console.log(`\x1b[32m✅ Elicitation ${elicitationId} completed!\x1b[0m`);
  458. pending.resolve();
  459. }
  460. else {
  461. // Shouldn't happen - discard it!
  462. console.warn(`Received completion notification for unknown elicitation: ${elicitationId}`);
  463. }
  464. });
  465. try {
  466. console.log('🔐 Starting OAuth flow...');
  467. await attemptConnection(oauthProvider);
  468. console.log('Connected to MCP server');
  469. // Set up error handler after connection is established so we don't double log errors
  470. client.onerror = error => {
  471. console.error('\x1b[31mClient error:', error, '\x1b[0m');
  472. };
  473. }
  474. catch (error) {
  475. console.error('Failed to connect:', error);
  476. client = null;
  477. transport = null;
  478. return;
  479. }
  480. }
  481. async function disconnect() {
  482. if (!client || !transport) {
  483. console.log('Not connected.');
  484. return;
  485. }
  486. try {
  487. await transport.close();
  488. console.log('Disconnected from MCP server');
  489. client = null;
  490. transport = null;
  491. }
  492. catch (error) {
  493. console.error('Error disconnecting:', error);
  494. }
  495. }
  496. async function terminateSession() {
  497. if (!client || !transport) {
  498. console.log('Not connected.');
  499. return;
  500. }
  501. try {
  502. console.log('Terminating session with ID:', transport.sessionId);
  503. await transport.terminateSession();
  504. console.log('Session terminated successfully');
  505. // Check if sessionId was cleared after termination
  506. if (!transport.sessionId) {
  507. console.log('Session ID has been cleared');
  508. sessionId = undefined;
  509. // Also close the transport and clear client objects
  510. await transport.close();
  511. console.log('Transport closed after session termination');
  512. client = null;
  513. transport = null;
  514. }
  515. else {
  516. console.log('Server responded with 405 Method Not Allowed (session termination not supported)');
  517. console.log('Session ID is still active:', transport.sessionId);
  518. }
  519. }
  520. catch (error) {
  521. console.error('Error terminating session:', error);
  522. }
  523. }
  524. async function reconnect() {
  525. if (client) {
  526. await disconnect();
  527. }
  528. await connect();
  529. }
  530. async function listTools() {
  531. if (!client) {
  532. console.log('Not connected to server.');
  533. return;
  534. }
  535. try {
  536. const toolsRequest = {
  537. method: 'tools/list',
  538. params: {}
  539. };
  540. const toolsResult = await client.request(toolsRequest, types_js_1.ListToolsResultSchema);
  541. console.log('Available tools:');
  542. if (toolsResult.tools.length === 0) {
  543. console.log(' No tools available');
  544. }
  545. else {
  546. for (const tool of toolsResult.tools) {
  547. console.log(` - id: ${tool.name}, name: ${(0, metadataUtils_js_1.getDisplayName)(tool)}, description: ${tool.description}`);
  548. }
  549. }
  550. }
  551. catch (error) {
  552. console.log(`Tools not supported by this server (${error})`);
  553. }
  554. }
  555. async function callTool(name, args) {
  556. if (!client) {
  557. console.log('Not connected to server.');
  558. return;
  559. }
  560. try {
  561. const request = {
  562. method: 'tools/call',
  563. params: {
  564. name,
  565. arguments: args
  566. }
  567. };
  568. console.log(`Calling tool '${name}' with args:`, args);
  569. const result = await client.request(request, types_js_1.CallToolResultSchema);
  570. console.log('Tool result:');
  571. const resourceLinks = [];
  572. result.content.forEach(item => {
  573. if (item.type === 'text') {
  574. console.log(` ${item.text}`);
  575. }
  576. else if (item.type === 'resource_link') {
  577. const resourceLink = item;
  578. resourceLinks.push(resourceLink);
  579. console.log(` 📁 Resource Link: ${resourceLink.name}`);
  580. console.log(` URI: ${resourceLink.uri}`);
  581. if (resourceLink.mimeType) {
  582. console.log(` Type: ${resourceLink.mimeType}`);
  583. }
  584. if (resourceLink.description) {
  585. console.log(` Description: ${resourceLink.description}`);
  586. }
  587. }
  588. else if (item.type === 'resource') {
  589. console.log(` [Embedded Resource: ${item.resource.uri}]`);
  590. }
  591. else if (item.type === 'image') {
  592. console.log(` [Image: ${item.mimeType}]`);
  593. }
  594. else if (item.type === 'audio') {
  595. console.log(` [Audio: ${item.mimeType}]`);
  596. }
  597. else {
  598. console.log(` [Unknown content type]:`, item);
  599. }
  600. });
  601. // Offer to read resource links
  602. if (resourceLinks.length > 0) {
  603. console.log(`\nFound ${resourceLinks.length} resource link(s). Use 'read-resource <uri>' to read their content.`);
  604. }
  605. }
  606. catch (error) {
  607. if (error instanceof types_js_1.UrlElicitationRequiredError) {
  608. console.log('\n🔔 Elicitation Required Error Received:');
  609. console.log(`Message: ${error.message}`);
  610. for (const e of error.elicitations) {
  611. await handleURLElicitation(e); // For the error handler, we discard the action result because we don't respond to an error response
  612. }
  613. return;
  614. }
  615. console.log(`Error calling tool ${name}: ${error}`);
  616. }
  617. }
  618. async function cleanup() {
  619. if (client && transport) {
  620. try {
  621. // First try to terminate the session gracefully
  622. if (transport.sessionId) {
  623. try {
  624. console.log('Terminating session before exit...');
  625. await transport.terminateSession();
  626. console.log('Session terminated successfully');
  627. }
  628. catch (error) {
  629. console.error('Error terminating session:', error);
  630. }
  631. }
  632. // Then close the transport
  633. await transport.close();
  634. }
  635. catch (error) {
  636. console.error('Error closing transport:', error);
  637. }
  638. }
  639. process.stdin.setRawMode(false);
  640. readline.close();
  641. console.log('\nGoodbye!');
  642. process.exit(0);
  643. }
  644. async function callPaymentConfirmTool() {
  645. console.log('Calling payment-confirm tool...');
  646. await callTool('payment-confirm', { cartId: 'cart_123' });
  647. }
  648. async function callThirdPartyAuthTool() {
  649. console.log('Calling third-party-auth tool...');
  650. await callTool('third-party-auth', { param1: 'test' });
  651. }
  652. // Set up raw mode for keyboard input to capture Escape key
  653. process.stdin.setRawMode(true);
  654. process.stdin.on('data', async (data) => {
  655. // Check for Escape key (27)
  656. if (data.length === 1 && data[0] === 27) {
  657. console.log('\nESC key pressed. Disconnecting from server...');
  658. // Abort current operation and disconnect from server
  659. if (client && transport) {
  660. await disconnect();
  661. console.log('Disconnected. Press Enter to continue.');
  662. }
  663. else {
  664. console.log('Not connected to server.');
  665. }
  666. // Re-display the prompt
  667. process.stdout.write('> ');
  668. }
  669. });
  670. // Handle Ctrl+C
  671. process.on('SIGINT', async () => {
  672. console.log('\nReceived SIGINT. Cleaning up...');
  673. await cleanup();
  674. });
  675. // Start the interactive client
  676. main().catch((error) => {
  677. console.error('Error running MCP client:', error);
  678. process.exit(1);
  679. });
  680. //# sourceMappingURL=elicitationUrlExample.js.map