@meldocio/mcp-stdio-proxy 1.0.19 → 1.0.21
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/.claude-plugin/SKILL.md +87 -0
- package/.claude-plugin/marketplace.json +39 -0
- package/.claude-plugin/plugin.json +10 -0
- package/.mcp.json +6 -0
- package/README.md +26 -20
- package/bin/cli.js +8 -24
- package/bin/meldoc-mcp-proxy.js +69 -2
- package/lib/device-flow.js +316 -1
- package/package.json +5 -2
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Meldoc MCP Integration
|
|
2
|
+
|
|
3
|
+
Connect to your Meldoc documentation directly from Claude Desktop, Claude Code, and other MCP clients.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Meldoc MCP provides seamless access to your Meldoc documentation workspace through the Model Context Protocol. Once configured, you can interact with your documentation naturally through AI conversations.
|
|
8
|
+
|
|
9
|
+
## Available Tools
|
|
10
|
+
|
|
11
|
+
### Document Operations
|
|
12
|
+
|
|
13
|
+
- **`docs_list`** - List all documents in a workspace or project
|
|
14
|
+
- **`docs_get`** - Get the complete content of a specific document
|
|
15
|
+
- **`docs_tree`** - Display the hierarchical structure of documents in a project
|
|
16
|
+
- **`docs_search`** - Search through all documents using full-text search
|
|
17
|
+
- **`docs_create`** - Create a new document (requires write permissions)
|
|
18
|
+
- **`docs_update`** - Update an existing document's content or metadata (requires write permissions)
|
|
19
|
+
- **`docs_delete`** - Delete a document (requires write permissions)
|
|
20
|
+
- **`docs_links`** - Show all outgoing links from a document
|
|
21
|
+
- **`docs_backlinks`** - Find all documents that link to a specific document
|
|
22
|
+
|
|
23
|
+
### Project Operations
|
|
24
|
+
|
|
25
|
+
- **`projects_list`** - List all projects available in your workspace
|
|
26
|
+
|
|
27
|
+
### Management Operations
|
|
28
|
+
|
|
29
|
+
- **`server_info`** - Get information about your account and access permissions
|
|
30
|
+
- **`list_workspaces`** - Show all workspaces you have access to
|
|
31
|
+
- **`set_workspace`** - Set the default workspace for operations
|
|
32
|
+
- **`get_workspace`** - Get information about the currently active workspace
|
|
33
|
+
- **`auth_status`** - Check your current authentication status
|
|
34
|
+
|
|
35
|
+
## Usage Examples
|
|
36
|
+
|
|
37
|
+
Simply ask Claude naturally! For example:
|
|
38
|
+
|
|
39
|
+
- "Show me all documents in the API project"
|
|
40
|
+
- "Find information about authentication"
|
|
41
|
+
- "Search for documents about error handling"
|
|
42
|
+
- "Create a new document about our deployment process"
|
|
43
|
+
- "Which documents link to the database schema?"
|
|
44
|
+
- "Show me the document tree for the frontend project"
|
|
45
|
+
- "Update the getting started guide with new information"
|
|
46
|
+
|
|
47
|
+
Your AI assistant will automatically:
|
|
48
|
+
|
|
49
|
+
- Select the appropriate tool
|
|
50
|
+
- Handle authentication
|
|
51
|
+
- Format the results nicely
|
|
52
|
+
- Provide context and explanations
|
|
53
|
+
|
|
54
|
+
## Authentication
|
|
55
|
+
|
|
56
|
+
Before using Meldoc MCP, you need to authenticate:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npx @meldocio/mcp-stdio-proxy@latest auth login
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
This will open a browser flow for secure authentication. Your credentials are stored locally and automatically refreshed.
|
|
63
|
+
|
|
64
|
+
## Workspace Management
|
|
65
|
+
|
|
66
|
+
If you have multiple workspaces, you can:
|
|
67
|
+
|
|
68
|
+
1. List all workspaces: `npx @meldocio/mcp-stdio-proxy@latest config list-workspaces`
|
|
69
|
+
2. Set default workspace: `npx @meldocio/mcp-stdio-proxy@latest config set-workspace <name>`
|
|
70
|
+
3. Or specify workspace in requests directly
|
|
71
|
+
|
|
72
|
+
## Permissions
|
|
73
|
+
|
|
74
|
+
Some operations require write permissions to your workspace:
|
|
75
|
+
|
|
76
|
+
- Creating documents
|
|
77
|
+
- Updating documents
|
|
78
|
+
- Deleting documents
|
|
79
|
+
|
|
80
|
+
Read-only operations (list, get, search) work with any authenticated account.
|
|
81
|
+
|
|
82
|
+
## Related Documentation
|
|
83
|
+
|
|
84
|
+
- [Getting Started Guide](docs/getting-started.meldoc.md)
|
|
85
|
+
- [Authentication Guide](docs/authentication.meldoc.md)
|
|
86
|
+
- [MCP Tools Reference](docs/mcp-tools.meldoc.md)
|
|
87
|
+
- [Full Documentation](https://docs.meldoc.io/integrations/mcp)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "meldoc-mcp",
|
|
3
|
+
"owner": {
|
|
4
|
+
"name": "Meldoc",
|
|
5
|
+
"email": "support@meldoc.io"
|
|
6
|
+
},
|
|
7
|
+
"metadata": {
|
|
8
|
+
"description": "Official Meldoc plugin marketplace for Claude Code MCP integrations"
|
|
9
|
+
},
|
|
10
|
+
"plugins": [
|
|
11
|
+
{
|
|
12
|
+
"name": "meldoc-mcp",
|
|
13
|
+
"source": {
|
|
14
|
+
"source": "github",
|
|
15
|
+
"repo": "meldoc-io/mcp-stdio-proxy"
|
|
16
|
+
},
|
|
17
|
+
"version": "1.0.21",
|
|
18
|
+
"description": "Connect Claude Desktop, Claude Code, and other MCP clients to your Meldoc documentation workspace. Read, search, create, and update your documentation directly from AI conversations.",
|
|
19
|
+
"author": {
|
|
20
|
+
"name": "Meldoc",
|
|
21
|
+
"email": "support@meldoc.io"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/meldoc-io/mcp-stdio-proxy#readme",
|
|
24
|
+
"repository": "https://github.com/meldoc-io/mcp-stdio-proxy",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"category": "productivity",
|
|
27
|
+
"keywords": [
|
|
28
|
+
"mcp",
|
|
29
|
+
"meldoc",
|
|
30
|
+
"documentation",
|
|
31
|
+
"claude",
|
|
32
|
+
"claude-code",
|
|
33
|
+
"ai",
|
|
34
|
+
"productivity",
|
|
35
|
+
"knowledge-base"
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "meldoc-mcp",
|
|
3
|
+
"version": "1.0.21",
|
|
4
|
+
"description": "Connect Claude Desktop, Claude Code, and other MCP clients to your Meldoc documentation workspace. Read, search, create, and update your documentation directly from AI conversations through the Model Context Protocol.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Meldoc",
|
|
7
|
+
"email": "support@meldoc.io"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/meldoc-io/mcp-stdio-proxy"
|
|
10
|
+
}
|
package/.mcp.json
ADDED
package/README.md
CHANGED
|
@@ -1,39 +1,45 @@
|
|
|
1
|
-
# Meldoc MCP for Claude Desktop
|
|
1
|
+
# Meldoc MCP for Claude Desktop & Claude Code
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@meldocio/mcp-stdio-proxy)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
|
|
6
|
-
This package allows you to connect Claude Desktop to your Meldoc account, so you can use all your documentation directly in Claude.
|
|
6
|
+
This package allows you to connect Claude Desktop and Claude Code to your Meldoc account, so you can use all your documentation directly in Claude.
|
|
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 Claude Code
|
|
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.
|
|
7
26
|
|
|
8
27
|
## What is this?
|
|
9
28
|
|
|
10
|
-
This is a bridge between Claude Desktop and Meldoc. After setup, Claude will be able to:
|
|
29
|
+
This is a bridge between Claude (Desktop & Code) and Meldoc. After setup, Claude will be able to:
|
|
11
30
|
|
|
12
31
|
- 📖 Read your documentation from Meldoc
|
|
13
32
|
- 🔍 Search through documents
|
|
14
33
|
- ✏️ Create and update documents (if you have permissions)
|
|
15
34
|
- 📁 Work with projects and workspaces
|
|
16
35
|
|
|
17
|
-
**No additional installation required** - everything works automatically through Claude Desktop.
|
|
36
|
+
**No additional installation required** - everything works automatically through Claude Desktop and Claude Code.
|
|
18
37
|
|
|
19
38
|
## Installation
|
|
20
39
|
|
|
21
40
|
### Via Claude Marketplace (Recommended) 🚀
|
|
22
41
|
|
|
23
|
-
|
|
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 {
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
}
|
package/bin/meldoc-mcp-proxy.js
CHANGED
|
@@ -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
|
-
|
|
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', {
|
package/lib/device-flow.js
CHANGED
|
@@ -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,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meldocio/mcp-stdio-proxy",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "MCP stdio proxy for meldoc - connects Claude Desktop to meldoc MCP API",
|
|
3
|
+
"version": "1.0.21",
|
|
4
|
+
"description": "MCP stdio proxy for meldoc - connects Claude Desktop and Claude Code to meldoc MCP API",
|
|
5
5
|
"bin": {
|
|
6
6
|
"meldoc-mcp": "bin/meldoc-mcp-proxy.js"
|
|
7
7
|
},
|
|
8
8
|
"files": [
|
|
9
9
|
"bin/**",
|
|
10
10
|
"lib/**",
|
|
11
|
+
".claude-plugin/**",
|
|
12
|
+
".mcp.json",
|
|
11
13
|
"README.md",
|
|
12
14
|
"LICENSE"
|
|
13
15
|
],
|
|
@@ -18,6 +20,7 @@
|
|
|
18
20
|
"mcp",
|
|
19
21
|
"meldoc",
|
|
20
22
|
"claude",
|
|
23
|
+
"claude-code",
|
|
21
24
|
"stdio",
|
|
22
25
|
"proxy",
|
|
23
26
|
"json-rpc"
|