@meldocio/mcp-stdio-proxy 1.0.18 → 1.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,6 +5,25 @@
5
5
 
6
6
  This package allows you to connect Claude Desktop to your Meldoc account, so you can use all your documentation directly in Claude.
7
7
 
8
+ ## šŸš€ Quick Start - Install from Claude Marketplace
9
+
10
+ The easiest way to install Meldoc MCP is through the Claude Marketplace:
11
+
12
+ ```bash
13
+ # Add the marketplace
14
+ claude plugin marketplace add meldoc-io/mcp-stdio-proxy
15
+
16
+ # Install the plugin
17
+ claude plugin install meldoc-mcp@meldoc-mcp
18
+ ```
19
+
20
+ After installation:
21
+
22
+ 1. Restart Claude Desktop (or your MCP client)
23
+ 2. Run `npx @meldocio/mcp-stdio-proxy@latest auth login` to authenticate
24
+
25
+ Done! šŸŽ‰ Now you can ask Claude to work with your Meldoc documentation.
26
+
8
27
  ## What is this?
9
28
 
10
29
  This is a bridge between Claude Desktop and Meldoc. After setup, Claude will be able to:
@@ -20,20 +39,7 @@ This is a bridge between Claude Desktop and Meldoc. After setup, Claude will be
20
39
 
21
40
  ### Via Claude Marketplace (Recommended) šŸš€
22
41
 
23
- The easiest way to install Meldoc MCP is through the Claude Marketplace:
24
-
25
- ```bash
26
- # Add the marketplace
27
- claude plugin marketplace add meldoc/mcp-stdio-proxy
28
-
29
- # Install the plugin
30
- claude plugin install meldoc-mcp@meldoc
31
- ```
32
-
33
- After installation:
34
-
35
- 1. Restart Claude Desktop (or your MCP client)
36
- 2. Run `npx @meldocio/mcp-stdio-proxy@latest auth login` to authenticate
42
+ See [Quick Start](#-quick-start---install-from-claude-marketplace) section above for the easiest installation method.
37
43
 
38
44
  ### Via NPM
39
45
 
@@ -481,7 +487,7 @@ If you're experiencing connection errors:
481
487
  ### Setup
482
488
 
483
489
  ```bash
484
- git clone https://github.com/meldoc/mcp-stdio-proxy.git
490
+ git clone https://github.com/meldoc-io/mcp-stdio-proxy.git
485
491
  cd mcp-stdio-proxy
486
492
  npm install
487
493
  ```
@@ -545,7 +551,7 @@ MIT License - see [LICENSE](LICENSE) file for details.
545
551
 
546
552
  For issues, questions, or contributions, please visit:
547
553
 
548
- - GitHub Issues: <https://github.com/meldoc/mcp-stdio-proxy/issues>
554
+ - GitHub Issues: <https://github.com/meldoc-io/mcp-stdio-proxy/issues>
549
555
  - Documentation: <https://docs.meldoc.io>
550
556
 
551
557
  ## Related
package/bin/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { deviceFlowLogin } = require('../lib/device-flow');
3
+ const { interactiveLogin } = require('../lib/device-flow');
4
4
  const { readCredentials, deleteCredentials } = require('../lib/credentials');
5
5
  const { getAuthStatus } = require('../lib/auth');
6
6
  const { setWorkspaceAlias, getWorkspaceAlias } = require('../lib/config');
@@ -30,31 +30,15 @@ if (process.env.MELDOC_APP_URL) {
30
30
  */
31
31
  async function handleAuthLogin() {
32
32
  try {
33
- logger.section('šŸ” Authentication');
34
- await deviceFlowLogin(
35
- (url, code) => {
36
- console.log('\n' + logger.label('Visit this URL:'));
37
- console.log(' ' + logger.url(url));
38
- console.log('\n' + logger.label('Enter this code:'));
39
- console.log(' ' + logger.code(code) + '\n');
40
- logger.info('Waiting for authentication...');
41
- },
42
- (status) => {
43
- if (status === 'denied') {
44
- logger.error('Login denied by user');
45
- process.exit(1);
46
- } else if (status === 'expired') {
47
- logger.error('Authentication code expired');
48
- process.exit(1);
49
- }
50
- },
51
- API_URL,
52
- APP_URL
53
- );
54
- logger.success('Login successful!');
33
+ await interactiveLogin({
34
+ autoOpen: true,
35
+ showQR: false,
36
+ timeout: 120000,
37
+ apiBaseUrl: API_URL,
38
+ appUrl: APP_URL
39
+ });
55
40
  process.exit(0);
56
41
  } catch (error) {
57
- logger.error(`Login failed: ${error.message}`);
58
42
  process.exit(1);
59
43
  }
60
44
  }
@@ -66,15 +66,21 @@ const LOG_LEVELS = {
66
66
  // Import new auth and workspace modules
67
67
  const { getAccessToken, getAuthStatus } = require('../lib/auth');
68
68
  const { resolveWorkspaceAlias } = require('../lib/workspace');
69
- const { getApiUrl } = require('../lib/constants');
69
+ const { getApiUrl, getAppUrl } = require('../lib/constants');
70
70
  const { setWorkspaceAlias, getWorkspaceAlias } = require('../lib/config');
71
+ const { interactiveLogin, canOpenBrowser } = require('../lib/device-flow');
71
72
 
72
73
  // Configuration
73
74
  const apiUrl = getApiUrl();
75
+ const appUrl = getAppUrl();
74
76
  const rpcEndpoint = `${apiUrl}/mcp/v1/rpc`;
75
77
  const REQUEST_TIMEOUT = 25000; // 25 seconds (less than Claude Desktop's 30s timeout)
76
78
  const LOG_LEVEL = getLogLevel(process.env.LOG_LEVEL || 'ERROR');
77
79
 
80
+ // Track if we've attempted auto-authentication
81
+ let autoAuthAttempted = false;
82
+ let autoAuthInProgress = false;
83
+
78
84
  // Get log level from environment
79
85
  function getLogLevel(level) {
80
86
  const upper = (level || '').toUpperCase();
@@ -803,13 +809,74 @@ async function handleToolsCall(request) {
803
809
  }
804
810
  }
805
811
 
812
+ /**
813
+ * Attempt automatic authentication if conditions are met
814
+ * @returns {Promise<boolean>} True if authentication was attempted and succeeded
815
+ */
816
+ async function attemptAutoAuth() {
817
+ // Only attempt once per session
818
+ if (autoAuthAttempted || autoAuthInProgress) {
819
+ return false;
820
+ }
821
+
822
+ // Only in interactive mode (TTY) and not in CI
823
+ if (!canOpenBrowser()) {
824
+ return false;
825
+ }
826
+
827
+ // Check if NO_AUTO_AUTH is set
828
+ if (process.env.NO_AUTO_AUTH === '1' || process.env.NO_AUTO_AUTH === 'true') {
829
+ return false;
830
+ }
831
+
832
+ // Check if token already exists
833
+ const tokenInfo = await getAccessToken();
834
+ if (tokenInfo) {
835
+ return false;
836
+ }
837
+
838
+ autoAuthAttempted = true;
839
+ autoAuthInProgress = true;
840
+
841
+ try {
842
+ log(LOG_LEVELS.INFO, 'šŸ” First time setup - authentication required');
843
+ process.stderr.write('\n');
844
+
845
+ await interactiveLogin({
846
+ autoOpen: true,
847
+ showQR: false,
848
+ timeout: 120000,
849
+ apiBaseUrl: apiUrl,
850
+ appUrl: appUrl
851
+ });
852
+
853
+ autoAuthInProgress = false;
854
+ return true;
855
+ } catch (error) {
856
+ autoAuthInProgress = false;
857
+ log(LOG_LEVELS.WARN, `Auto-authentication failed: ${error.message}`);
858
+ log(LOG_LEVELS.INFO, 'You can authenticate manually: npx @meldocio/mcp-stdio-proxy@latest auth login');
859
+ return false;
860
+ }
861
+ }
862
+
806
863
  /**
807
864
  * Process a single JSON-RPC request
808
865
  * Forwards the request to the backend MCP API
809
866
  */
810
867
  async function processSingleRequest(request) {
811
868
  // Get access token with priority and auto-refresh
812
- const tokenInfo = await getAccessToken();
869
+ let tokenInfo = await getAccessToken();
870
+
871
+ // If no token and we haven't attempted auto-auth, try it
872
+ if (!tokenInfo && !autoAuthAttempted && !autoAuthInProgress) {
873
+ const authSucceeded = await attemptAutoAuth();
874
+ if (authSucceeded) {
875
+ // Retry getting token after successful auth
876
+ tokenInfo = await getAccessToken();
877
+ }
878
+ }
879
+
813
880
  if (!tokenInfo) {
814
881
  sendError(request.id, CUSTOM_ERROR_CODES.AUTH_REQUIRED,
815
882
  'Meldoc token not found. Set MELDOC_ACCESS_TOKEN environment variable or run: npx @meldocio/mcp-stdio-proxy@latest auth login', {
@@ -3,6 +3,8 @@ const https = require('https');
3
3
  const { writeCredentials } = require('./credentials');
4
4
  const { getApiUrl, getAppUrl } = require('./constants');
5
5
  const logger = require('./logger');
6
+ const os = require('os');
7
+ const { exec } = require('child_process');
6
8
 
7
9
  /**
8
10
  * Start device flow authentication
@@ -256,8 +258,321 @@ async function deviceFlowLogin(onCodeDisplay, onStatusChange = null, apiBaseUrl
256
258
  throw new Error('Device code expired');
257
259
  }
258
260
 
261
+ /**
262
+ * Check if browser can be opened automatically
263
+ * @returns {boolean} True if browser can be opened
264
+ */
265
+ function canOpenBrowser() {
266
+ return process.stdout.isTTY && !process.env.CI && !process.env.NO_BROWSER;
267
+ }
268
+
269
+ /**
270
+ * Open browser automatically
271
+ * @param {string} url - URL to open
272
+ * @returns {Promise<void>}
273
+ */
274
+ async function openBrowser(url) {
275
+ try {
276
+ // Try using 'open' package first (cross-platform)
277
+ const open = require('open');
278
+ await open(url);
279
+ } catch (error) {
280
+ // Fallback to platform-specific commands
281
+ try {
282
+ const platform = os.platform();
283
+ let command;
284
+
285
+ if (platform === 'darwin') {
286
+ command = `open "${url}"`;
287
+ } else if (platform === 'win32') {
288
+ command = `start "" "${url}"`;
289
+ } else {
290
+ command = `xdg-open "${url}"`;
291
+ }
292
+
293
+ exec(command, (err) => {
294
+ if (err) {
295
+ // Silent fail - browser opening is optional
296
+ }
297
+ });
298
+ } catch (fallbackError) {
299
+ // Silent fail - browser opening is optional
300
+ }
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Copy text to clipboard
306
+ * @param {string} text - Text to copy
307
+ * @returns {Promise<void>}
308
+ */
309
+ async function copyToClipboard(text) {
310
+ try {
311
+ const clipboardy = require('clipboardy');
312
+ await clipboardy.write(text);
313
+ } catch (error) {
314
+ // Silent fail - clipboard copy is optional
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Show QR code for mobile authentication
320
+ * @param {string} url - URL to encode in QR code
321
+ */
322
+ function showQRCode(url) {
323
+ try {
324
+ const qrcode = require('qrcode-terminal');
325
+ qrcode.generate(url, { small: true });
326
+ } catch (error) {
327
+ // Silent fail - QR code is optional
328
+ }
329
+ }
330
+
331
+ /**
332
+ * Sleep utility
333
+ * @param {number} ms - Milliseconds to sleep
334
+ * @returns {Promise<void>}
335
+ */
336
+ function sleep(ms) {
337
+ return new Promise(resolve => setTimeout(resolve, ms));
338
+ }
339
+
340
+ /**
341
+ * Poll for tokens with spinner and progress
342
+ * @param {string} deviceCode - Device code
343
+ * @param {number} interval - Polling interval in seconds
344
+ * @param {number} timeout - Timeout in milliseconds
345
+ * @param {string} apiBaseUrl - API base URL
346
+ * @returns {Promise<Object>} Credentials object
347
+ */
348
+ async function pollForTokens({ deviceCode, interval, timeout, apiBaseUrl }) {
349
+ const startTime = Date.now();
350
+ const spinner = ['ā ‹', 'ā ™', 'ā ¹', 'ā ø', 'ā ¼', 'ā “', 'ā ¦', 'ā §', 'ā ‡', 'ā '];
351
+ let spinnerIndex = 0;
352
+ let spinnerInterval = null;
353
+
354
+ // Start spinner animation if stderr is TTY
355
+ if (process.stderr.isTTY) {
356
+ spinnerInterval = setInterval(() => {
357
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
358
+ process.stderr.write(
359
+ `\r${spinner[spinnerIndex]} Waiting for authorization... (${elapsed}s)`
360
+ );
361
+ spinnerIndex = (spinnerIndex + 1) % spinner.length;
362
+ }, 100);
363
+ }
364
+
365
+ try {
366
+ while (true) {
367
+ // Check timeout
368
+ if (Date.now() - startTime > timeout) {
369
+ throw new Error('Authentication timeout. Please try again.');
370
+ }
371
+
372
+ try {
373
+ // Poll for status
374
+ const pollResponse = await pollDeviceFlow(deviceCode, apiBaseUrl);
375
+
376
+ if (pollResponse.status === 'approved') {
377
+ // Clear spinner
378
+ if (spinnerInterval) {
379
+ clearInterval(spinnerInterval);
380
+ process.stderr.write('\rāœ… Authorization confirmed! \n');
381
+ }
382
+
383
+ // Validate that we have required fields
384
+ if (!pollResponse.accessToken) {
385
+ throw new Error(`Invalid poll response: missing accessToken. Response: ${JSON.stringify(pollResponse)}`);
386
+ }
387
+
388
+ return pollResponse;
389
+ }
390
+
391
+ if (pollResponse.status === 'denied') {
392
+ throw new Error('Authorization denied by user');
393
+ }
394
+
395
+ if (pollResponse.status === 'expired') {
396
+ throw new Error('Device code expired');
397
+ }
398
+
399
+ // status === 'pending' - continue polling
400
+ await sleep(interval * 1000);
401
+
402
+ } catch (error) {
403
+ // If network error, retry
404
+ if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT' || error.message.includes('No response')) {
405
+ await sleep(interval * 1000);
406
+ continue;
407
+ }
408
+ throw error;
409
+ }
410
+ }
411
+ } finally {
412
+ if (spinnerInterval) {
413
+ clearInterval(spinnerInterval);
414
+ process.stderr.write('\r \r');
415
+ }
416
+ }
417
+ }
418
+
419
+ /**
420
+ * Interactive login with automatic browser opening and enhanced UX
421
+ * @param {Object} options - Login options
422
+ * @param {boolean} options.autoOpen - Automatically open browser (default: true)
423
+ * @param {boolean} options.showQR - Show QR code (default: false)
424
+ * @param {number} options.timeout - Timeout in milliseconds (default: 120000)
425
+ * @param {string} options.apiBaseUrl - API base URL
426
+ * @param {string} options.appUrl - App URL
427
+ * @returns {Promise<Object>} Credentials object
428
+ */
429
+ async function interactiveLogin(options = {}) {
430
+ const {
431
+ autoOpen = true,
432
+ showQR = false,
433
+ timeout = 120000,
434
+ apiBaseUrl = null,
435
+ appUrl = null
436
+ } = options;
437
+
438
+ const url = apiBaseUrl || getApiUrl();
439
+ const frontendUrl = appUrl || getAppUrl();
440
+
441
+ try {
442
+ // Step 1: Get device code from server
443
+ const startResponse = await startDeviceFlow(url);
444
+ const { deviceCode, userCode, verificationUrl, expiresIn, interval } = startResponse;
445
+
446
+ // Step 2: Build full URL
447
+ let displayUrl = verificationUrl;
448
+
449
+ // Check if verificationUrl already contains code in query parameter
450
+ let urlHasCode = false;
451
+ try {
452
+ const urlObj = new URL(verificationUrl);
453
+ if (urlObj.searchParams.has('code')) {
454
+ urlHasCode = true;
455
+ }
456
+ } catch (e) {
457
+ // URL parsing failed, continue
458
+ }
459
+
460
+ if (process.env.MELDOC_APP_URL || appUrl) {
461
+ try {
462
+ const urlObj = new URL(verificationUrl);
463
+ const appUrlObj = new URL(frontendUrl);
464
+ displayUrl = `${appUrlObj.origin}${urlObj.pathname}${urlObj.search}`;
465
+ } catch (e) {
466
+ displayUrl = verificationUrl;
467
+ }
468
+ }
469
+
470
+ // If URL doesn't have code, add it as path parameter
471
+ let fullUrl = displayUrl;
472
+ if (!urlHasCode) {
473
+ if (displayUrl.endsWith('/')) {
474
+ fullUrl = `${displayUrl}${userCode}`;
475
+ } else {
476
+ fullUrl = `${displayUrl}/${userCode}`;
477
+ }
478
+ }
479
+
480
+ // Step 3: Display authentication UI
481
+ process.stderr.write('\n');
482
+ process.stderr.write('╔═══════════════════════════════════════════════════════╗\n');
483
+ process.stderr.write('ā•‘ ā•‘\n');
484
+ process.stderr.write('ā•‘ šŸ” Meldoc Authentication Required ā•‘\n');
485
+ process.stderr.write('ā•‘ ā•‘\n');
486
+ process.stderr.write('ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•\n');
487
+ process.stderr.write('\n');
488
+
489
+ // Show QR code if requested
490
+ if (showQR) {
491
+ process.stderr.write('šŸ“± Scan QR code with your phone:\n\n');
492
+ showQRCode(fullUrl);
493
+ process.stderr.write('\n');
494
+ }
495
+
496
+ // Show URL and code
497
+ process.stderr.write(`🌐 Visit: ${fullUrl}\n`);
498
+ process.stderr.write(`šŸ“ Enter this code: ${userCode}\n\n`);
499
+ process.stderr.write('───────────────────────────────────────────────────────\n\n');
500
+
501
+ // Step 4: Automatically open browser if enabled
502
+ const shouldOpenBrowser = autoOpen && canOpenBrowser();
503
+ if (shouldOpenBrowser) {
504
+ process.stderr.write('šŸš€ Opening browser automatically...\n\n');
505
+ await openBrowser(fullUrl);
506
+ } else if (!canOpenBrowser()) {
507
+ process.stderr.write('āš ļø Please open the link manually in your browser\n\n');
508
+ }
509
+
510
+ // Step 5: Copy code to clipboard
511
+ try {
512
+ await copyToClipboard(userCode);
513
+ process.stderr.write('āœ… Code copied to clipboard!\n\n');
514
+ } catch (error) {
515
+ // Silent fail
516
+ }
517
+
518
+ // Step 6: Poll for authorization with spinner
519
+ process.stderr.write('ā³ Waiting for authorization...\n\n');
520
+
521
+ const pollResponse = await pollForTokens({
522
+ deviceCode,
523
+ interval,
524
+ timeout,
525
+ apiBaseUrl: url
526
+ });
527
+
528
+ // Step 7: Save credentials
529
+ const credentials = {
530
+ type: 'user_session',
531
+ apiBaseUrl: url,
532
+ user: pollResponse.user,
533
+ tokens: {
534
+ accessToken: pollResponse.accessToken,
535
+ accessExpiresAt: pollResponse.expiresAt || new Date(Date.now() + 3600000).toISOString(),
536
+ refreshToken: pollResponse.refreshToken || null
537
+ },
538
+ updatedAt: new Date().toISOString()
539
+ };
540
+
541
+ writeCredentials(credentials);
542
+
543
+ process.stderr.write('\nāœ… Successfully authenticated!\n\n');
544
+
545
+ return credentials;
546
+
547
+ } catch (error) {
548
+ // Handle specific errors with helpful messages
549
+ if (error.code === 'ECONNREFUSED' || error.message.includes('No response')) {
550
+ process.stderr.write('\nāŒ Cannot connect to Meldoc API\n');
551
+ process.stderr.write(' Please check your internet connection\n\n');
552
+ } else if (error.message.includes('timeout')) {
553
+ process.stderr.write('\nā±ļø Authentication timed out\n');
554
+ process.stderr.write(' Please try again or authenticate manually:\n');
555
+ process.stderr.write(' npx @meldocio/mcp-stdio-proxy@latest auth login\n\n');
556
+ } else if (error.message.includes('denied')) {
557
+ process.stderr.write('\n🚫 Authentication was denied\n');
558
+ process.stderr.write(' Please try again if this was a mistake\n\n');
559
+ } else {
560
+ process.stderr.write(`\nāŒ Authentication failed: ${error.message}\n\n`);
561
+ process.stderr.write(' Manual authentication:\n');
562
+ process.stderr.write(' npx @meldocio/mcp-stdio-proxy@latest auth login\n\n');
563
+ }
564
+
565
+ throw error;
566
+ }
567
+ }
568
+
259
569
  module.exports = {
260
570
  startDeviceFlow,
261
571
  pollDeviceFlow,
262
- deviceFlowLogin
572
+ deviceFlowLogin,
573
+ interactiveLogin,
574
+ openBrowser,
575
+ copyToClipboard,
576
+ showQRCode,
577
+ canOpenBrowser
263
578
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meldocio/mcp-stdio-proxy",
3
- "version": "1.0.18",
3
+ "version": "1.0.20",
4
4
  "description": "MCP stdio proxy for meldoc - connects Claude Desktop to meldoc MCP API",
5
5
  "bin": {
6
6
  "meldoc-mcp": "bin/meldoc-mcp-proxy.js"