simpleOAuthClient.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. #!/usr/bin/env node
  2. "use strict";
  3. Object.defineProperty(exports, "__esModule", { value: true });
  4. const node_http_1 = require("node:http");
  5. const node_readline_1 = require("node:readline");
  6. const node_url_1 = require("node:url");
  7. const index_js_1 = require("../../client/index.js");
  8. const streamableHttp_js_1 = require("../../client/streamableHttp.js");
  9. const types_js_1 = require("../../types.js");
  10. const auth_js_1 = require("../../client/auth.js");
  11. const simpleOAuthClientProvider_js_1 = require("./simpleOAuthClientProvider.js");
  12. // Configuration
  13. const DEFAULT_SERVER_URL = 'http://localhost:3000/mcp';
  14. const CALLBACK_PORT = 8090; // Use different port than auth server (3001)
  15. const CALLBACK_URL = `http://localhost:${CALLBACK_PORT}/callback`;
  16. /**
  17. * Interactive MCP client with OAuth authentication
  18. * Demonstrates the complete OAuth flow with browser-based authorization
  19. */
  20. class InteractiveOAuthClient {
  21. constructor(serverUrl, clientMetadataUrl) {
  22. this.serverUrl = serverUrl;
  23. this.clientMetadataUrl = clientMetadataUrl;
  24. this.client = null;
  25. this.rl = (0, node_readline_1.createInterface)({
  26. input: process.stdin,
  27. output: process.stdout
  28. });
  29. }
  30. /**
  31. * Prompts user for input via readline
  32. */
  33. async question(query) {
  34. return new Promise(resolve => {
  35. this.rl.question(query, resolve);
  36. });
  37. }
  38. /**
  39. * Example OAuth callback handler - in production, use a more robust approach
  40. * for handling callbacks and storing tokens
  41. */
  42. /**
  43. * Starts a temporary HTTP server to receive the OAuth callback
  44. */
  45. async waitForOAuthCallback() {
  46. return new Promise((resolve, reject) => {
  47. const server = (0, node_http_1.createServer)((req, res) => {
  48. // Ignore favicon requests
  49. if (req.url === '/favicon.ico') {
  50. res.writeHead(404);
  51. res.end();
  52. return;
  53. }
  54. console.log(`📥 Received callback: ${req.url}`);
  55. const parsedUrl = new node_url_1.URL(req.url || '', 'http://localhost');
  56. const code = parsedUrl.searchParams.get('code');
  57. const error = parsedUrl.searchParams.get('error');
  58. if (code) {
  59. console.log(`✅ Authorization code received: ${code?.substring(0, 10)}...`);
  60. res.writeHead(200, { 'Content-Type': 'text/html' });
  61. res.end(`
  62. <html>
  63. <body>
  64. <h1>Authorization Successful!</h1>
  65. <p>You can close this window and return to the terminal.</p>
  66. <script>setTimeout(() => window.close(), 2000);</script>
  67. </body>
  68. </html>
  69. `);
  70. resolve(code);
  71. setTimeout(() => server.close(), 3000);
  72. }
  73. else if (error) {
  74. console.log(`❌ Authorization error: ${error}`);
  75. res.writeHead(400, { 'Content-Type': 'text/html' });
  76. res.end(`
  77. <html>
  78. <body>
  79. <h1>Authorization Failed</h1>
  80. <p>Error: ${error}</p>
  81. </body>
  82. </html>
  83. `);
  84. reject(new Error(`OAuth authorization failed: ${error}`));
  85. }
  86. else {
  87. console.log(`❌ No authorization code or error in callback`);
  88. res.writeHead(400);
  89. res.end('Bad request');
  90. reject(new Error('No authorization code provided'));
  91. }
  92. });
  93. server.listen(CALLBACK_PORT, () => {
  94. console.log(`OAuth callback server started on http://localhost:${CALLBACK_PORT}`);
  95. });
  96. });
  97. }
  98. async attemptConnection(oauthProvider) {
  99. console.log('🚢 Creating transport with OAuth provider...');
  100. const baseUrl = new node_url_1.URL(this.serverUrl);
  101. const transport = new streamableHttp_js_1.StreamableHTTPClientTransport(baseUrl, {
  102. authProvider: oauthProvider
  103. });
  104. console.log('🚢 Transport created');
  105. try {
  106. console.log('🔌 Attempting connection (this will trigger OAuth redirect)...');
  107. await this.client.connect(transport);
  108. console.log('✅ Connected successfully');
  109. }
  110. catch (error) {
  111. if (error instanceof auth_js_1.UnauthorizedError) {
  112. console.log('🔐 OAuth required - waiting for authorization...');
  113. const callbackPromise = this.waitForOAuthCallback();
  114. const authCode = await callbackPromise;
  115. await transport.finishAuth(authCode);
  116. console.log('🔐 Authorization code received:', authCode);
  117. console.log('🔌 Reconnecting with authenticated transport...');
  118. await this.attemptConnection(oauthProvider);
  119. }
  120. else {
  121. console.error('❌ Connection failed with non-auth error:', error);
  122. throw error;
  123. }
  124. }
  125. }
  126. /**
  127. * Establishes connection to the MCP server with OAuth authentication
  128. */
  129. async connect() {
  130. console.log(`🔗 Attempting to connect to ${this.serverUrl}...`);
  131. const clientMetadata = {
  132. client_name: 'Simple OAuth MCP Client',
  133. redirect_uris: [CALLBACK_URL],
  134. grant_types: ['authorization_code', 'refresh_token'],
  135. response_types: ['code'],
  136. token_endpoint_auth_method: 'client_secret_post'
  137. };
  138. console.log('🔐 Creating OAuth provider...');
  139. const oauthProvider = new simpleOAuthClientProvider_js_1.InMemoryOAuthClientProvider(CALLBACK_URL, clientMetadata, (redirectUrl) => {
  140. console.log(`\n🔗 Please open this URL in your browser to authorize:\n ${redirectUrl.toString()}`);
  141. }, this.clientMetadataUrl);
  142. console.log('🔐 OAuth provider created');
  143. console.log('👤 Creating MCP client...');
  144. this.client = new index_js_1.Client({
  145. name: 'simple-oauth-client',
  146. version: '1.0.0'
  147. }, { capabilities: {} });
  148. console.log('👤 Client created');
  149. console.log('🔐 Starting OAuth flow...');
  150. await this.attemptConnection(oauthProvider);
  151. // Start interactive loop
  152. await this.interactiveLoop();
  153. }
  154. /**
  155. * Main interactive loop for user commands
  156. */
  157. async interactiveLoop() {
  158. console.log('\n🎯 Interactive MCP Client with OAuth');
  159. console.log('Commands:');
  160. console.log(' list - List available tools');
  161. console.log(' call <tool_name> [args] - Call a tool');
  162. console.log(' stream <tool_name> [args] - Call a tool with streaming (shows task status)');
  163. console.log(' quit - Exit the client');
  164. console.log();
  165. while (true) {
  166. try {
  167. const command = await this.question('mcp> ');
  168. if (!command.trim()) {
  169. continue;
  170. }
  171. if (command === 'quit') {
  172. console.log('\n👋 Goodbye!');
  173. this.close();
  174. process.exit(0);
  175. }
  176. else if (command === 'list') {
  177. await this.listTools();
  178. }
  179. else if (command.startsWith('call ')) {
  180. await this.handleCallTool(command);
  181. }
  182. else if (command.startsWith('stream ')) {
  183. await this.handleStreamTool(command);
  184. }
  185. else {
  186. console.log("❌ Unknown command. Try 'list', 'call <tool_name>', 'stream <tool_name>', or 'quit'");
  187. }
  188. }
  189. catch (error) {
  190. if (error instanceof Error && error.message === 'SIGINT') {
  191. console.log('\n\n👋 Goodbye!');
  192. break;
  193. }
  194. console.error('❌ Error:', error);
  195. }
  196. }
  197. }
  198. async listTools() {
  199. if (!this.client) {
  200. console.log('❌ Not connected to server');
  201. return;
  202. }
  203. try {
  204. const request = {
  205. method: 'tools/list',
  206. params: {}
  207. };
  208. const result = await this.client.request(request, types_js_1.ListToolsResultSchema);
  209. if (result.tools && result.tools.length > 0) {
  210. console.log('\n📋 Available tools:');
  211. result.tools.forEach((tool, index) => {
  212. console.log(`${index + 1}. ${tool.name}`);
  213. if (tool.description) {
  214. console.log(` Description: ${tool.description}`);
  215. }
  216. console.log();
  217. });
  218. }
  219. else {
  220. console.log('No tools available');
  221. }
  222. }
  223. catch (error) {
  224. console.error('❌ Failed to list tools:', error);
  225. }
  226. }
  227. async handleCallTool(command) {
  228. const parts = command.split(/\s+/);
  229. const toolName = parts[1];
  230. if (!toolName) {
  231. console.log('❌ Please specify a tool name');
  232. return;
  233. }
  234. // Parse arguments (simple JSON-like format)
  235. let toolArgs = {};
  236. if (parts.length > 2) {
  237. const argsString = parts.slice(2).join(' ');
  238. try {
  239. toolArgs = JSON.parse(argsString);
  240. }
  241. catch {
  242. console.log('❌ Invalid arguments format (expected JSON)');
  243. return;
  244. }
  245. }
  246. await this.callTool(toolName, toolArgs);
  247. }
  248. async callTool(toolName, toolArgs) {
  249. if (!this.client) {
  250. console.log('❌ Not connected to server');
  251. return;
  252. }
  253. try {
  254. const request = {
  255. method: 'tools/call',
  256. params: {
  257. name: toolName,
  258. arguments: toolArgs
  259. }
  260. };
  261. const result = await this.client.request(request, types_js_1.CallToolResultSchema);
  262. console.log(`\n🔧 Tool '${toolName}' result:`);
  263. if (result.content) {
  264. result.content.forEach(content => {
  265. if (content.type === 'text') {
  266. console.log(content.text);
  267. }
  268. else {
  269. console.log(content);
  270. }
  271. });
  272. }
  273. else {
  274. console.log(result);
  275. }
  276. }
  277. catch (error) {
  278. console.error(`❌ Failed to call tool '${toolName}':`, error);
  279. }
  280. }
  281. async handleStreamTool(command) {
  282. const parts = command.split(/\s+/);
  283. const toolName = parts[1];
  284. if (!toolName) {
  285. console.log('❌ Please specify a tool name');
  286. return;
  287. }
  288. // Parse arguments (simple JSON-like format)
  289. let toolArgs = {};
  290. if (parts.length > 2) {
  291. const argsString = parts.slice(2).join(' ');
  292. try {
  293. toolArgs = JSON.parse(argsString);
  294. }
  295. catch {
  296. console.log('❌ Invalid arguments format (expected JSON)');
  297. return;
  298. }
  299. }
  300. await this.streamTool(toolName, toolArgs);
  301. }
  302. async streamTool(toolName, toolArgs) {
  303. if (!this.client) {
  304. console.log('❌ Not connected to server');
  305. return;
  306. }
  307. try {
  308. // Using the experimental tasks API - WARNING: may change without notice
  309. console.log(`\n🔧 Streaming tool '${toolName}'...`);
  310. const stream = this.client.experimental.tasks.callToolStream({
  311. name: toolName,
  312. arguments: toolArgs
  313. }, types_js_1.CallToolResultSchema, {
  314. task: {
  315. taskId: `task-${Date.now()}`,
  316. ttl: 60000
  317. }
  318. });
  319. // Iterate through all messages yielded by the generator
  320. for await (const message of stream) {
  321. switch (message.type) {
  322. case 'taskCreated':
  323. console.log(`✓ Task created: ${message.task.taskId}`);
  324. break;
  325. case 'taskStatus':
  326. console.log(`⟳ Status: ${message.task.status}`);
  327. if (message.task.statusMessage) {
  328. console.log(` ${message.task.statusMessage}`);
  329. }
  330. break;
  331. case 'result':
  332. console.log('✓ Completed!');
  333. message.result.content.forEach(content => {
  334. if (content.type === 'text') {
  335. console.log(content.text);
  336. }
  337. else {
  338. console.log(content);
  339. }
  340. });
  341. break;
  342. case 'error':
  343. console.log('✗ Error:');
  344. console.log(` ${message.error.message}`);
  345. break;
  346. }
  347. }
  348. }
  349. catch (error) {
  350. console.error(`❌ Failed to stream tool '${toolName}':`, error);
  351. }
  352. }
  353. close() {
  354. this.rl.close();
  355. if (this.client) {
  356. // Note: Client doesn't have a close method in the current implementation
  357. // This would typically close the transport connection
  358. }
  359. }
  360. }
  361. /**
  362. * Main entry point
  363. */
  364. async function main() {
  365. const args = process.argv.slice(2);
  366. const serverUrl = args[0] || DEFAULT_SERVER_URL;
  367. const clientMetadataUrl = args[1];
  368. console.log('🚀 Simple MCP OAuth Client');
  369. console.log(`Connecting to: ${serverUrl}`);
  370. if (clientMetadataUrl) {
  371. console.log(`Client Metadata URL: ${clientMetadataUrl}`);
  372. }
  373. console.log();
  374. const client = new InteractiveOAuthClient(serverUrl, clientMetadataUrl);
  375. // Handle graceful shutdown
  376. process.on('SIGINT', () => {
  377. console.log('\n\n👋 Goodbye!');
  378. client.close();
  379. process.exit(0);
  380. });
  381. try {
  382. await client.connect();
  383. }
  384. catch (error) {
  385. console.error('Failed to start client:', error);
  386. process.exit(1);
  387. }
  388. finally {
  389. client.close();
  390. }
  391. }
  392. // Run if this file is executed directly
  393. main().catch(error => {
  394. console.error('Unhandled error:', error);
  395. process.exit(1);
  396. });
  397. //# sourceMappingURL=simpleOAuthClient.js.map