elicitationUrlExample.js 26 KB

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