simpleStreamableHttp.js 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855
  1. import { Client } from '../../client/index.js';
  2. import { StreamableHTTPClientTransport } from '../../client/streamableHttp.js';
  3. import { createInterface } from 'node:readline';
  4. import { ListToolsResultSchema, CallToolResultSchema, ListPromptsResultSchema, GetPromptResultSchema, ListResourcesResultSchema, LoggingMessageNotificationSchema, ResourceListChangedNotificationSchema, ElicitRequestSchema, ReadResourceResultSchema, RELATED_TASK_META_KEY, ErrorCode, McpError } from '../../types.js';
  5. import { InMemoryTaskStore } from '../../experimental/tasks/stores/in-memory.js';
  6. import { getDisplayName } from '../../shared/metadataUtils.js';
  7. import { Ajv } from 'ajv';
  8. // Create readline interface for user input
  9. const readline = createInterface({
  10. input: process.stdin,
  11. output: process.stdout
  12. });
  13. // Track received notifications for debugging resumability
  14. let notificationCount = 0;
  15. // Global client and transport for interactive commands
  16. let client = null;
  17. let transport = null;
  18. let serverUrl = 'http://localhost:3000/mcp';
  19. let notificationsToolLastEventId = undefined;
  20. let sessionId = undefined;
  21. async function main() {
  22. console.log('MCP Interactive Client');
  23. console.log('=====================');
  24. // Connect to server immediately with default settings
  25. await connect();
  26. // Print help and start the command loop
  27. printHelp();
  28. commandLoop();
  29. }
  30. function printHelp() {
  31. console.log('\nAvailable commands:');
  32. console.log(' connect [url] - Connect to MCP server (default: http://localhost:3000/mcp)');
  33. console.log(' disconnect - Disconnect from server');
  34. console.log(' terminate-session - Terminate the current session');
  35. console.log(' reconnect - Reconnect to the server');
  36. console.log(' list-tools - List available tools');
  37. console.log(' call-tool <name> [args] - Call a tool with optional JSON arguments');
  38. console.log(' call-tool-task <name> [args] - Call a tool with task-based execution (example: call-tool-task delay {"duration":3000})');
  39. console.log(' greet [name] - Call the greet tool');
  40. console.log(' multi-greet [name] - Call the multi-greet tool with notifications');
  41. console.log(' collect-info [type] - Test form elicitation with collect-user-info tool (contact/preferences/feedback)');
  42. console.log(' collect-info-task [type] - Test bidirectional task support (server+client tasks) with elicitation');
  43. console.log(' start-notifications [interval] [count] - Start periodic notifications');
  44. console.log(' run-notifications-tool-with-resumability [interval] [count] - Run notification tool with resumability');
  45. console.log(' list-prompts - List available prompts');
  46. console.log(' get-prompt [name] [args] - Get a prompt with optional JSON arguments');
  47. console.log(' list-resources - List available resources');
  48. console.log(' read-resource <uri> - Read a specific resource by URI');
  49. console.log(' help - Show this help');
  50. console.log(' quit - Exit the program');
  51. }
  52. function commandLoop() {
  53. readline.question('\n> ', async (input) => {
  54. const args = input.trim().split(/\s+/);
  55. const command = args[0]?.toLowerCase();
  56. try {
  57. switch (command) {
  58. case 'connect':
  59. await connect(args[1]);
  60. break;
  61. case 'disconnect':
  62. await disconnect();
  63. break;
  64. case 'terminate-session':
  65. await terminateSession();
  66. break;
  67. case 'reconnect':
  68. await reconnect();
  69. break;
  70. case 'list-tools':
  71. await listTools();
  72. break;
  73. case 'call-tool':
  74. if (args.length < 2) {
  75. console.log('Usage: call-tool <name> [args]');
  76. }
  77. else {
  78. const toolName = args[1];
  79. let toolArgs = {};
  80. if (args.length > 2) {
  81. try {
  82. toolArgs = JSON.parse(args.slice(2).join(' '));
  83. }
  84. catch {
  85. console.log('Invalid JSON arguments. Using empty args.');
  86. }
  87. }
  88. await callTool(toolName, toolArgs);
  89. }
  90. break;
  91. case 'greet':
  92. await callGreetTool(args[1] || 'MCP User');
  93. break;
  94. case 'multi-greet':
  95. await callMultiGreetTool(args[1] || 'MCP User');
  96. break;
  97. case 'collect-info':
  98. await callCollectInfoTool(args[1] || 'contact');
  99. break;
  100. case 'collect-info-task': {
  101. await callCollectInfoWithTask(args[1] || 'contact');
  102. break;
  103. }
  104. case 'start-notifications': {
  105. const interval = args[1] ? parseInt(args[1], 10) : 2000;
  106. const count = args[2] ? parseInt(args[2], 10) : 10;
  107. await startNotifications(interval, count);
  108. break;
  109. }
  110. case 'run-notifications-tool-with-resumability': {
  111. const interval = args[1] ? parseInt(args[1], 10) : 2000;
  112. const count = args[2] ? parseInt(args[2], 10) : 10;
  113. await runNotificationsToolWithResumability(interval, count);
  114. break;
  115. }
  116. case 'call-tool-task':
  117. if (args.length < 2) {
  118. console.log('Usage: call-tool-task <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 callToolTask(toolName, toolArgs);
  132. }
  133. break;
  134. case 'list-prompts':
  135. await listPrompts();
  136. break;
  137. case 'get-prompt':
  138. if (args.length < 2) {
  139. console.log('Usage: get-prompt <name> [args]');
  140. }
  141. else {
  142. const promptName = args[1];
  143. let promptArgs = {};
  144. if (args.length > 2) {
  145. try {
  146. promptArgs = JSON.parse(args.slice(2).join(' '));
  147. }
  148. catch {
  149. console.log('Invalid JSON arguments. Using empty args.');
  150. }
  151. }
  152. await getPrompt(promptName, promptArgs);
  153. }
  154. break;
  155. case 'list-resources':
  156. await listResources();
  157. break;
  158. case 'read-resource':
  159. if (args.length < 2) {
  160. console.log('Usage: read-resource <uri>');
  161. }
  162. else {
  163. await readResource(args[1]);
  164. }
  165. break;
  166. case 'help':
  167. printHelp();
  168. break;
  169. case 'quit':
  170. case 'exit':
  171. await cleanup();
  172. return;
  173. default:
  174. if (command) {
  175. console.log(`Unknown command: ${command}`);
  176. }
  177. break;
  178. }
  179. }
  180. catch (error) {
  181. console.error(`Error executing command: ${error}`);
  182. }
  183. // Continue the command loop
  184. commandLoop();
  185. });
  186. }
  187. async function connect(url) {
  188. if (client) {
  189. console.log('Already connected. Disconnect first.');
  190. return;
  191. }
  192. if (url) {
  193. serverUrl = url;
  194. }
  195. console.log(`Connecting to ${serverUrl}...`);
  196. try {
  197. // Create task store for client-side task support
  198. const clientTaskStore = new InMemoryTaskStore();
  199. // Create a new client with form elicitation capability and task support
  200. client = new Client({
  201. name: 'example-client',
  202. version: '1.0.0'
  203. }, {
  204. capabilities: {
  205. elicitation: {
  206. form: {}
  207. },
  208. tasks: {
  209. requests: {
  210. elicitation: {
  211. create: {}
  212. }
  213. }
  214. }
  215. },
  216. taskStore: clientTaskStore
  217. });
  218. client.onerror = error => {
  219. console.error('\x1b[31mClient error:', error, '\x1b[0m');
  220. };
  221. // Set up elicitation request handler with proper validation and task support
  222. client.setRequestHandler(ElicitRequestSchema, async (request, extra) => {
  223. if (request.params.mode !== 'form') {
  224. throw new McpError(ErrorCode.InvalidParams, `Unsupported elicitation mode: ${request.params.mode}`);
  225. }
  226. console.log('\n🔔 Elicitation (form) Request Received:');
  227. console.log(`Message: ${request.params.message}`);
  228. console.log(`Related Task: ${request.params._meta?.[RELATED_TASK_META_KEY]?.taskId}`);
  229. console.log(`Task Creation Requested: ${request.params.task ? 'yes' : 'no'}`);
  230. console.log('Requested Schema:');
  231. console.log(JSON.stringify(request.params.requestedSchema, null, 2));
  232. // Helper to return result, optionally creating a task if requested
  233. const returnResult = async (result) => {
  234. if (request.params.task && extra.taskStore) {
  235. // Create a task and store the result
  236. const task = await extra.taskStore.createTask({ ttl: extra.taskRequestedTtl });
  237. await extra.taskStore.storeTaskResult(task.taskId, 'completed', result);
  238. console.log(`📋 Created client-side task: ${task.taskId}`);
  239. return { task };
  240. }
  241. return result;
  242. };
  243. const schema = request.params.requestedSchema;
  244. const properties = schema.properties;
  245. const required = schema.required || [];
  246. // Set up AJV validator for the requested schema
  247. const ajv = new Ajv();
  248. const validate = ajv.compile(schema);
  249. let attempts = 0;
  250. const maxAttempts = 3;
  251. while (attempts < maxAttempts) {
  252. attempts++;
  253. console.log(`\nPlease provide the following information (attempt ${attempts}/${maxAttempts}):`);
  254. const content = {};
  255. let inputCancelled = false;
  256. // Collect input for each field
  257. for (const [fieldName, fieldSchema] of Object.entries(properties)) {
  258. const field = fieldSchema;
  259. const isRequired = required.includes(fieldName);
  260. let prompt = `${field.title || fieldName}`;
  261. // Add helpful information to the prompt
  262. if (field.description) {
  263. prompt += ` (${field.description})`;
  264. }
  265. if (field.enum) {
  266. prompt += ` [options: ${field.enum.join(', ')}]`;
  267. }
  268. if (field.type === 'number' || field.type === 'integer') {
  269. if (field.minimum !== undefined && field.maximum !== undefined) {
  270. prompt += ` [${field.minimum}-${field.maximum}]`;
  271. }
  272. else if (field.minimum !== undefined) {
  273. prompt += ` [min: ${field.minimum}]`;
  274. }
  275. else if (field.maximum !== undefined) {
  276. prompt += ` [max: ${field.maximum}]`;
  277. }
  278. }
  279. if (field.type === 'string' && field.format) {
  280. prompt += ` [format: ${field.format}]`;
  281. }
  282. if (isRequired) {
  283. prompt += ' *required*';
  284. }
  285. if (field.default !== undefined) {
  286. prompt += ` [default: ${field.default}]`;
  287. }
  288. prompt += ': ';
  289. const answer = await new Promise(resolve => {
  290. readline.question(prompt, input => {
  291. resolve(input.trim());
  292. });
  293. });
  294. // Check for cancellation
  295. if (answer.toLowerCase() === 'cancel' || answer.toLowerCase() === 'c') {
  296. inputCancelled = true;
  297. break;
  298. }
  299. // Parse and validate the input
  300. try {
  301. if (answer === '' && field.default !== undefined) {
  302. content[fieldName] = field.default;
  303. }
  304. else if (answer === '' && !isRequired) {
  305. // Skip optional empty fields
  306. continue;
  307. }
  308. else if (answer === '') {
  309. throw new Error(`${fieldName} is required`);
  310. }
  311. else {
  312. // Parse the value based on type
  313. let parsedValue;
  314. if (field.type === 'boolean') {
  315. parsedValue = answer.toLowerCase() === 'true' || answer.toLowerCase() === 'yes' || answer === '1';
  316. }
  317. else if (field.type === 'number') {
  318. parsedValue = parseFloat(answer);
  319. if (isNaN(parsedValue)) {
  320. throw new Error(`${fieldName} must be a valid number`);
  321. }
  322. }
  323. else if (field.type === 'integer') {
  324. parsedValue = parseInt(answer, 10);
  325. if (isNaN(parsedValue)) {
  326. throw new Error(`${fieldName} must be a valid integer`);
  327. }
  328. }
  329. else if (field.enum) {
  330. if (!field.enum.includes(answer)) {
  331. throw new Error(`${fieldName} must be one of: ${field.enum.join(', ')}`);
  332. }
  333. parsedValue = answer;
  334. }
  335. else {
  336. parsedValue = answer;
  337. }
  338. content[fieldName] = parsedValue;
  339. }
  340. }
  341. catch (error) {
  342. console.log(`❌ Error: ${error}`);
  343. // Continue to next attempt
  344. break;
  345. }
  346. }
  347. if (inputCancelled) {
  348. return returnResult({ action: 'cancel' });
  349. }
  350. // If we didn't complete all fields due to an error, try again
  351. if (Object.keys(content).length !==
  352. Object.keys(properties).filter(name => required.includes(name) || content[name] !== undefined).length) {
  353. if (attempts < maxAttempts) {
  354. console.log('Please try again...');
  355. continue;
  356. }
  357. else {
  358. console.log('Maximum attempts reached. Declining request.');
  359. return returnResult({ action: 'decline' });
  360. }
  361. }
  362. // Validate the complete object against the schema
  363. const isValid = validate(content);
  364. if (!isValid) {
  365. console.log('❌ Validation errors:');
  366. validate.errors?.forEach(error => {
  367. console.log(` - ${error.instancePath || 'root'}: ${error.message}`);
  368. });
  369. if (attempts < maxAttempts) {
  370. console.log('Please correct the errors and try again...');
  371. continue;
  372. }
  373. else {
  374. console.log('Maximum attempts reached. Declining request.');
  375. return returnResult({ action: 'decline' });
  376. }
  377. }
  378. // Show the collected data and ask for confirmation
  379. console.log('\n✅ Collected data:');
  380. console.log(JSON.stringify(content, null, 2));
  381. const confirmAnswer = await new Promise(resolve => {
  382. readline.question('\nSubmit this information? (yes/no/cancel): ', input => {
  383. resolve(input.trim().toLowerCase());
  384. });
  385. });
  386. switch (confirmAnswer) {
  387. case 'yes':
  388. case 'y': {
  389. return returnResult({
  390. action: 'accept',
  391. content: content
  392. });
  393. }
  394. case 'cancel':
  395. case 'c': {
  396. return returnResult({ action: 'cancel' });
  397. }
  398. case 'no':
  399. case 'n': {
  400. if (attempts < maxAttempts) {
  401. console.log('Please re-enter the information...');
  402. continue;
  403. }
  404. else {
  405. return returnResult({ action: 'decline' });
  406. }
  407. break;
  408. }
  409. }
  410. }
  411. console.log('Maximum attempts reached. Declining request.');
  412. return returnResult({ action: 'decline' });
  413. });
  414. transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
  415. sessionId: sessionId
  416. });
  417. // Set up notification handlers
  418. client.setNotificationHandler(LoggingMessageNotificationSchema, notification => {
  419. notificationCount++;
  420. console.log(`\nNotification #${notificationCount}: ${notification.params.level} - ${notification.params.data}`);
  421. // Re-display the prompt
  422. process.stdout.write('> ');
  423. });
  424. client.setNotificationHandler(ResourceListChangedNotificationSchema, async (_) => {
  425. console.log(`\nResource list changed notification received!`);
  426. try {
  427. if (!client) {
  428. console.log('Client disconnected, cannot fetch resources');
  429. return;
  430. }
  431. const resourcesResult = await client.request({
  432. method: 'resources/list',
  433. params: {}
  434. }, ListResourcesResultSchema);
  435. console.log('Available resources count:', resourcesResult.resources.length);
  436. }
  437. catch {
  438. console.log('Failed to list resources after change notification');
  439. }
  440. // Re-display the prompt
  441. process.stdout.write('> ');
  442. });
  443. // Connect the client
  444. await client.connect(transport);
  445. sessionId = transport.sessionId;
  446. console.log('Transport created with session ID:', sessionId);
  447. console.log('Connected to MCP server');
  448. }
  449. catch (error) {
  450. console.error('Failed to connect:', error);
  451. client = null;
  452. transport = null;
  453. }
  454. }
  455. async function disconnect() {
  456. if (!client || !transport) {
  457. console.log('Not connected.');
  458. return;
  459. }
  460. try {
  461. await transport.close();
  462. console.log('Disconnected from MCP server');
  463. client = null;
  464. transport = null;
  465. }
  466. catch (error) {
  467. console.error('Error disconnecting:', error);
  468. }
  469. }
  470. async function terminateSession() {
  471. if (!client || !transport) {
  472. console.log('Not connected.');
  473. return;
  474. }
  475. try {
  476. console.log('Terminating session with ID:', transport.sessionId);
  477. await transport.terminateSession();
  478. console.log('Session terminated successfully');
  479. // Check if sessionId was cleared after termination
  480. if (!transport.sessionId) {
  481. console.log('Session ID has been cleared');
  482. sessionId = undefined;
  483. // Also close the transport and clear client objects
  484. await transport.close();
  485. console.log('Transport closed after session termination');
  486. client = null;
  487. transport = null;
  488. }
  489. else {
  490. console.log('Server responded with 405 Method Not Allowed (session termination not supported)');
  491. console.log('Session ID is still active:', transport.sessionId);
  492. }
  493. }
  494. catch (error) {
  495. console.error('Error terminating session:', error);
  496. }
  497. }
  498. async function reconnect() {
  499. if (client) {
  500. await disconnect();
  501. }
  502. await connect();
  503. }
  504. async function listTools() {
  505. if (!client) {
  506. console.log('Not connected to server.');
  507. return;
  508. }
  509. try {
  510. const toolsRequest = {
  511. method: 'tools/list',
  512. params: {}
  513. };
  514. const toolsResult = await client.request(toolsRequest, ListToolsResultSchema);
  515. console.log('Available tools:');
  516. if (toolsResult.tools.length === 0) {
  517. console.log(' No tools available');
  518. }
  519. else {
  520. for (const tool of toolsResult.tools) {
  521. console.log(` - id: ${tool.name}, name: ${getDisplayName(tool)}, description: ${tool.description}`);
  522. }
  523. }
  524. }
  525. catch (error) {
  526. console.log(`Tools not supported by this server (${error})`);
  527. }
  528. }
  529. async function callTool(name, args) {
  530. if (!client) {
  531. console.log('Not connected to server.');
  532. return;
  533. }
  534. try {
  535. const request = {
  536. method: 'tools/call',
  537. params: {
  538. name,
  539. arguments: args
  540. }
  541. };
  542. console.log(`Calling tool '${name}' with args:`, args);
  543. const result = await client.request(request, CallToolResultSchema);
  544. console.log('Tool result:');
  545. const resourceLinks = [];
  546. result.content.forEach(item => {
  547. if (item.type === 'text') {
  548. console.log(` ${item.text}`);
  549. }
  550. else if (item.type === 'resource_link') {
  551. const resourceLink = item;
  552. resourceLinks.push(resourceLink);
  553. console.log(` 📁 Resource Link: ${resourceLink.name}`);
  554. console.log(` URI: ${resourceLink.uri}`);
  555. if (resourceLink.mimeType) {
  556. console.log(` Type: ${resourceLink.mimeType}`);
  557. }
  558. if (resourceLink.description) {
  559. console.log(` Description: ${resourceLink.description}`);
  560. }
  561. }
  562. else if (item.type === 'resource') {
  563. console.log(` [Embedded Resource: ${item.resource.uri}]`);
  564. }
  565. else if (item.type === 'image') {
  566. console.log(` [Image: ${item.mimeType}]`);
  567. }
  568. else if (item.type === 'audio') {
  569. console.log(` [Audio: ${item.mimeType}]`);
  570. }
  571. else {
  572. console.log(` [Unknown content type]:`, item);
  573. }
  574. });
  575. // Offer to read resource links
  576. if (resourceLinks.length > 0) {
  577. console.log(`\nFound ${resourceLinks.length} resource link(s). Use 'read-resource <uri>' to read their content.`);
  578. }
  579. }
  580. catch (error) {
  581. console.log(`Error calling tool ${name}: ${error}`);
  582. }
  583. }
  584. async function callGreetTool(name) {
  585. await callTool('greet', { name });
  586. }
  587. async function callMultiGreetTool(name) {
  588. console.log('Calling multi-greet tool with notifications...');
  589. await callTool('multi-greet', { name });
  590. }
  591. async function callCollectInfoTool(infoType) {
  592. console.log(`Testing form elicitation with collect-user-info tool (${infoType})...`);
  593. await callTool('collect-user-info', { infoType });
  594. }
  595. async function callCollectInfoWithTask(infoType) {
  596. console.log(`\n🔄 Testing bidirectional task support with collect-user-info-task tool (${infoType})...`);
  597. console.log('This will create a task on the server, which will elicit input and create a task on the client.\n');
  598. await callToolTask('collect-user-info-task', { infoType });
  599. }
  600. async function startNotifications(interval, count) {
  601. console.log(`Starting notification stream: interval=${interval}ms, count=${count || 'unlimited'}`);
  602. await callTool('start-notification-stream', { interval, count });
  603. }
  604. async function runNotificationsToolWithResumability(interval, count) {
  605. if (!client) {
  606. console.log('Not connected to server.');
  607. return;
  608. }
  609. try {
  610. console.log(`Starting notification stream with resumability: interval=${interval}ms, count=${count || 'unlimited'}`);
  611. console.log(`Using resumption token: ${notificationsToolLastEventId || 'none'}`);
  612. const request = {
  613. method: 'tools/call',
  614. params: {
  615. name: 'start-notification-stream',
  616. arguments: { interval, count }
  617. }
  618. };
  619. const onLastEventIdUpdate = (event) => {
  620. notificationsToolLastEventId = event;
  621. console.log(`Updated resumption token: ${event}`);
  622. };
  623. const result = await client.request(request, CallToolResultSchema, {
  624. resumptionToken: notificationsToolLastEventId,
  625. onresumptiontoken: onLastEventIdUpdate
  626. });
  627. console.log('Tool result:');
  628. result.content.forEach(item => {
  629. if (item.type === 'text') {
  630. console.log(` ${item.text}`);
  631. }
  632. else {
  633. console.log(` ${item.type} content:`, item);
  634. }
  635. });
  636. }
  637. catch (error) {
  638. console.log(`Error starting notification stream: ${error}`);
  639. }
  640. }
  641. async function listPrompts() {
  642. if (!client) {
  643. console.log('Not connected to server.');
  644. return;
  645. }
  646. try {
  647. const promptsRequest = {
  648. method: 'prompts/list',
  649. params: {}
  650. };
  651. const promptsResult = await client.request(promptsRequest, ListPromptsResultSchema);
  652. console.log('Available prompts:');
  653. if (promptsResult.prompts.length === 0) {
  654. console.log(' No prompts available');
  655. }
  656. else {
  657. for (const prompt of promptsResult.prompts) {
  658. console.log(` - id: ${prompt.name}, name: ${getDisplayName(prompt)}, description: ${prompt.description}`);
  659. }
  660. }
  661. }
  662. catch (error) {
  663. console.log(`Prompts not supported by this server (${error})`);
  664. }
  665. }
  666. async function getPrompt(name, args) {
  667. if (!client) {
  668. console.log('Not connected to server.');
  669. return;
  670. }
  671. try {
  672. const promptRequest = {
  673. method: 'prompts/get',
  674. params: {
  675. name,
  676. arguments: args
  677. }
  678. };
  679. const promptResult = await client.request(promptRequest, GetPromptResultSchema);
  680. console.log('Prompt template:');
  681. promptResult.messages.forEach((msg, index) => {
  682. console.log(` [${index + 1}] ${msg.role}: ${msg.content.type === 'text' ? msg.content.text : JSON.stringify(msg.content)}`);
  683. });
  684. }
  685. catch (error) {
  686. console.log(`Error getting prompt ${name}: ${error}`);
  687. }
  688. }
  689. async function listResources() {
  690. if (!client) {
  691. console.log('Not connected to server.');
  692. return;
  693. }
  694. try {
  695. const resourcesRequest = {
  696. method: 'resources/list',
  697. params: {}
  698. };
  699. const resourcesResult = await client.request(resourcesRequest, ListResourcesResultSchema);
  700. console.log('Available resources:');
  701. if (resourcesResult.resources.length === 0) {
  702. console.log(' No resources available');
  703. }
  704. else {
  705. for (const resource of resourcesResult.resources) {
  706. console.log(` - id: ${resource.name}, name: ${getDisplayName(resource)}, description: ${resource.uri}`);
  707. }
  708. }
  709. }
  710. catch (error) {
  711. console.log(`Resources not supported by this server (${error})`);
  712. }
  713. }
  714. async function readResource(uri) {
  715. if (!client) {
  716. console.log('Not connected to server.');
  717. return;
  718. }
  719. try {
  720. const request = {
  721. method: 'resources/read',
  722. params: { uri }
  723. };
  724. console.log(`Reading resource: ${uri}`);
  725. const result = await client.request(request, ReadResourceResultSchema);
  726. console.log('Resource contents:');
  727. for (const content of result.contents) {
  728. console.log(` URI: ${content.uri}`);
  729. if (content.mimeType) {
  730. console.log(` Type: ${content.mimeType}`);
  731. }
  732. if ('text' in content && typeof content.text === 'string') {
  733. console.log(' Content:');
  734. console.log(' ---');
  735. console.log(content.text
  736. .split('\n')
  737. .map((line) => ' ' + line)
  738. .join('\n'));
  739. console.log(' ---');
  740. }
  741. else if ('blob' in content && typeof content.blob === 'string') {
  742. console.log(` [Binary data: ${content.blob.length} bytes]`);
  743. }
  744. }
  745. }
  746. catch (error) {
  747. console.log(`Error reading resource ${uri}: ${error}`);
  748. }
  749. }
  750. async function callToolTask(name, args) {
  751. if (!client) {
  752. console.log('Not connected to server.');
  753. return;
  754. }
  755. console.log(`Calling tool '${name}' with task-based execution...`);
  756. console.log('Arguments:', args);
  757. // Use task-based execution - call now, fetch later
  758. // Using the experimental tasks API - WARNING: may change without notice
  759. console.log('This will return immediately while processing continues in the background...');
  760. try {
  761. // Call the tool with task metadata using streaming API
  762. const stream = client.experimental.tasks.callToolStream({
  763. name,
  764. arguments: args
  765. }, CallToolResultSchema, {
  766. task: {
  767. ttl: 60000 // Keep results for 60 seconds
  768. }
  769. });
  770. console.log('Waiting for task completion...');
  771. let lastStatus = '';
  772. for await (const message of stream) {
  773. switch (message.type) {
  774. case 'taskCreated':
  775. console.log('Task created successfully with ID:', message.task.taskId);
  776. break;
  777. case 'taskStatus':
  778. if (lastStatus !== message.task.status) {
  779. console.log(` ${message.task.status}${message.task.statusMessage ? ` - ${message.task.statusMessage}` : ''}`);
  780. }
  781. lastStatus = message.task.status;
  782. break;
  783. case 'result':
  784. console.log('Task completed!');
  785. console.log('Tool result:');
  786. message.result.content.forEach(item => {
  787. if (item.type === 'text') {
  788. console.log(` ${item.text}`);
  789. }
  790. });
  791. break;
  792. case 'error':
  793. throw message.error;
  794. }
  795. }
  796. }
  797. catch (error) {
  798. console.log(`Error with task-based execution: ${error}`);
  799. }
  800. }
  801. async function cleanup() {
  802. if (client && transport) {
  803. try {
  804. // First try to terminate the session gracefully
  805. if (transport.sessionId) {
  806. try {
  807. console.log('Terminating session before exit...');
  808. await transport.terminateSession();
  809. console.log('Session terminated successfully');
  810. }
  811. catch (error) {
  812. console.error('Error terminating session:', error);
  813. }
  814. }
  815. // Then close the transport
  816. await transport.close();
  817. }
  818. catch (error) {
  819. console.error('Error closing transport:', error);
  820. }
  821. }
  822. process.stdin.setRawMode(false);
  823. readline.close();
  824. console.log('\nGoodbye!');
  825. process.exit(0);
  826. }
  827. // Set up raw mode for keyboard input to capture Escape key
  828. process.stdin.setRawMode(true);
  829. process.stdin.on('data', async (data) => {
  830. // Check for Escape key (27)
  831. if (data.length === 1 && data[0] === 27) {
  832. console.log('\nESC key pressed. Disconnecting from server...');
  833. // Abort current operation and disconnect from server
  834. if (client && transport) {
  835. await disconnect();
  836. console.log('Disconnected. Press Enter to continue.');
  837. }
  838. else {
  839. console.log('Not connected to server.');
  840. }
  841. // Re-display the prompt
  842. process.stdout.write('> ');
  843. }
  844. });
  845. // Handle Ctrl+C
  846. process.on('SIGINT', async () => {
  847. console.log('\nReceived SIGINT. Cleaning up...');
  848. await cleanup();
  849. });
  850. // Start the interactive client
  851. main().catch((error) => {
  852. console.error('Error running MCP client:', error);
  853. process.exit(1);
  854. });
  855. //# sourceMappingURL=simpleStreamableHttp.js.map