@meldocio/mcp-stdio-proxy 1.0.29 → 1.1.0
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/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -3
- package/bin/cli.js +2 -1
- package/bin/meldoc-mcp-proxy.js +178 -134
- package/lib/cli/commands.js +19 -8
- package/lib/core/auth.js +35 -16
- package/lib/core/oauth-pkce.js +279 -0
- package/lib/http/proxy.js +230 -0
- package/lib/mcp/handlers.js +16 -165
- package/lib/mcp/tools-call.js +16 -18
- package/lib/protocol/tools-schema.js +15 -173
- package/package.json +1 -1
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"source": "github",
|
|
15
15
|
"repo": "meldoc-io/mcp-stdio-proxy"
|
|
16
16
|
},
|
|
17
|
-
"version": "1.0
|
|
17
|
+
"version": "1.1.0",
|
|
18
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
19
|
"author": {
|
|
20
20
|
"name": "Meldoc",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "meldoc-mcp",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
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
5
|
"author": {
|
|
6
6
|
"name": "Meldoc",
|
package/README.md
CHANGED
|
@@ -377,14 +377,14 @@ After setup, Claude gets access to the following capabilities:
|
|
|
377
377
|
### 📖 Working with documents
|
|
378
378
|
|
|
379
379
|
- **`docs_list`** - Show list of all documents in the project
|
|
380
|
-
- **`docs_get`** - Get content of a
|
|
380
|
+
- **`docs_get`** - Get content of a document (by UUID or alias)
|
|
381
381
|
- **`docs_tree`** - Show document structure (tree)
|
|
382
382
|
- **`docs_search`** - Find documents by text
|
|
383
383
|
- **`docs_create`** - Create a new document (requires permissions)
|
|
384
384
|
- **`docs_update`** - Update a document (requires permissions)
|
|
385
385
|
- **`docs_delete`** - Delete a document (requires permissions)
|
|
386
|
-
- **`docs_links`** - Show all links from a document
|
|
387
|
-
- **`docs_backlinks`** - Show all documents that link to this one
|
|
386
|
+
- **`docs_links`** - Show all links from a document (by UUID or alias)
|
|
387
|
+
- **`docs_backlinks`** - Show all documents that link to this one (by UUID or alias)
|
|
388
388
|
|
|
389
389
|
### 📁 Working with projects
|
|
390
390
|
|
package/bin/cli.js
CHANGED
|
@@ -67,7 +67,8 @@ async function main() {
|
|
|
67
67
|
handleUninstall();
|
|
68
68
|
} else if (command === 'auth') {
|
|
69
69
|
if (subcommand === 'login') {
|
|
70
|
-
|
|
70
|
+
const usePkce = args.includes('--pkce');
|
|
71
|
+
await handleAuthLogin({ pkce: usePkce });
|
|
71
72
|
} else if (subcommand === 'status') {
|
|
72
73
|
await handleAuthStatus();
|
|
73
74
|
} else if (subcommand === 'logout') {
|
package/bin/meldoc-mcp-proxy.js
CHANGED
|
@@ -3,208 +3,252 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Meldoc MCP Stdio Proxy
|
|
5
5
|
*
|
|
6
|
-
* Thin
|
|
7
|
-
*
|
|
6
|
+
* Thin stdio ↔ Streamable HTTP proxy implementing MCP transport (spec 2025-03-26).
|
|
7
|
+
* Forwards all MCP protocol messages to the server-side MCP at /mcp.
|
|
8
|
+
* Handles a small set of local tools (auth_status, auth_login_instructions,
|
|
9
|
+
* set_workspace, get_workspace) directly.
|
|
8
10
|
*/
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
const { sendError } = require('../lib/protocol/json-rpc');
|
|
12
|
-
const { JSON_RPC_ERROR_CODES } = require('../lib/protocol/error-codes');
|
|
13
|
-
const { handleLocalMethod } = require('../lib/mcp/handlers');
|
|
14
|
-
const { handleToolsCall } = require('../lib/mcp/tools-call');
|
|
15
|
-
const { makeBackendRequest } = require('../lib/http/client');
|
|
16
|
-
const { handleBackendResponse } = require('../lib/http/error-handler');
|
|
17
|
-
const { LOG_LEVELS } = require('../lib/core/constants');
|
|
18
|
-
|
|
19
|
-
// Check for CLI commands first
|
|
12
|
+
// Check for CLI commands first (auth, config, install, etc.)
|
|
20
13
|
const args = process.argv.slice(2);
|
|
21
14
|
if (args.length > 0) {
|
|
22
|
-
const
|
|
23
|
-
if (
|
|
24
|
-
|
|
15
|
+
const cmd = args[0];
|
|
16
|
+
if (cmd === 'auth' || cmd === 'config' || cmd === 'install' ||
|
|
17
|
+
cmd === 'uninstall' || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
25
18
|
require('./cli');
|
|
26
19
|
return;
|
|
27
20
|
}
|
|
28
21
|
}
|
|
29
22
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
23
|
+
const { StreamableHTTPProxy } = require('../lib/http/proxy');
|
|
24
|
+
const { handleToolsCall, isLocalTool } = require('../lib/mcp/tools-call');
|
|
25
|
+
const { handlePing, isNotification } = require('../lib/mcp/handlers');
|
|
26
|
+
const { getLocalToolsList } = require('../lib/protocol/tools-schema');
|
|
27
|
+
const { getAccessToken } = require('../lib/core/auth');
|
|
28
|
+
const { getApiUrl, LOG_LEVELS } = require('../lib/core/constants');
|
|
29
|
+
|
|
30
|
+
let pkg;
|
|
31
|
+
try { pkg = require('../package.json'); } catch (e) { pkg = { name: '@meldocio/mcp-stdio-proxy', version: '1.0.0' }; }
|
|
32
|
+
|
|
33
|
+
// Log level
|
|
34
|
+
const LOG_LEVEL = (() => {
|
|
34
35
|
const level = (process.env.LOG_LEVEL || 'ERROR').toUpperCase();
|
|
35
36
|
return LOG_LEVELS[level] !== undefined ? LOG_LEVELS[level] : LOG_LEVELS.ERROR;
|
|
36
|
-
}
|
|
37
|
+
})();
|
|
37
38
|
|
|
38
|
-
const LOG_LEVEL = getLogLevel();
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Log message to stderr
|
|
42
|
-
*/
|
|
43
39
|
function log(level, message) {
|
|
44
40
|
if (LOG_LEVEL >= level) {
|
|
45
|
-
|
|
46
|
-
process.stderr.write(`[${levelName}] ${message}\n`);
|
|
41
|
+
process.stderr.write(`[${Object.keys(LOG_LEVELS)[level] || 'UNKNOWN'}] ${message}\n`);
|
|
47
42
|
}
|
|
48
43
|
}
|
|
49
44
|
|
|
45
|
+
// Create the Streamable HTTP proxy
|
|
46
|
+
const proxy = new StreamableHTTPProxy(
|
|
47
|
+
getApiUrl() + '/mcp',
|
|
48
|
+
async () => {
|
|
49
|
+
const tokenInfo = await getAccessToken();
|
|
50
|
+
return tokenInfo ? tokenInfo.token : null;
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
|
|
50
54
|
/**
|
|
51
|
-
*
|
|
55
|
+
* Inject local tools into a tools/list response from the server.
|
|
56
|
+
* @param {string} responseText - Raw JSON-RPC response text from server
|
|
57
|
+
* @returns {string} Modified response with local tools prepended
|
|
52
58
|
*/
|
|
53
|
-
|
|
59
|
+
function injectLocalTools(responseText) {
|
|
54
60
|
try {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
});
|
|
64
|
-
} else {
|
|
65
|
-
sendError(request.id, JSON_RPC_ERROR_CODES.INTERNAL_ERROR,
|
|
66
|
-
`Failed to communicate with backend: ${error.message}`, {
|
|
67
|
-
code: 'BACKEND_ERROR'
|
|
68
|
-
});
|
|
61
|
+
const parsed = JSON.parse(responseText);
|
|
62
|
+
if (parsed.result && Array.isArray(parsed.result.tools)) {
|
|
63
|
+
const localTools = getLocalToolsList();
|
|
64
|
+
const serverToolNames = new Set(parsed.result.tools.map(t => t.name));
|
|
65
|
+
// Prepend local tools that aren't already in the server list
|
|
66
|
+
const toInject = localTools.filter(t => !serverToolNames.has(t.name));
|
|
67
|
+
parsed.result.tools = [...toInject, ...parsed.result.tools];
|
|
68
|
+
return JSON.stringify(parsed);
|
|
69
69
|
}
|
|
70
|
+
} catch (e) {
|
|
71
|
+
// If parsing fails, return original text unchanged
|
|
70
72
|
}
|
|
73
|
+
return responseText;
|
|
71
74
|
}
|
|
72
75
|
|
|
73
76
|
/**
|
|
74
|
-
* Handle a JSON-RPC request
|
|
77
|
+
* Handle a single parsed JSON-RPC request.
|
|
78
|
+
* @param {Object} request
|
|
75
79
|
*/
|
|
76
80
|
async function handleRequest(request) {
|
|
77
|
-
if (!request) return;
|
|
81
|
+
if (!request || typeof request !== 'object') return;
|
|
78
82
|
|
|
79
|
-
|
|
80
|
-
// Handle batch requests
|
|
81
|
-
if (Array.isArray(request)) {
|
|
82
|
-
for (const req of request) {
|
|
83
|
-
if (req) {
|
|
84
|
-
try {
|
|
85
|
-
await handleSingleRequest(req);
|
|
86
|
-
} catch (error) {
|
|
87
|
-
log(LOG_LEVELS.ERROR, `Error processing batch request: ${error.message || 'Unknown error'}`);
|
|
88
|
-
if (req && req.id !== undefined && req.id !== null) {
|
|
89
|
-
sendError(req.id, JSON_RPC_ERROR_CODES.INTERNAL_ERROR,
|
|
90
|
-
`Error processing request: ${error.message || 'Unknown error'}`, {
|
|
91
|
-
code: 'INTERNAL_ERROR'
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
83
|
+
const { method, id } = request;
|
|
99
84
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
code: 'INTERNAL_ERROR'
|
|
108
|
-
});
|
|
85
|
+
// Basic validation: method is required
|
|
86
|
+
if (!method) {
|
|
87
|
+
if (id != null) {
|
|
88
|
+
process.stdout.write(JSON.stringify({
|
|
89
|
+
jsonrpc: '2.0', id,
|
|
90
|
+
error: { code: -32600, message: 'Invalid Request: missing method' }
|
|
91
|
+
}) + '\n');
|
|
109
92
|
}
|
|
93
|
+
return;
|
|
110
94
|
}
|
|
111
|
-
}
|
|
112
95
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (!validation.valid) {
|
|
120
|
-
sendError(request.id, JSON_RPC_ERROR_CODES.INVALID_REQUEST, validation.error);
|
|
96
|
+
// Notifications require no response
|
|
97
|
+
if (isNotification(method)) return;
|
|
98
|
+
|
|
99
|
+
// ping is handled locally (fast keep-alive)
|
|
100
|
+
if (method === 'ping') {
|
|
101
|
+
handlePing(request);
|
|
121
102
|
return;
|
|
122
103
|
}
|
|
123
104
|
|
|
124
|
-
|
|
105
|
+
// tools/call: check if it's a local tool first
|
|
106
|
+
if (method === 'tools/call') {
|
|
107
|
+
const toolName = request.params && request.params.name;
|
|
108
|
+
if (isLocalTool(toolName)) {
|
|
109
|
+
await handleToolsCall(request);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
125
113
|
|
|
126
|
-
//
|
|
127
|
-
if
|
|
128
|
-
|
|
114
|
+
// initialize: handled locally so Claude Desktop gets a fast response
|
|
115
|
+
// even if the server is temporarily unavailable.
|
|
116
|
+
// The server session is initialized lazily on the first forwarded request.
|
|
117
|
+
if (method === 'initialize') {
|
|
118
|
+
process.stdout.write(JSON.stringify({
|
|
119
|
+
jsonrpc: '2.0',
|
|
120
|
+
id,
|
|
121
|
+
result: {
|
|
122
|
+
protocolVersion: '2025-06-18',
|
|
123
|
+
capabilities: { tools: {} },
|
|
124
|
+
serverInfo: { name: pkg.name, version: pkg.version }
|
|
125
|
+
}
|
|
126
|
+
}) + '\n');
|
|
127
|
+
return;
|
|
129
128
|
}
|
|
130
129
|
|
|
131
|
-
//
|
|
132
|
-
if (method === 'tools/
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
130
|
+
// tools/list: forward to server and inject local tools
|
|
131
|
+
if (method === 'tools/list') {
|
|
132
|
+
// Intercept stdout temporarily to inject local tools
|
|
133
|
+
const origWrite = process.stdout.write.bind(process.stdout);
|
|
134
|
+
let captured = '';
|
|
135
|
+
let done = false;
|
|
136
|
+
|
|
137
|
+
process.stdout.write = (chunk) => {
|
|
138
|
+
if (!done) {
|
|
139
|
+
captured += chunk.toString();
|
|
140
|
+
// Check if we have a complete line
|
|
141
|
+
const lines = captured.split('\n');
|
|
142
|
+
if (lines.length > 1) {
|
|
143
|
+
done = true;
|
|
144
|
+
process.stdout.write = origWrite;
|
|
145
|
+
const modified = injectLocalTools(lines[0]);
|
|
146
|
+
origWrite(modified + '\n');
|
|
147
|
+
// Write any remaining lines
|
|
148
|
+
for (let i = 1; i < lines.length - 1; i++) {
|
|
149
|
+
if (lines[i]) origWrite(lines[i] + '\n');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
origWrite(chunk);
|
|
154
|
+
}
|
|
155
|
+
return true;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
await proxy.forwardAndWrite(request);
|
|
160
|
+
} finally {
|
|
161
|
+
if (!done) {
|
|
162
|
+
process.stdout.write = origWrite;
|
|
163
|
+
if (captured.trim()) {
|
|
164
|
+
origWrite(injectLocalTools(captured.trim()) + '\n');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
136
167
|
}
|
|
137
|
-
|
|
168
|
+
return;
|
|
138
169
|
}
|
|
139
170
|
|
|
140
|
-
//
|
|
141
|
-
await
|
|
171
|
+
// Everything else: forward to server
|
|
172
|
+
await proxy.forwardAndWrite(request);
|
|
142
173
|
}
|
|
143
174
|
|
|
144
175
|
/**
|
|
145
|
-
* Handle a
|
|
176
|
+
* Handle a raw JSON line from stdin.
|
|
146
177
|
*/
|
|
147
|
-
function handleLine(line) {
|
|
148
|
-
if (!line
|
|
178
|
+
async function handleLine(line) {
|
|
179
|
+
if (!line.trim()) return;
|
|
149
180
|
|
|
181
|
+
let request;
|
|
150
182
|
try {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
183
|
+
request = JSON.parse(line);
|
|
184
|
+
} catch (e) {
|
|
185
|
+
log(LOG_LEVELS.ERROR, 'Parse error: ' + e.message);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
if (Array.isArray(request)) {
|
|
191
|
+
// Batch requests
|
|
192
|
+
for (const req of request) {
|
|
193
|
+
try {
|
|
194
|
+
await handleRequest(req);
|
|
195
|
+
} catch (err) {
|
|
196
|
+
log(LOG_LEVELS.ERROR, 'Batch request error: ' + err.message);
|
|
197
|
+
if (req && req.id != null) {
|
|
198
|
+
process.stdout.write(JSON.stringify({
|
|
199
|
+
jsonrpc: '2.0', id: req.id,
|
|
200
|
+
error: { code: -32603, message: err.message }
|
|
201
|
+
}) + '\n');
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
await handleRequest(request);
|
|
207
|
+
}
|
|
208
|
+
} catch (err) {
|
|
209
|
+
log(LOG_LEVELS.ERROR, 'Unexpected error: ' + err.message);
|
|
210
|
+
if (request && request.id != null) {
|
|
211
|
+
process.stdout.write(JSON.stringify({
|
|
212
|
+
jsonrpc: '2.0', id: request.id,
|
|
213
|
+
error: { code: -32603, message: err.message }
|
|
214
|
+
}) + '\n');
|
|
215
|
+
}
|
|
155
216
|
}
|
|
156
217
|
}
|
|
157
218
|
|
|
158
|
-
|
|
159
|
-
* Setup stdin/stdout handling
|
|
160
|
-
*/
|
|
219
|
+
// Stdin/stdout setup
|
|
161
220
|
let buffer = '';
|
|
162
221
|
|
|
163
222
|
process.stdin.setEncoding('utf8');
|
|
223
|
+
|
|
164
224
|
process.stdin.on('data', (chunk) => {
|
|
165
225
|
buffer += chunk;
|
|
166
226
|
const lines = buffer.split('\n');
|
|
167
227
|
buffer = lines.pop() || '';
|
|
168
|
-
|
|
169
228
|
for (const line of lines) {
|
|
170
|
-
|
|
171
|
-
if (trimmed) {
|
|
172
|
-
handleLine(trimmed);
|
|
173
|
-
}
|
|
229
|
+
if (line.trim()) handleLine(line.trim());
|
|
174
230
|
}
|
|
175
231
|
});
|
|
176
232
|
|
|
177
233
|
process.stdin.on('end', () => {
|
|
178
|
-
if (buffer.trim())
|
|
179
|
-
handleLine(buffer.trim());
|
|
180
|
-
}
|
|
234
|
+
if (buffer.trim()) handleLine(buffer.trim());
|
|
181
235
|
});
|
|
182
236
|
|
|
183
|
-
process.stdin.on('error', (
|
|
184
|
-
// Silently handle stdin errors - normal when Claude Desktop closes connection
|
|
185
|
-
});
|
|
237
|
+
process.stdin.on('error', () => {});
|
|
186
238
|
|
|
187
|
-
process.stdout.on('error', (
|
|
188
|
-
|
|
189
|
-
if (error.code === 'EPIPE') {
|
|
190
|
-
process.exit(0);
|
|
191
|
-
}
|
|
239
|
+
process.stdout.on('error', (err) => {
|
|
240
|
+
if (err.code === 'EPIPE') process.exit(0);
|
|
192
241
|
});
|
|
193
242
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if (isShuttingDown) return;
|
|
201
|
-
isShuttingDown = true;
|
|
202
|
-
log(LOG_LEVELS.INFO, `Received ${signal}, shutting down gracefully...`);
|
|
243
|
+
// Graceful shutdown
|
|
244
|
+
let shuttingDown = false;
|
|
245
|
+
function shutdown(signal) {
|
|
246
|
+
if (shuttingDown) return;
|
|
247
|
+
shuttingDown = true;
|
|
248
|
+
log(LOG_LEVELS.INFO, `Received ${signal}, shutting down`);
|
|
203
249
|
setTimeout(() => process.exit(0), 100);
|
|
204
250
|
}
|
|
251
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
252
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
205
253
|
|
|
206
|
-
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
207
|
-
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
208
|
-
|
|
209
|
-
// Start message
|
|
210
254
|
log(LOG_LEVELS.DEBUG, 'Meldoc MCP Stdio Proxy started');
|
package/lib/cli/commands.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const { interactiveLogin } = require('../core/device-flow');
|
|
8
|
+
const { pkceLogin } = require('../core/oauth-pkce');
|
|
8
9
|
const { deleteCredentials } = require('../core/credentials');
|
|
9
10
|
const { getAuthStatus, getAccessToken } = require('../core/auth');
|
|
10
11
|
const { setWorkspaceAlias, getWorkspaceAlias } = require('../core/config');
|
|
@@ -24,16 +25,26 @@ const APP_URL = getAppUrl();
|
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* Handle auth login command
|
|
28
|
+
* @param {Object} options
|
|
29
|
+
* @param {boolean} options.pkce - Use OAuth 2.1 PKCE flow instead of device flow
|
|
27
30
|
*/
|
|
28
|
-
async function handleAuthLogin() {
|
|
31
|
+
async function handleAuthLogin(options = {}) {
|
|
29
32
|
try {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
33
|
+
if (options.pkce) {
|
|
34
|
+
await pkceLogin({
|
|
35
|
+
apiBaseUrl: API_URL,
|
|
36
|
+
appUrl: APP_URL,
|
|
37
|
+
timeout: 120000
|
|
38
|
+
});
|
|
39
|
+
} else {
|
|
40
|
+
await interactiveLogin({
|
|
41
|
+
autoOpen: true,
|
|
42
|
+
showQR: false,
|
|
43
|
+
timeout: 120000,
|
|
44
|
+
apiBaseUrl: API_URL,
|
|
45
|
+
appUrl: APP_URL
|
|
46
|
+
});
|
|
47
|
+
}
|
|
37
48
|
process.exit(0);
|
|
38
49
|
} catch (error) {
|
|
39
50
|
process.exit(1);
|
package/lib/core/auth.js
CHANGED
|
@@ -2,6 +2,7 @@ const axios = require('axios');
|
|
|
2
2
|
const https = require('https');
|
|
3
3
|
const { readCredentials, writeCredentials, isTokenExpiredOrExpiringSoon, isTokenExpired } = require('./credentials');
|
|
4
4
|
const { getApiUrl } = require('./constants');
|
|
5
|
+
const { refreshOAuthToken } = require('./oauth-pkce');
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Get access token with priority:
|
|
@@ -57,7 +58,9 @@ async function getAccessToken() {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
/**
|
|
60
|
-
* Refresh access token using
|
|
61
|
+
* Refresh access token using the appropriate method based on auth_method.
|
|
62
|
+
* - device flow (default): POST /api/auth/refresh
|
|
63
|
+
* - pkce: POST /mcp/oauth/token with grant_type=refresh_token
|
|
61
64
|
* @param {Object} credentials - Current credentials object
|
|
62
65
|
* @returns {Promise<Object|null>} Updated credentials or null if refresh failed
|
|
63
66
|
*/
|
|
@@ -65,32 +68,48 @@ async function refreshToken(credentials) {
|
|
|
65
68
|
if (!credentials || !credentials.tokens || !credentials.tokens.refreshToken) {
|
|
66
69
|
return null;
|
|
67
70
|
}
|
|
68
|
-
|
|
71
|
+
|
|
69
72
|
const apiBaseUrl = credentials.apiBaseUrl || getApiUrl();
|
|
70
|
-
|
|
73
|
+
|
|
71
74
|
try {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
let newTokens;
|
|
76
|
+
|
|
77
|
+
if (credentials.auth_method === 'pkce') {
|
|
78
|
+
// OAuth 2.1 PKCE refresh (token rotation — server issues new refresh token)
|
|
79
|
+
newTokens = await refreshOAuthToken(
|
|
80
|
+
apiBaseUrl,
|
|
81
|
+
credentials.tokens.refreshToken,
|
|
82
|
+
credentials.client_id
|
|
83
|
+
);
|
|
84
|
+
} else {
|
|
85
|
+
// Device flow refresh
|
|
86
|
+
const response = await axios.post(`${apiBaseUrl}/api/auth/refresh`, {
|
|
87
|
+
refreshToken: credentials.tokens.refreshToken
|
|
88
|
+
}, {
|
|
89
|
+
timeout: 10000,
|
|
90
|
+
httpsAgent: new https.Agent({ keepAlive: true })
|
|
91
|
+
});
|
|
92
|
+
newTokens = {
|
|
93
|
+
accessToken: response.data.accessToken,
|
|
94
|
+
refreshToken: response.data.refreshToken || credentials.tokens.refreshToken,
|
|
95
|
+
expiresAt: response.data.expiresAt
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
80
99
|
const updatedCredentials = {
|
|
81
100
|
...credentials,
|
|
82
101
|
tokens: {
|
|
83
|
-
accessToken:
|
|
84
|
-
accessExpiresAt:
|
|
85
|
-
refreshToken:
|
|
102
|
+
accessToken: newTokens.accessToken,
|
|
103
|
+
accessExpiresAt: newTokens.expiresAt || newTokens.accessExpiresAt,
|
|
104
|
+
refreshToken: newTokens.refreshToken
|
|
86
105
|
},
|
|
87
106
|
updatedAt: new Date().toISOString()
|
|
88
107
|
};
|
|
89
|
-
|
|
108
|
+
|
|
90
109
|
writeCredentials(updatedCredentials);
|
|
91
110
|
return updatedCredentials;
|
|
92
111
|
} catch (error) {
|
|
93
|
-
// Refresh failed - delete credentials
|
|
112
|
+
// Refresh failed - delete credentials so user must re-login
|
|
94
113
|
const { deleteCredentials } = require('./credentials');
|
|
95
114
|
deleteCredentials();
|
|
96
115
|
return null;
|