@meldocio/mcp-stdio-proxy 1.0.22 → 1.0.24
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/bin/cli.js +33 -1088
- package/bin/meldoc-mcp-proxy.js +134 -1264
- package/lib/cli/commands.js +277 -0
- package/lib/cli/formatters.js +137 -0
- package/lib/core/constants.js +98 -0
- package/lib/http/client.js +61 -0
- package/lib/http/error-handler.js +195 -0
- package/lib/install/config-manager.js +203 -0
- package/lib/install/config-paths.js +198 -0
- package/lib/install/installers.js +328 -0
- package/lib/install/templates.js +266 -0
- package/lib/mcp/handlers.js +185 -0
- package/lib/mcp/tools-call.js +179 -0
- package/lib/protocol/error-codes.js +143 -0
- package/lib/protocol/json-rpc.js +183 -0
- package/lib/protocol/tools-schema.js +239 -0
- package/package.json +1 -1
- package/lib/constants.js +0 -31
- /package/lib/{auth.js → core/auth.js} +0 -0
- /package/lib/{config.js → core/config.js} +0 -0
- /package/lib/{credentials.js → core/credentials.js} +0 -0
- /package/lib/{device-flow.js → core/device-flow.js} +0 -0
- /package/lib/{logger.js → core/logger.js} +0 -0
- /package/lib/{workspace.js → core/workspace.js} +0 -0
package/bin/meldoc-mcp-proxy.js
CHANGED
|
@@ -1,337 +1,109 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Meldoc MCP Stdio Proxy
|
|
5
|
+
*
|
|
6
|
+
* Thin proxy that handles MCP protocol and forwards requests to Meldoc API.
|
|
7
|
+
* Most logic has been extracted to lib/ modules for better maintainability.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { validateRequest } = require('../lib/protocol/json-rpc');
|
|
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');
|
|
9
18
|
|
|
10
19
|
// Check for CLI commands first
|
|
11
20
|
const args = process.argv.slice(2);
|
|
12
21
|
if (args.length > 0) {
|
|
13
22
|
const command = args[0];
|
|
14
|
-
|
|
15
|
-
if (command === 'auth' || command === 'config' || command === 'install' ||
|
|
23
|
+
if (command === 'auth' || command === 'config' || command === 'install' ||
|
|
16
24
|
command === 'uninstall' || command === 'help' || command === '--help' || command === '-h') {
|
|
17
25
|
require('./cli');
|
|
18
|
-
// Don't exit here - cli.js will handle process.exit() after async operations complete
|
|
19
26
|
return;
|
|
20
27
|
}
|
|
21
28
|
}
|
|
22
29
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
// Try from node_modules (when installed)
|
|
31
|
-
pkg = require(path.join(__dirname, '../../package.json'));
|
|
32
|
-
} catch (e2) {
|
|
33
|
-
// Fallback to hardcoded values
|
|
34
|
-
pkg = {
|
|
35
|
-
name: '@meldocio/mcp-stdio-proxy',
|
|
36
|
-
version: '1.0.5'
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// JSON-RPC error codes
|
|
42
|
-
const JSON_RPC_ERROR_CODES = {
|
|
43
|
-
PARSE_ERROR: -32700,
|
|
44
|
-
INVALID_REQUEST: -32600,
|
|
45
|
-
METHOD_NOT_FOUND: -32601,
|
|
46
|
-
INVALID_PARAMS: -32602,
|
|
47
|
-
INTERNAL_ERROR: -32603,
|
|
48
|
-
SERVER_ERROR: -32000
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
// Custom error codes
|
|
52
|
-
const CUSTOM_ERROR_CODES = {
|
|
53
|
-
AUTH_REQUIRED: -32001,
|
|
54
|
-
NOT_FOUND: -32002,
|
|
55
|
-
RATE_LIMIT: -32003
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
// Log levels
|
|
59
|
-
const LOG_LEVELS = {
|
|
60
|
-
ERROR: 0,
|
|
61
|
-
WARN: 1,
|
|
62
|
-
INFO: 2,
|
|
63
|
-
DEBUG: 3
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
// Import new auth and workspace modules
|
|
67
|
-
const { getAccessToken, getAuthStatus } = require('../lib/auth');
|
|
68
|
-
const { resolveWorkspaceAlias } = require('../lib/workspace');
|
|
69
|
-
const { getApiUrl, getAppUrl } = require('../lib/constants');
|
|
70
|
-
const { setWorkspaceAlias, getWorkspaceAlias } = require('../lib/config');
|
|
71
|
-
const { interactiveLogin, canOpenBrowser } = require('../lib/device-flow');
|
|
72
|
-
|
|
73
|
-
// Configuration
|
|
74
|
-
const apiUrl = getApiUrl();
|
|
75
|
-
const appUrl = getAppUrl();
|
|
76
|
-
const rpcEndpoint = `${apiUrl}/mcp/v1/rpc`;
|
|
77
|
-
const REQUEST_TIMEOUT = 25000; // 25 seconds (less than Claude Desktop's 30s timeout)
|
|
78
|
-
const LOG_LEVEL = getLogLevel(process.env.LOG_LEVEL || 'ERROR');
|
|
79
|
-
|
|
80
|
-
// Track if we've attempted auto-authentication
|
|
81
|
-
let autoAuthAttempted = false;
|
|
82
|
-
let autoAuthInProgress = false;
|
|
83
|
-
|
|
84
|
-
// Get log level from environment
|
|
85
|
-
function getLogLevel(level) {
|
|
86
|
-
const upper = (level || '').toUpperCase();
|
|
87
|
-
return LOG_LEVELS[upper] !== undefined ? LOG_LEVELS[upper] : LOG_LEVELS.ERROR;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Logging function - all logs go to stderr with beautiful colors
|
|
91
|
-
function log(level, message, ...args) {
|
|
92
|
-
if (LOG_LEVEL >= level) {
|
|
93
|
-
const levelName = Object.keys(LOG_LEVELS)[level];
|
|
94
|
-
let prefix, colorFn;
|
|
95
|
-
|
|
96
|
-
switch (level) {
|
|
97
|
-
case LOG_LEVELS.ERROR:
|
|
98
|
-
prefix = chalk.red('✗ [ERROR]');
|
|
99
|
-
colorFn = chalk.redBright;
|
|
100
|
-
break;
|
|
101
|
-
case LOG_LEVELS.WARN:
|
|
102
|
-
prefix = chalk.yellow('⚠ [WARN]');
|
|
103
|
-
colorFn = chalk.yellowBright;
|
|
104
|
-
break;
|
|
105
|
-
case LOG_LEVELS.INFO:
|
|
106
|
-
prefix = chalk.blue('ℹ [INFO]');
|
|
107
|
-
colorFn = chalk.blueBright;
|
|
108
|
-
break;
|
|
109
|
-
case LOG_LEVELS.DEBUG:
|
|
110
|
-
prefix = chalk.gray('🔍 [DEBUG]');
|
|
111
|
-
colorFn = chalk.gray;
|
|
112
|
-
break;
|
|
113
|
-
default:
|
|
114
|
-
prefix = `[${levelName}]`;
|
|
115
|
-
colorFn = (text) => text;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
process.stderr.write(`${prefix} ${colorFn(message)}\n`);
|
|
119
|
-
if (args.length > 0 && LOG_LEVEL >= LOG_LEVELS.DEBUG) {
|
|
120
|
-
process.stderr.write(chalk.gray(JSON.stringify(args, null, 2)) + '\n');
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Buffer for incomplete lines
|
|
126
|
-
let buffer = '';
|
|
127
|
-
|
|
128
|
-
// Set stdin encoding
|
|
129
|
-
process.stdin.setEncoding('utf8');
|
|
130
|
-
|
|
131
|
-
// Handle stdin data
|
|
132
|
-
process.stdin.on('data', (chunk) => {
|
|
133
|
-
buffer += chunk;
|
|
134
|
-
const lines = buffer.split('\n');
|
|
135
|
-
// Keep the last incomplete line in buffer
|
|
136
|
-
buffer = lines.pop() || '';
|
|
137
|
-
|
|
138
|
-
// Process complete lines
|
|
139
|
-
for (const line of lines) {
|
|
140
|
-
const trimmed = line.trim();
|
|
141
|
-
// Skip empty lines but don't exit
|
|
142
|
-
if (trimmed) {
|
|
143
|
-
handleLine(trimmed);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
// Handle end of input
|
|
149
|
-
process.stdin.on('end', () => {
|
|
150
|
-
// Process any remaining buffer
|
|
151
|
-
if (buffer.trim()) {
|
|
152
|
-
handleLine(buffer.trim());
|
|
153
|
-
}
|
|
154
|
-
// Don't exit - Claude Desktop may reconnect
|
|
155
|
-
// The process will be terminated by Claude Desktop when needed
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
// Handle errors - don't exit, just log
|
|
159
|
-
process.stdin.on('error', (error) => {
|
|
160
|
-
// Log to stderr but don't exit - Claude Desktop may close stdin
|
|
161
|
-
// Silently handle stdin errors - they're normal when Claude Desktop closes the connection
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
// Handle stdout errors
|
|
165
|
-
process.stdout.on('error', (error) => {
|
|
166
|
-
// If stdout is closed (e.g., Claude Desktop disconnected), exit gracefully
|
|
167
|
-
if (error.code === 'EPIPE') {
|
|
168
|
-
process.exit(0);
|
|
169
|
-
}
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
// Handle SIGINT/SIGTERM for graceful shutdown
|
|
173
|
-
let isShuttingDown = false;
|
|
174
|
-
function gracefulShutdown(signal) {
|
|
175
|
-
if (isShuttingDown) {
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
isShuttingDown = true;
|
|
179
|
-
log(LOG_LEVELS.INFO, `Received ${signal}, shutting down gracefully...`);
|
|
180
|
-
// Give a moment for any pending requests to complete
|
|
181
|
-
setTimeout(() => {
|
|
182
|
-
process.exit(0);
|
|
183
|
-
}, 100);
|
|
30
|
+
/**
|
|
31
|
+
* Get log level from environment
|
|
32
|
+
*/
|
|
33
|
+
function getLogLevel() {
|
|
34
|
+
const level = (process.env.LOG_LEVEL || 'ERROR').toUpperCase();
|
|
35
|
+
return LOG_LEVELS[level] !== undefined ? LOG_LEVELS[level] : LOG_LEVELS.ERROR;
|
|
184
36
|
}
|
|
185
37
|
|
|
186
|
-
|
|
187
|
-
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
38
|
+
const LOG_LEVEL = getLogLevel();
|
|
188
39
|
|
|
189
40
|
/**
|
|
190
|
-
*
|
|
41
|
+
* Log message to stderr
|
|
191
42
|
*/
|
|
192
|
-
function
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
try {
|
|
199
|
-
const request = JSON.parse(line);
|
|
200
|
-
handleRequest(request);
|
|
201
|
-
} catch (parseError) {
|
|
202
|
-
// Invalid JSON - try to extract id from the raw line if possible
|
|
203
|
-
// For parse errors, we can't reliably get the id, so we skip the response
|
|
204
|
-
// to avoid Zod validation errors in Claude Desktop (it doesn't accept id: null)
|
|
205
|
-
// This is acceptable per JSON-RPC spec - parse errors can be ignored if id is unknown
|
|
206
|
-
log(LOG_LEVELS.ERROR, `Parse error: ${parseError.message}`);
|
|
43
|
+
function log(level, message) {
|
|
44
|
+
if (LOG_LEVEL >= level) {
|
|
45
|
+
const levelName = Object.keys(LOG_LEVELS)[level] || 'UNKNOWN';
|
|
46
|
+
process.stderr.write(`[${levelName}] ${message}\n`);
|
|
207
47
|
}
|
|
208
48
|
}
|
|
209
49
|
|
|
210
50
|
/**
|
|
211
|
-
*
|
|
51
|
+
* Process single request to backend
|
|
212
52
|
*/
|
|
213
|
-
function
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
53
|
+
async function processSingleRequest(request) {
|
|
54
|
+
try {
|
|
55
|
+
const response = await makeBackendRequest(request);
|
|
56
|
+
handleBackendResponse(response, request);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
// Handle request errors (network, timeout, etc.)
|
|
59
|
+
if (error.code === 'ECONNABORTED') {
|
|
60
|
+
sendError(request.id, JSON_RPC_ERROR_CODES.INTERNAL_ERROR,
|
|
61
|
+
`Request timeout after ${error.timeout || 25000}ms`, {
|
|
62
|
+
code: 'TIMEOUT'
|
|
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
|
+
});
|
|
69
|
+
}
|
|
227
70
|
}
|
|
228
|
-
|
|
229
|
-
return { valid: true };
|
|
230
71
|
}
|
|
231
72
|
|
|
232
73
|
/**
|
|
233
74
|
* Handle a JSON-RPC request
|
|
234
|
-
* This function MUST always send a response for requests with id (except notifications)
|
|
235
75
|
*/
|
|
236
76
|
async function handleRequest(request) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Wrap in try-catch to ensure we always handle errors gracefully
|
|
77
|
+
if (!request) return;
|
|
78
|
+
|
|
243
79
|
try {
|
|
244
|
-
// Handle batch requests
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
// Notification - no response needed
|
|
259
|
-
continue;
|
|
260
|
-
} else if (method === 'ping') {
|
|
261
|
-
handlePing(req);
|
|
262
|
-
} else if (method === 'resources/list') {
|
|
263
|
-
handleResourcesList(req);
|
|
264
|
-
} else if (method === 'tools/list') {
|
|
265
|
-
handleToolsList(req);
|
|
266
|
-
} else if (method === 'tools/call') {
|
|
267
|
-
await handleToolsCall(req);
|
|
268
|
-
} else {
|
|
269
|
-
// Forward to backend
|
|
270
|
-
await processSingleRequest(req);
|
|
271
|
-
}
|
|
272
|
-
} catch (error) {
|
|
273
|
-
// Catch any errors in batch request processing
|
|
274
|
-
log(LOG_LEVELS.ERROR, `Error processing batch request: ${error.message || 'Unknown error'}`);
|
|
275
|
-
log(LOG_LEVELS.DEBUG, `Error stack: ${error.stack || 'No stack trace'}`);
|
|
276
|
-
// Send error response if request has id
|
|
277
|
-
if (req && req.id !== undefined && req.id !== null) {
|
|
278
|
-
sendError(req.id, JSON_RPC_ERROR_CODES.INTERNAL_ERROR,
|
|
279
|
-
`Error processing request: ${error.message || 'Unknown error'}`, {
|
|
280
|
-
code: 'INTERNAL_ERROR'
|
|
281
|
-
});
|
|
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
|
+
}
|
|
282
94
|
}
|
|
283
95
|
}
|
|
284
96
|
}
|
|
97
|
+
return;
|
|
285
98
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
// Validate single request
|
|
290
|
-
const validation = validateRequest(request);
|
|
291
|
-
if (!validation.valid) {
|
|
292
|
-
sendError(request.id, JSON_RPC_ERROR_CODES.INVALID_REQUEST, validation.error);
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// Handle MCP protocol methods locally (not forwarded to backend)
|
|
297
|
-
const method = request.method;
|
|
298
|
-
if (method === 'initialize') {
|
|
299
|
-
handleInitialize(request);
|
|
300
|
-
return;
|
|
301
|
-
} else if (method === 'initialized' || method === 'notifications/initialized') {
|
|
302
|
-
// Notification - no response needed per MCP spec
|
|
303
|
-
return;
|
|
304
|
-
} else if (method === 'notifications/cancelled') {
|
|
305
|
-
// Notification - no response needed
|
|
306
|
-
return;
|
|
307
|
-
} else if (method === 'ping') {
|
|
308
|
-
handlePing(request);
|
|
309
|
-
return;
|
|
310
|
-
} else if (method === 'resources/list') {
|
|
311
|
-
// Return empty resources list (resources not supported yet)
|
|
312
|
-
handleResourcesList(request);
|
|
313
|
-
return;
|
|
314
|
-
} else if (method === 'tools/list') {
|
|
315
|
-
// Return static list of tools (never proxy to backend)
|
|
316
|
-
handleToolsList(request);
|
|
317
|
-
return;
|
|
318
|
-
} else if (method === 'tools/call') {
|
|
319
|
-
// Handle tools/call - check if it's a local tool first
|
|
320
|
-
await handleToolsCall(request);
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// All other methods are forwarded to backend
|
|
325
|
-
await processSingleRequest(request);
|
|
99
|
+
|
|
100
|
+
// Handle single request
|
|
101
|
+
await handleSingleRequest(request);
|
|
326
102
|
} catch (error) {
|
|
327
|
-
// Catch any completely unexpected errors in handleRequest
|
|
328
103
|
log(LOG_LEVELS.ERROR, `Unexpected error in handleRequest: ${error.message || 'Unknown error'}`);
|
|
329
|
-
log(LOG_LEVELS.DEBUG, `Error stack: ${error.stack || 'No stack trace'}`);
|
|
330
|
-
|
|
331
|
-
// Only send error if request has id (not for notifications)
|
|
332
104
|
if (request && request.id !== undefined && request.id !== null) {
|
|
333
|
-
sendError(request.id, JSON_RPC_ERROR_CODES.INTERNAL_ERROR,
|
|
334
|
-
`
|
|
105
|
+
sendError(request.id, JSON_RPC_ERROR_CODES.INTERNAL_ERROR,
|
|
106
|
+
`Error processing request: ${error.message || 'Unknown error'}`, {
|
|
335
107
|
code: 'INTERNAL_ERROR'
|
|
336
108
|
});
|
|
337
109
|
}
|
|
@@ -339,1002 +111,100 @@ async function handleRequest(request) {
|
|
|
339
111
|
}
|
|
340
112
|
|
|
341
113
|
/**
|
|
342
|
-
* Handle
|
|
343
|
-
* This is called by Claude Desktop to establish the connection
|
|
114
|
+
* Handle a single JSON-RPC request
|
|
344
115
|
*/
|
|
345
|
-
function
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
capabilities: {
|
|
352
|
-
tools: {},
|
|
353
|
-
resources: {}
|
|
354
|
-
},
|
|
355
|
-
serverInfo: {
|
|
356
|
-
name: pkg.name,
|
|
357
|
-
version: pkg.version
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
};
|
|
361
|
-
|
|
362
|
-
log(LOG_LEVELS.DEBUG, 'Initialize request received');
|
|
363
|
-
process.stdout.write(JSON.stringify(response) + '\n');
|
|
364
|
-
if (process.stdout.isTTY) {
|
|
365
|
-
process.stdout.flush();
|
|
116
|
+
async function handleSingleRequest(request) {
|
|
117
|
+
// Validate request
|
|
118
|
+
const validation = validateRequest(request);
|
|
119
|
+
if (!validation.valid) {
|
|
120
|
+
sendError(request.id, JSON_RPC_ERROR_CODES.INVALID_REQUEST, validation.error);
|
|
121
|
+
return;
|
|
366
122
|
}
|
|
367
|
-
}
|
|
368
123
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
jsonrpc: '2.0',
|
|
375
|
-
id: request.id,
|
|
376
|
-
result: {}
|
|
377
|
-
};
|
|
378
|
-
|
|
379
|
-
process.stdout.write(JSON.stringify(response) + '\n');
|
|
380
|
-
if (process.stdout.isTTY) {
|
|
381
|
-
process.stdout.flush();
|
|
124
|
+
const method = request.method;
|
|
125
|
+
|
|
126
|
+
// Handle local MCP methods (initialize, ping, tools/list, etc.)
|
|
127
|
+
if (handleLocalMethod(request)) {
|
|
128
|
+
return; // Handled locally
|
|
382
129
|
}
|
|
383
|
-
}
|
|
384
130
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
const response = {
|
|
391
|
-
jsonrpc: '2.0',
|
|
392
|
-
id: request.id,
|
|
393
|
-
result: {
|
|
394
|
-
resources: []
|
|
131
|
+
// Handle tools/call (may be local or proxied)
|
|
132
|
+
if (method === 'tools/call') {
|
|
133
|
+
const handled = await handleToolsCall(request);
|
|
134
|
+
if (handled) {
|
|
135
|
+
return; // Handled locally
|
|
395
136
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
process.stdout.write(JSON.stringify(response) + '\n');
|
|
399
|
-
if (process.stdout.isTTY) {
|
|
400
|
-
process.stdout.flush();
|
|
137
|
+
// Fall through to proxy to backend
|
|
401
138
|
}
|
|
402
|
-
}
|
|
403
139
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
* This is always returned locally, never proxied to backend
|
|
407
|
-
*/
|
|
408
|
-
function getToolsList() {
|
|
409
|
-
return [
|
|
410
|
-
{
|
|
411
|
-
name: 'docs_list',
|
|
412
|
-
description: 'List documents in workspace/project. For public tokens, only shows published public documents.',
|
|
413
|
-
inputSchema: {
|
|
414
|
-
type: 'object',
|
|
415
|
-
properties: {
|
|
416
|
-
workspaceAlias: { type: 'string', description: 'Workspace alias (auto-selected if user has only one workspace)' },
|
|
417
|
-
workspaceId: { type: 'string', description: 'Workspace UUID (auto-selected if user has only one workspace)' },
|
|
418
|
-
projectId: { type: 'string', description: 'UUID of the project to list documents from' },
|
|
419
|
-
cursor: { type: 'string', description: 'Pagination cursor' },
|
|
420
|
-
limit: { type: 'integer', description: 'Maximum number of documents to return (default: 50, max: 100)' }
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
},
|
|
424
|
-
{
|
|
425
|
-
name: 'docs_get',
|
|
426
|
-
description: 'Get a specific document by ID or path. For public tokens, allows access to public and unlisted documents.',
|
|
427
|
-
inputSchema: {
|
|
428
|
-
type: 'object',
|
|
429
|
-
required: ['docId'],
|
|
430
|
-
properties: {
|
|
431
|
-
workspaceAlias: { type: 'string', description: 'Workspace alias (auto-selected if user has only one workspace)' },
|
|
432
|
-
workspaceId: { type: 'string', description: 'Workspace UUID (auto-selected if user has only one workspace)' },
|
|
433
|
-
docId: { type: 'string', description: 'UUID of the document (alias: id)' },
|
|
434
|
-
id: { type: 'string', description: 'UUID of the document (alias for docId)' },
|
|
435
|
-
path: { type: 'string', description: 'Path of the document (not yet implemented)' }
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
},
|
|
439
|
-
{
|
|
440
|
-
name: 'docs_tree',
|
|
441
|
-
description: 'Get the document tree structure for a project. For public tokens, only includes published public documents.',
|
|
442
|
-
inputSchema: {
|
|
443
|
-
type: 'object',
|
|
444
|
-
required: ['projectId'],
|
|
445
|
-
properties: {
|
|
446
|
-
workspaceAlias: { type: 'string', description: 'Workspace alias (auto-selected if user has only one workspace)' },
|
|
447
|
-
workspaceId: { type: 'string', description: 'Workspace UUID (auto-selected if user has only one workspace)' },
|
|
448
|
-
projectId: { type: 'string', description: 'UUID of the project' },
|
|
449
|
-
project_alias: { type: 'string', description: 'Alias of the project (alternative to projectId)' }
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
},
|
|
453
|
-
{
|
|
454
|
-
name: 'docs_search',
|
|
455
|
-
description: 'Search documents by text query. For public tokens, only searches published public documents.',
|
|
456
|
-
inputSchema: {
|
|
457
|
-
type: 'object',
|
|
458
|
-
required: ['query'],
|
|
459
|
-
properties: {
|
|
460
|
-
workspaceAlias: { type: 'string', description: 'Workspace alias (auto-selected if user has only one workspace)' },
|
|
461
|
-
workspaceId: { type: 'string', description: 'Workspace UUID (auto-selected if user has only one workspace)' },
|
|
462
|
-
query: { type: 'string', description: 'Search query text' },
|
|
463
|
-
projectId: { type: 'string', description: 'UUID of the project to search in' },
|
|
464
|
-
limit: { type: 'integer', description: 'Maximum number of results (default: 20, max: 50)' }
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
},
|
|
468
|
-
{
|
|
469
|
-
name: 'docs_update',
|
|
470
|
-
description: 'Update a document\'s content and/or metadata. Requires update permission (internal tokens only).',
|
|
471
|
-
inputSchema: {
|
|
472
|
-
type: 'object',
|
|
473
|
-
required: ['docId'],
|
|
474
|
-
properties: {
|
|
475
|
-
workspaceAlias: { type: 'string', description: 'Workspace alias (auto-selected if user has only one workspace)' },
|
|
476
|
-
workspaceId: { type: 'string', description: 'Workspace UUID (auto-selected if user has only one workspace)' },
|
|
477
|
-
docId: { type: 'string', description: 'UUID of the document to update' },
|
|
478
|
-
contentMd: { type: 'string', description: 'New markdown content for the document (optional, can update individual fields without content)' },
|
|
479
|
-
title: { type: 'string', description: 'New title for the document' },
|
|
480
|
-
alias: { type: 'string', description: 'New alias for the document' },
|
|
481
|
-
parentAlias: { type: 'string', description: 'Alias of the parent document (set to empty string to remove parent)' },
|
|
482
|
-
workflow: { type: 'string', enum: ['published', 'draft'], description: 'Workflow status: \'published\' or \'draft\'' },
|
|
483
|
-
visibility: { type: 'string', enum: ['visible', 'hidden'], description: 'Visibility: \'visible\' or \'hidden\'' },
|
|
484
|
-
exposure: { type: 'string', enum: ['private', 'unlisted', 'public', 'inherit'], description: 'Exposure level: \'private\', \'unlisted\', \'public\', or \'inherit\'' },
|
|
485
|
-
expectedUpdatedAt: { type: 'string', description: 'Expected updatedAt timestamp for optimistic locking (RFC3339 format)' }
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
},
|
|
489
|
-
{
|
|
490
|
-
name: 'docs_create',
|
|
491
|
-
description: 'Create a new document. Requires create permission (internal tokens only).',
|
|
492
|
-
inputSchema: {
|
|
493
|
-
type: 'object',
|
|
494
|
-
required: ['projectId', 'title', 'contentMd'],
|
|
495
|
-
properties: {
|
|
496
|
-
workspaceAlias: { type: 'string', description: 'Workspace alias (auto-selected if user has only one workspace)' },
|
|
497
|
-
workspaceId: { type: 'string', description: 'Workspace UUID (auto-selected if user has only one workspace)' },
|
|
498
|
-
projectId: { type: 'string', description: 'UUID of the project to create the document in' },
|
|
499
|
-
title: { type: 'string', description: 'Title of the document' },
|
|
500
|
-
contentMd: { type: 'string', description: 'Markdown content for the document' },
|
|
501
|
-
alias: { type: 'string', description: 'Alias for the document (will be auto-generated from title if not provided)' },
|
|
502
|
-
parentAlias: { type: 'string', description: 'Alias of the parent document' }
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
},
|
|
506
|
-
{
|
|
507
|
-
name: 'docs_delete',
|
|
508
|
-
description: 'Delete a document. Requires delete permission (internal tokens only).',
|
|
509
|
-
inputSchema: {
|
|
510
|
-
type: 'object',
|
|
511
|
-
required: ['docId'],
|
|
512
|
-
properties: {
|
|
513
|
-
workspaceAlias: { type: 'string', description: 'Workspace alias (auto-selected if user has only one workspace)' },
|
|
514
|
-
workspaceId: { type: 'string', description: 'Workspace UUID (auto-selected if user has only one workspace)' },
|
|
515
|
-
docId: { type: 'string', description: 'UUID of the document to delete' }
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
},
|
|
519
|
-
{
|
|
520
|
-
name: 'docs_links',
|
|
521
|
-
description: 'Get all outgoing links from a document (links that point from this document to other documents).',
|
|
522
|
-
inputSchema: {
|
|
523
|
-
type: 'object',
|
|
524
|
-
required: ['docId'],
|
|
525
|
-
properties: {
|
|
526
|
-
workspaceAlias: { type: 'string', description: 'Workspace alias (auto-selected if user has only one workspace)' },
|
|
527
|
-
workspaceId: { type: 'string', description: 'Workspace UUID (auto-selected if user has only one workspace)' },
|
|
528
|
-
docId: { type: 'string', description: 'UUID of the document' }
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
},
|
|
532
|
-
{
|
|
533
|
-
name: 'docs_backlinks',
|
|
534
|
-
description: 'Get all backlinks to a document (links from other documents that point to this document).',
|
|
535
|
-
inputSchema: {
|
|
536
|
-
type: 'object',
|
|
537
|
-
required: ['docId'],
|
|
538
|
-
properties: {
|
|
539
|
-
docId: { type: 'string', description: 'UUID of the document' }
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
},
|
|
543
|
-
{
|
|
544
|
-
name: 'projects_list',
|
|
545
|
-
description: 'List projects accessible by this token. For public tokens, only shows public projects.',
|
|
546
|
-
inputSchema: {
|
|
547
|
-
type: 'object',
|
|
548
|
-
properties: {
|
|
549
|
-
workspaceAlias: { type: 'string', description: 'Workspace alias (auto-selected if user has only one workspace)' },
|
|
550
|
-
workspaceId: { type: 'string', description: 'Workspace UUID (auto-selected if user has only one workspace)' }
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
},
|
|
554
|
-
{
|
|
555
|
-
name: 'server_info',
|
|
556
|
-
description: 'Get information about this MCP server\'s configuration, capabilities, and accessible projects.',
|
|
557
|
-
inputSchema: {
|
|
558
|
-
type: 'object',
|
|
559
|
-
properties: {
|
|
560
|
-
workspaceAlias: { type: 'string', description: 'Workspace alias (auto-selected if user has only one workspace)' },
|
|
561
|
-
workspaceId: { type: 'string', description: 'Workspace UUID (auto-selected if user has only one workspace)' }
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
},
|
|
565
|
-
{
|
|
566
|
-
name: 'list_workspaces',
|
|
567
|
-
description: 'List all workspaces accessible by the current user or integration token. For integration tokens, returns the workspace from token scope. Works without workspace header via /mcp/v1/rpc endpoint.',
|
|
568
|
-
inputSchema: {
|
|
569
|
-
type: 'object',
|
|
570
|
-
properties: {}
|
|
571
|
-
}
|
|
572
|
-
},
|
|
573
|
-
{
|
|
574
|
-
name: 'get_workspace',
|
|
575
|
-
description: 'Get the current workspace alias from repo config or global config. Reads workspaceAlias from configuration files.',
|
|
576
|
-
inputSchema: {
|
|
577
|
-
type: 'object',
|
|
578
|
-
properties: {}
|
|
579
|
-
}
|
|
580
|
-
},
|
|
581
|
-
{
|
|
582
|
-
name: 'set_workspace',
|
|
583
|
-
description: 'Set the workspace alias in global config (~/.meldoc/config.json). This workspace will be used automatically if user has multiple workspaces.',
|
|
584
|
-
inputSchema: {
|
|
585
|
-
type: 'object',
|
|
586
|
-
required: ['alias'],
|
|
587
|
-
properties: {
|
|
588
|
-
alias: { type: 'string', description: 'Workspace alias to set' }
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
},
|
|
592
|
-
{
|
|
593
|
-
name: 'auth_status',
|
|
594
|
-
description: 'Check authentication status. Returns whether user is logged in and authentication details.',
|
|
595
|
-
inputSchema: {
|
|
596
|
-
type: 'object',
|
|
597
|
-
properties: {}
|
|
598
|
-
}
|
|
599
|
-
},
|
|
600
|
-
{
|
|
601
|
-
name: 'auth_login_instructions',
|
|
602
|
-
description: 'Get instructions for logging in. Returns the command to run for authentication.',
|
|
603
|
-
inputSchema: {
|
|
604
|
-
type: 'object',
|
|
605
|
-
properties: {}
|
|
606
|
-
}
|
|
607
|
-
},
|
|
608
|
-
{
|
|
609
|
-
name: 'auth_login',
|
|
610
|
-
description: 'Start interactive login process. Opens browser for authentication. This is equivalent to running: npx @meldocio/mcp-stdio-proxy@latest auth login',
|
|
611
|
-
inputSchema: {
|
|
612
|
-
type: 'object',
|
|
613
|
-
properties: {
|
|
614
|
-
timeout: { type: 'integer', description: 'Timeout in milliseconds (default: 120000)' }
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
];
|
|
140
|
+
// Forward all other methods to backend
|
|
141
|
+
await processSingleRequest(request);
|
|
619
142
|
}
|
|
620
143
|
|
|
621
144
|
/**
|
|
622
|
-
* Handle
|
|
623
|
-
* Always returns static list locally, never proxies to backend
|
|
624
|
-
* This function MUST always succeed and return a response
|
|
145
|
+
* Handle a single line from stdin
|
|
625
146
|
*/
|
|
626
|
-
function
|
|
147
|
+
function handleLine(line) {
|
|
148
|
+
if (!line || !line.trim()) return;
|
|
149
|
+
|
|
627
150
|
try {
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
log(LOG_LEVELS.INFO, `Returning ${tools.length} tools locally: ${toolNames}`);
|
|
633
|
-
|
|
634
|
-
const response = {
|
|
635
|
-
jsonrpc: '2.0',
|
|
636
|
-
id: request.id,
|
|
637
|
-
result: {
|
|
638
|
-
tools: tools
|
|
639
|
-
}
|
|
640
|
-
};
|
|
641
|
-
|
|
642
|
-
// Always send response, even if there's an error writing
|
|
643
|
-
try {
|
|
644
|
-
const responseStr = JSON.stringify(response);
|
|
645
|
-
process.stdout.write(responseStr + '\n');
|
|
646
|
-
if (process.stdout.isTTY) {
|
|
647
|
-
process.stdout.flush();
|
|
648
|
-
}
|
|
649
|
-
log(LOG_LEVELS.DEBUG, `Sent tools/list response (${responseStr.length} bytes)`);
|
|
650
|
-
} catch (writeError) {
|
|
651
|
-
// If stdout write fails, log but don't throw - we've already logged the response
|
|
652
|
-
log(LOG_LEVELS.ERROR, `Failed to write tools/list response: ${writeError.message}`);
|
|
653
|
-
}
|
|
654
|
-
} catch (error) {
|
|
655
|
-
// This should never happen, but if it does, send error response
|
|
656
|
-
log(LOG_LEVELS.ERROR, `Unexpected error in handleToolsList: ${error.message || 'Unknown error'}`);
|
|
657
|
-
log(LOG_LEVELS.DEBUG, `Error stack: ${error.stack || 'No stack trace'}`);
|
|
658
|
-
|
|
659
|
-
// Send error response
|
|
660
|
-
sendError(request.id, JSON_RPC_ERROR_CODES.INTERNAL_ERROR,
|
|
661
|
-
`Failed to get tools list: ${error.message || 'Unknown error'}`, {
|
|
662
|
-
code: 'INTERNAL_ERROR'
|
|
663
|
-
});
|
|
151
|
+
const request = JSON.parse(line);
|
|
152
|
+
handleRequest(request);
|
|
153
|
+
} catch (parseError) {
|
|
154
|
+
log(LOG_LEVELS.ERROR, `Parse error: ${parseError.message}`);
|
|
664
155
|
}
|
|
665
156
|
}
|
|
666
157
|
|
|
667
158
|
/**
|
|
668
|
-
*
|
|
669
|
-
* Checks if it's a local tool first, otherwise forwards to backend
|
|
159
|
+
* Setup stdin/stdout handling
|
|
670
160
|
*/
|
|
671
|
-
|
|
672
|
-
try {
|
|
673
|
-
const toolName = request.params?.name;
|
|
674
|
-
const arguments_ = request.params?.arguments || {};
|
|
675
|
-
|
|
676
|
-
log(LOG_LEVELS.DEBUG, `handleToolsCall: toolName=${toolName}`);
|
|
677
|
-
|
|
678
|
-
// Handle local tools
|
|
679
|
-
if (toolName === 'set_workspace') {
|
|
680
|
-
const alias = arguments_.alias;
|
|
681
|
-
if (!alias || typeof alias !== 'string') {
|
|
682
|
-
sendError(request.id, JSON_RPC_ERROR_CODES.INVALID_PARAMS, 'alias parameter is required and must be a string');
|
|
683
|
-
return;
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
try {
|
|
687
|
-
setWorkspaceAlias(alias);
|
|
688
|
-
const response = {
|
|
689
|
-
jsonrpc: '2.0',
|
|
690
|
-
id: request.id,
|
|
691
|
-
result: {
|
|
692
|
-
content: [
|
|
693
|
-
{
|
|
694
|
-
type: 'text',
|
|
695
|
-
text: `Workspace alias set to: ${alias}`
|
|
696
|
-
}
|
|
697
|
-
]
|
|
698
|
-
}
|
|
699
|
-
};
|
|
700
|
-
process.stdout.write(JSON.stringify(response) + '\n');
|
|
701
|
-
if (process.stdout.isTTY) {
|
|
702
|
-
process.stdout.flush();
|
|
703
|
-
}
|
|
704
|
-
} catch (error) {
|
|
705
|
-
sendError(request.id, JSON_RPC_ERROR_CODES.INTERNAL_ERROR, `Failed to set workspace alias: ${error.message}`);
|
|
706
|
-
}
|
|
707
|
-
return;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
if (toolName === 'get_workspace') {
|
|
711
|
-
try {
|
|
712
|
-
const workspaceAlias = getWorkspaceAlias();
|
|
713
|
-
const response = {
|
|
714
|
-
jsonrpc: '2.0',
|
|
715
|
-
id: request.id,
|
|
716
|
-
result: {
|
|
717
|
-
content: [
|
|
718
|
-
{
|
|
719
|
-
type: 'text',
|
|
720
|
-
text: JSON.stringify({
|
|
721
|
-
workspaceAlias: workspaceAlias || null,
|
|
722
|
-
source: workspaceAlias ? 'config' : 'not_found',
|
|
723
|
-
message: workspaceAlias ? `Current workspace: ${workspaceAlias}` : 'No workspace set in config'
|
|
724
|
-
}, null, 2)
|
|
725
|
-
}
|
|
726
|
-
]
|
|
727
|
-
}
|
|
728
|
-
};
|
|
729
|
-
process.stdout.write(JSON.stringify(response) + '\n');
|
|
730
|
-
if (process.stdout.isTTY) {
|
|
731
|
-
process.stdout.flush();
|
|
732
|
-
}
|
|
733
|
-
} catch (error) {
|
|
734
|
-
sendError(request.id, JSON_RPC_ERROR_CODES.INTERNAL_ERROR, `Failed to get workspace: ${error.message}`);
|
|
735
|
-
}
|
|
736
|
-
return;
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
if (toolName === 'auth_status') {
|
|
740
|
-
try {
|
|
741
|
-
const authStatus = await getAuthStatus();
|
|
742
|
-
if (!authStatus) {
|
|
743
|
-
const response = {
|
|
744
|
-
jsonrpc: '2.0',
|
|
745
|
-
id: request.id,
|
|
746
|
-
result: {
|
|
747
|
-
content: [
|
|
748
|
-
{
|
|
749
|
-
type: 'text',
|
|
750
|
-
text: JSON.stringify({
|
|
751
|
-
authenticated: false,
|
|
752
|
-
message: 'Not authenticated. Run: npx @meldocio/mcp-stdio-proxy@latest auth login'
|
|
753
|
-
}, null, 2)
|
|
754
|
-
}
|
|
755
|
-
]
|
|
756
|
-
}
|
|
757
|
-
};
|
|
758
|
-
process.stdout.write(JSON.stringify(response) + '\n');
|
|
759
|
-
if (process.stdout.isTTY) {
|
|
760
|
-
process.stdout.flush();
|
|
761
|
-
}
|
|
762
|
-
return;
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
const response = {
|
|
766
|
-
jsonrpc: '2.0',
|
|
767
|
-
id: request.id,
|
|
768
|
-
result: {
|
|
769
|
-
content: [
|
|
770
|
-
{
|
|
771
|
-
type: 'text',
|
|
772
|
-
text: JSON.stringify(authStatus, null, 2)
|
|
773
|
-
}
|
|
774
|
-
]
|
|
775
|
-
}
|
|
776
|
-
};
|
|
777
|
-
process.stdout.write(JSON.stringify(response) + '\n');
|
|
778
|
-
if (process.stdout.isTTY) {
|
|
779
|
-
process.stdout.flush();
|
|
780
|
-
}
|
|
781
|
-
} catch (error) {
|
|
782
|
-
sendError(request.id, JSON_RPC_ERROR_CODES.INTERNAL_ERROR, `Failed to get auth status: ${error.message}`);
|
|
783
|
-
}
|
|
784
|
-
return;
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
if (toolName === 'auth_login_instructions') {
|
|
788
|
-
const response = {
|
|
789
|
-
jsonrpc: '2.0',
|
|
790
|
-
id: request.id,
|
|
791
|
-
result: {
|
|
792
|
-
content: [
|
|
793
|
-
{
|
|
794
|
-
type: 'text',
|
|
795
|
-
text: 'To authenticate, run the following command:\n\nnpx @meldocio/mcp-stdio-proxy@latest auth login'
|
|
796
|
-
}
|
|
797
|
-
]
|
|
798
|
-
}
|
|
799
|
-
};
|
|
800
|
-
process.stdout.write(JSON.stringify(response) + '\n');
|
|
801
|
-
if (process.stdout.isTTY) {
|
|
802
|
-
process.stdout.flush();
|
|
803
|
-
}
|
|
804
|
-
return;
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
if (toolName === 'auth_login') {
|
|
808
|
-
try {
|
|
809
|
-
const timeout = arguments_.timeout || 120000;
|
|
810
|
-
|
|
811
|
-
// Check if we can open browser
|
|
812
|
-
if (!canOpenBrowser()) {
|
|
813
|
-
const response = {
|
|
814
|
-
jsonrpc: '2.0',
|
|
815
|
-
id: request.id,
|
|
816
|
-
result: {
|
|
817
|
-
content: [
|
|
818
|
-
{
|
|
819
|
-
type: 'text',
|
|
820
|
-
text: JSON.stringify({
|
|
821
|
-
success: false,
|
|
822
|
-
error: 'Cannot open browser automatically. Please run the command manually: npx @meldocio/mcp-stdio-proxy@latest auth login'
|
|
823
|
-
}, null, 2)
|
|
824
|
-
}
|
|
825
|
-
]
|
|
826
|
-
}
|
|
827
|
-
};
|
|
828
|
-
process.stdout.write(JSON.stringify(response) + '\n');
|
|
829
|
-
if (process.stdout.isTTY) {
|
|
830
|
-
process.stdout.flush();
|
|
831
|
-
}
|
|
832
|
-
return;
|
|
833
|
-
}
|
|
161
|
+
let buffer = '';
|
|
834
162
|
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
apiBaseUrl: apiUrl,
|
|
841
|
-
appUrl: appUrl
|
|
842
|
-
});
|
|
163
|
+
process.stdin.setEncoding('utf8');
|
|
164
|
+
process.stdin.on('data', (chunk) => {
|
|
165
|
+
buffer += chunk;
|
|
166
|
+
const lines = buffer.split('\n');
|
|
167
|
+
buffer = lines.pop() || '';
|
|
843
168
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
content: [
|
|
849
|
-
{
|
|
850
|
-
type: 'text',
|
|
851
|
-
text: JSON.stringify({
|
|
852
|
-
success: true,
|
|
853
|
-
message: 'Authentication successful! You are now logged in to Meldoc.'
|
|
854
|
-
}, null, 2)
|
|
855
|
-
}
|
|
856
|
-
]
|
|
857
|
-
}
|
|
858
|
-
};
|
|
859
|
-
process.stdout.write(JSON.stringify(response) + '\n');
|
|
860
|
-
if (process.stdout.isTTY) {
|
|
861
|
-
process.stdout.flush();
|
|
862
|
-
}
|
|
863
|
-
} catch (error) {
|
|
864
|
-
const response = {
|
|
865
|
-
jsonrpc: '2.0',
|
|
866
|
-
id: request.id,
|
|
867
|
-
result: {
|
|
868
|
-
content: [
|
|
869
|
-
{
|
|
870
|
-
type: 'text',
|
|
871
|
-
text: JSON.stringify({
|
|
872
|
-
success: false,
|
|
873
|
-
error: error.message || 'Authentication failed',
|
|
874
|
-
hint: 'You can try again or run: npx @meldocio/mcp-stdio-proxy@latest auth login'
|
|
875
|
-
}, null, 2)
|
|
876
|
-
}
|
|
877
|
-
]
|
|
878
|
-
}
|
|
879
|
-
};
|
|
880
|
-
process.stdout.write(JSON.stringify(response) + '\n');
|
|
881
|
-
if (process.stdout.isTTY) {
|
|
882
|
-
process.stdout.flush();
|
|
883
|
-
}
|
|
169
|
+
for (const line of lines) {
|
|
170
|
+
const trimmed = line.trim();
|
|
171
|
+
if (trimmed) {
|
|
172
|
+
handleLine(trimmed);
|
|
884
173
|
}
|
|
885
|
-
return;
|
|
886
174
|
}
|
|
175
|
+
});
|
|
887
176
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
} catch (error) {
|
|
892
|
-
// Catch any unexpected errors in handleToolsCall
|
|
893
|
-
log(LOG_LEVELS.ERROR, `Unexpected error in handleToolsCall: ${error.message || 'Unknown error'}`);
|
|
894
|
-
log(LOG_LEVELS.DEBUG, `Error stack: ${error.stack || 'No stack trace'}`);
|
|
895
|
-
sendError(request.id, JSON_RPC_ERROR_CODES.INTERNAL_ERROR,
|
|
896
|
-
`Internal error in tool handler: ${error.message || 'Unknown error'}`, {
|
|
897
|
-
code: 'INTERNAL_ERROR',
|
|
898
|
-
toolName: request.params?.name
|
|
899
|
-
});
|
|
177
|
+
process.stdin.on('end', () => {
|
|
178
|
+
if (buffer.trim()) {
|
|
179
|
+
handleLine(buffer.trim());
|
|
900
180
|
}
|
|
901
|
-
}
|
|
181
|
+
});
|
|
902
182
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
*/
|
|
907
|
-
async function attemptAutoAuth() {
|
|
908
|
-
// Only attempt once per session
|
|
909
|
-
if (autoAuthAttempted || autoAuthInProgress) {
|
|
910
|
-
return false;
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
// Only in interactive mode (TTY) and not in CI
|
|
914
|
-
if (!canOpenBrowser()) {
|
|
915
|
-
return false;
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
// Check if NO_AUTO_AUTH is set
|
|
919
|
-
if (process.env.NO_AUTO_AUTH === '1' || process.env.NO_AUTO_AUTH === 'true') {
|
|
920
|
-
return false;
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
// Check if token already exists
|
|
924
|
-
const tokenInfo = await getAccessToken();
|
|
925
|
-
if (tokenInfo) {
|
|
926
|
-
return false;
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
autoAuthAttempted = true;
|
|
930
|
-
autoAuthInProgress = true;
|
|
931
|
-
|
|
932
|
-
try {
|
|
933
|
-
log(LOG_LEVELS.INFO, '🔐 First time setup - authentication required');
|
|
934
|
-
process.stderr.write('\n');
|
|
935
|
-
|
|
936
|
-
await interactiveLogin({
|
|
937
|
-
autoOpen: true,
|
|
938
|
-
showQR: false,
|
|
939
|
-
timeout: 120000,
|
|
940
|
-
apiBaseUrl: apiUrl,
|
|
941
|
-
appUrl: appUrl
|
|
942
|
-
});
|
|
943
|
-
|
|
944
|
-
autoAuthInProgress = false;
|
|
945
|
-
return true;
|
|
946
|
-
} catch (error) {
|
|
947
|
-
autoAuthInProgress = false;
|
|
948
|
-
log(LOG_LEVELS.WARN, `Auto-authentication failed: ${error.message}`);
|
|
949
|
-
log(LOG_LEVELS.INFO, 'You can authenticate manually: npx @meldocio/mcp-stdio-proxy@latest auth login');
|
|
950
|
-
return false;
|
|
951
|
-
}
|
|
952
|
-
}
|
|
183
|
+
process.stdin.on('error', (error) => {
|
|
184
|
+
// Silently handle stdin errors - normal when Claude Desktop closes connection
|
|
185
|
+
});
|
|
953
186
|
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
async function processSingleRequest(request) {
|
|
959
|
-
// Get access token with priority and auto-refresh
|
|
960
|
-
let tokenInfo = await getAccessToken();
|
|
961
|
-
|
|
962
|
-
// If no token and we haven't attempted auto-auth, try it
|
|
963
|
-
if (!tokenInfo && !autoAuthAttempted && !autoAuthInProgress) {
|
|
964
|
-
const authSucceeded = await attemptAutoAuth();
|
|
965
|
-
if (authSucceeded) {
|
|
966
|
-
// Retry getting token after successful auth
|
|
967
|
-
tokenInfo = await getAccessToken();
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
if (!tokenInfo) {
|
|
972
|
-
sendError(request.id, CUSTOM_ERROR_CODES.AUTH_REQUIRED,
|
|
973
|
-
'Meldoc token not found. Set MELDOC_ACCESS_TOKEN environment variable or run: npx @meldocio/mcp-stdio-proxy@latest auth login', {
|
|
974
|
-
code: 'AUTH_REQUIRED',
|
|
975
|
-
hint: 'Use meldoc.auth_login_instructions tool to get login command'
|
|
976
|
-
});
|
|
977
|
-
return;
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
try {
|
|
981
|
-
// Ensure request has jsonrpc field
|
|
982
|
-
const requestWithJsonRpc = {
|
|
983
|
-
...request,
|
|
984
|
-
jsonrpc: request.jsonrpc || '2.0'
|
|
985
|
-
};
|
|
986
|
-
|
|
987
|
-
log(LOG_LEVELS.DEBUG, `Forwarding request: ${request.method || 'unknown'}`);
|
|
988
|
-
|
|
989
|
-
// Prepare headers
|
|
990
|
-
const headers = {
|
|
991
|
-
'Authorization': `Bearer ${tokenInfo.token}`,
|
|
992
|
-
'Content-Type': 'application/json',
|
|
993
|
-
'User-Agent': `${pkg.name}/${pkg.version}`
|
|
994
|
-
};
|
|
995
|
-
|
|
996
|
-
// For tools/call: special handling
|
|
997
|
-
if (request.method === 'tools/call') {
|
|
998
|
-
const toolName = request.params?.name;
|
|
999
|
-
|
|
1000
|
-
// For list_workspaces: NEVER add workspace header
|
|
1001
|
-
// This tool should work without workspace header
|
|
1002
|
-
// Backend middleware should handle this specially
|
|
1003
|
-
if (toolName === 'list_workspaces') {
|
|
1004
|
-
log(LOG_LEVELS.DEBUG, `Skipping workspace header for ${toolName} tool`);
|
|
1005
|
-
// Explicitly don't add workspace header - this tool must work without it
|
|
1006
|
-
// Do nothing - headers will not include X-Meldoc-Workspace
|
|
1007
|
-
} else {
|
|
1008
|
-
// For other tools/call: don't add workspace header automatically
|
|
1009
|
-
// Backend will auto-select if user has only one workspace
|
|
1010
|
-
// If multiple workspaces, backend will return WORKSPACE_REQUIRED error
|
|
1011
|
-
log(LOG_LEVELS.DEBUG, `Tool ${toolName} - not adding workspace header automatically`);
|
|
1012
|
-
}
|
|
1013
|
-
} else {
|
|
1014
|
-
// For other methods: add workspace header if available (for backward compatibility)
|
|
1015
|
-
const workspaceAlias = resolveWorkspaceAlias(true);
|
|
1016
|
-
if (workspaceAlias) {
|
|
1017
|
-
headers['X-Meldoc-Workspace'] = workspaceAlias;
|
|
1018
|
-
log(LOG_LEVELS.DEBUG, `Added workspace header: ${workspaceAlias}`);
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
log(LOG_LEVELS.DEBUG, `Making request to ${rpcEndpoint}, method: ${request.method || 'unknown'}, headers: ${JSON.stringify(Object.keys(headers))}`);
|
|
1023
|
-
|
|
1024
|
-
// Make HTTP request to MCP API
|
|
1025
|
-
log(LOG_LEVELS.DEBUG, `POST ${rpcEndpoint} with body: ${JSON.stringify({
|
|
1026
|
-
method: requestWithJsonRpc.method,
|
|
1027
|
-
params: requestWithJsonRpc.params ? {
|
|
1028
|
-
name: requestWithJsonRpc.params.name,
|
|
1029
|
-
arguments: requestWithJsonRpc.params.arguments
|
|
1030
|
-
} : undefined
|
|
1031
|
-
})}`);
|
|
1032
|
-
|
|
1033
|
-
const response = await axios.post(rpcEndpoint, requestWithJsonRpc, {
|
|
1034
|
-
headers,
|
|
1035
|
-
timeout: REQUEST_TIMEOUT,
|
|
1036
|
-
validateStatus: (status) => status < 500, // Don't throw on 4xx errors
|
|
1037
|
-
// Keep connection alive for better performance
|
|
1038
|
-
httpsAgent: new https.Agent({ keepAlive: true, keepAliveMsecs: 1000 })
|
|
1039
|
-
});
|
|
1040
|
-
|
|
1041
|
-
log(LOG_LEVELS.DEBUG, `Response status: ${response.status}, data keys: ${JSON.stringify(Object.keys(response.data || {}))}`);
|
|
1042
|
-
log(LOG_LEVELS.DEBUG, `Full response data: ${JSON.stringify(response.data)}`);
|
|
1043
|
-
|
|
1044
|
-
// Handle successful response
|
|
1045
|
-
if (response.status >= 200 && response.status < 300) {
|
|
1046
|
-
const responseData = response.data;
|
|
1047
|
-
|
|
1048
|
-
// Ensure response has jsonrpc and id if original request had id
|
|
1049
|
-
if (responseData && typeof responseData === 'object') {
|
|
1050
|
-
if (!responseData.jsonrpc) {
|
|
1051
|
-
responseData.jsonrpc = '2.0';
|
|
1052
|
-
}
|
|
1053
|
-
if (request.id !== undefined && responseData.id === undefined) {
|
|
1054
|
-
responseData.id = request.id;
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
// Check for WORKSPACE_REQUIRED error
|
|
1059
|
-
if (responseData.error) {
|
|
1060
|
-
const errorCode = responseData.error.code;
|
|
1061
|
-
const errorMessage = responseData.error.message || '';
|
|
1062
|
-
const errorData = responseData.error.data || {};
|
|
1063
|
-
|
|
1064
|
-
// Get tool name from request (check both original and modified request)
|
|
1065
|
-
const toolName = request.params?.name || requestWithJsonRpc.params?.name;
|
|
1066
|
-
|
|
1067
|
-
log(LOG_LEVELS.DEBUG, `Error response: code=${errorCode} (type: ${typeof errorCode}), message="${errorMessage}", toolName=${toolName}, errorData=${JSON.stringify(errorData)}`);
|
|
1068
|
-
log(LOG_LEVELS.DEBUG, `Full error response: ${JSON.stringify(responseData.error)}`);
|
|
1069
|
-
|
|
1070
|
-
// Check if error message contains "Multiple workspaces available" (backend may return this with different error codes)
|
|
1071
|
-
// Backend may return code as number (-32000) or string ('WORKSPACE_REQUIRED')
|
|
1072
|
-
const errorMsgStr = String(errorMessage || '');
|
|
1073
|
-
const hasWorkspaceMessage = errorMsgStr.includes('Multiple workspaces available') ||
|
|
1074
|
-
errorMsgStr.includes('Specify workspace');
|
|
1075
|
-
|
|
1076
|
-
const isWorkspaceRequired = errorCode === 'WORKSPACE_REQUIRED' ||
|
|
1077
|
-
errorData.code === 'WORKSPACE_REQUIRED' ||
|
|
1078
|
-
(errorCode === JSON_RPC_ERROR_CODES.SERVER_ERROR && hasWorkspaceMessage);
|
|
1079
|
-
|
|
1080
|
-
log(LOG_LEVELS.DEBUG, `Workspace check: isWorkspaceRequired=${isWorkspaceRequired}, hasWorkspaceMessage=${hasWorkspaceMessage}, errorMsgStr="${errorMsgStr}"`);
|
|
1081
|
-
|
|
1082
|
-
if (isWorkspaceRequired) {
|
|
1083
|
-
log(LOG_LEVELS.DEBUG, `Detected WORKSPACE_REQUIRED error for tool: ${toolName}`);
|
|
1084
|
-
|
|
1085
|
-
// Special handling for tools that should work without workspace
|
|
1086
|
-
if (toolName === 'list_workspaces') {
|
|
1087
|
-
// This is a backend issue - this tool should work without workspace header
|
|
1088
|
-
// But we can still provide helpful message
|
|
1089
|
-
const message = `Backend requires workspace selection even for ${toolName}. Please set a default workspace using set_workspace tool first, or contact support if this persists.`;
|
|
1090
|
-
sendError(request.id, JSON_RPC_ERROR_CODES.SERVER_ERROR, message, {
|
|
1091
|
-
code: 'WORKSPACE_REQUIRED',
|
|
1092
|
-
hint: 'Try setting a default workspace first using set_workspace tool, or specify workspaceAlias/workspaceId in the tool call arguments.'
|
|
1093
|
-
});
|
|
1094
|
-
return;
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
const message = 'Multiple workspaces available. Use list_workspaces tool to get list, then use set_workspace to set default workspace, or specify workspaceAlias or workspaceId parameter in tool call.';
|
|
1098
|
-
sendError(request.id, JSON_RPC_ERROR_CODES.SERVER_ERROR, message, {
|
|
1099
|
-
code: 'WORKSPACE_REQUIRED',
|
|
1100
|
-
hint: 'Use list_workspaces tool to get available workspaces, then use set_workspace to set default, or specify workspaceAlias or workspaceId in tool call.'
|
|
1101
|
-
});
|
|
1102
|
-
return;
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
// Check for AUTH_REQUIRED error
|
|
1106
|
-
if (errorCode === 'AUTH_REQUIRED' || errorData.code === 'AUTH_REQUIRED') {
|
|
1107
|
-
const message = 'Authentication required. Run: npx @meldocio/mcp-stdio-proxy@latest auth login';
|
|
1108
|
-
sendError(request.id, CUSTOM_ERROR_CODES.AUTH_REQUIRED, message, {
|
|
1109
|
-
code: 'AUTH_REQUIRED',
|
|
1110
|
-
hint: 'Use auth_login_instructions tool to get login command'
|
|
1111
|
-
});
|
|
1112
|
-
return;
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
// If error was not handled above, forward it as-is
|
|
1116
|
-
log(LOG_LEVELS.DEBUG, `Forwarding unhandled error: ${JSON.stringify(responseData.error)}`);
|
|
1117
|
-
// Forward the error response as-is
|
|
1118
|
-
process.stdout.write(JSON.stringify(responseData) + '\n');
|
|
1119
|
-
if (process.stdout.isTTY) {
|
|
1120
|
-
process.stdout.flush();
|
|
1121
|
-
}
|
|
1122
|
-
return;
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
// If there's an error that we handled, we already sent a response, so return
|
|
1126
|
-
if (responseData.error) {
|
|
1127
|
-
return;
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
// Success response - ensure stdout is flushed immediately
|
|
1131
|
-
process.stdout.write(JSON.stringify(responseData) + '\n');
|
|
1132
|
-
// Flush stdout to ensure data is sent immediately
|
|
1133
|
-
if (process.stdout.isTTY) {
|
|
1134
|
-
process.stdout.flush();
|
|
1135
|
-
}
|
|
1136
|
-
} else {
|
|
1137
|
-
// HTTP error status (400, 401, 404, etc.)
|
|
1138
|
-
log(LOG_LEVELS.DEBUG, `HTTP error status ${response.status}, full response: ${JSON.stringify(response.data)}`);
|
|
1139
|
-
|
|
1140
|
-
// Try to extract error information from different possible formats
|
|
1141
|
-
// Format 1: JSON-RPC error format { error: { code, message, data } }
|
|
1142
|
-
// Format 2: Direct error format { code, message, details, error }
|
|
1143
|
-
// Format 3: Simple error format { message }
|
|
1144
|
-
|
|
1145
|
-
const responseData = response.data || {};
|
|
1146
|
-
const errorMessage = responseData.error?.message ||
|
|
1147
|
-
responseData.message ||
|
|
1148
|
-
`HTTP ${response.status}: ${response.statusText}`;
|
|
1149
|
-
|
|
1150
|
-
// Check for WORKSPACE_REQUIRED in various places
|
|
1151
|
-
const errorData = responseData.error?.data || responseData.details || {};
|
|
1152
|
-
const errorMsg = responseData.error?.message || responseData.message || '';
|
|
1153
|
-
const errorCode = responseData.error?.code || responseData.code;
|
|
1154
|
-
|
|
1155
|
-
log(LOG_LEVELS.DEBUG, `HTTP error details: errorCode=${errorCode}, errorMsg="${errorMsg}", errorData=${JSON.stringify(errorData)}`);
|
|
1156
|
-
|
|
1157
|
-
const isWorkspaceRequired = errorCode === 'WORKSPACE_REQUIRED' ||
|
|
1158
|
-
errorData.code === 'WORKSPACE_REQUIRED' ||
|
|
1159
|
-
errorMsg.includes('Multiple workspaces available') ||
|
|
1160
|
-
errorMsg.includes('Specify workspace') ||
|
|
1161
|
-
errorMsg.includes('workspace selection') ||
|
|
1162
|
-
errorMsg.includes('workspace slug') ||
|
|
1163
|
-
errorMsg.includes('workspaceId') ||
|
|
1164
|
-
errorMsg.includes('workspaceAlias');
|
|
1165
|
-
|
|
1166
|
-
if (isWorkspaceRequired) {
|
|
1167
|
-
// Get tool name from request
|
|
1168
|
-
const toolName = request.params?.name;
|
|
1169
|
-
|
|
1170
|
-
log(LOG_LEVELS.DEBUG, `Detected WORKSPACE_REQUIRED for tool: ${toolName}`);
|
|
1171
|
-
|
|
1172
|
-
// Special handling for tools that should work without workspace
|
|
1173
|
-
if (toolName === 'list_workspaces') {
|
|
1174
|
-
// For this tool, backend should not require workspace
|
|
1175
|
-
// Log the actual backend error for debugging
|
|
1176
|
-
log(LOG_LEVELS.WARN, `Backend returned workspace requirement for ${toolName} tool. Backend error: ${errorMsg}`);
|
|
1177
|
-
const message = `Backend requires workspace selection even for ${toolName}. This may indicate a backend configuration issue. Backend error: ${errorMsg}`;
|
|
1178
|
-
sendError(request.id, JSON_RPC_ERROR_CODES.SERVER_ERROR, message, {
|
|
1179
|
-
code: 'WORKSPACE_REQUIRED',
|
|
1180
|
-
hint: 'This tool should work without workspace. Please contact support or check backend configuration.',
|
|
1181
|
-
backendError: errorMsg,
|
|
1182
|
-
backendCode: errorCode
|
|
1183
|
-
});
|
|
1184
|
-
return;
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
const message = 'Multiple workspaces available. Use list_workspaces tool to get list, then use set_workspace to set default workspace, or specify workspaceAlias or workspaceId parameter in tool call.';
|
|
1188
|
-
sendError(request.id, JSON_RPC_ERROR_CODES.SERVER_ERROR, message, {
|
|
1189
|
-
code: 'WORKSPACE_REQUIRED',
|
|
1190
|
-
hint: 'Use list_workspaces tool to get available workspaces, then use set_workspace to set default, or specify workspaceAlias or workspaceId in tool call.'
|
|
1191
|
-
});
|
|
1192
|
-
return;
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
// Check for AUTH_REQUIRED
|
|
1196
|
-
if (errorCode === 'AUTH_REQUIRED' || errorData.code === 'AUTH_REQUIRED') {
|
|
1197
|
-
const message = 'Authentication required. Run: npx @meldocio/mcp-stdio-proxy@latest auth login';
|
|
1198
|
-
sendError(request.id, CUSTOM_ERROR_CODES.AUTH_REQUIRED, message, {
|
|
1199
|
-
code: 'AUTH_REQUIRED',
|
|
1200
|
-
hint: 'Use auth_login_instructions tool to get login command'
|
|
1201
|
-
});
|
|
1202
|
-
return;
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
// Forward the error as-is, but ensure JSON-RPC format
|
|
1206
|
-
// If response is already in JSON-RPC format, forward it
|
|
1207
|
-
if (responseData.jsonrpc && responseData.error) {
|
|
1208
|
-
process.stdout.write(JSON.stringify(responseData) + '\n');
|
|
1209
|
-
if (process.stdout.isTTY) {
|
|
1210
|
-
process.stdout.flush();
|
|
1211
|
-
}
|
|
1212
|
-
} else {
|
|
1213
|
-
// Convert to JSON-RPC format
|
|
1214
|
-
sendError(request.id, JSON_RPC_ERROR_CODES.SERVER_ERROR, errorMessage, {
|
|
1215
|
-
status: response.status,
|
|
1216
|
-
code: errorCode,
|
|
1217
|
-
details: errorData
|
|
1218
|
-
});
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
} catch (error) {
|
|
1222
|
-
// Handle different types of errors
|
|
1223
|
-
if (error.response) {
|
|
1224
|
-
// Server responded with error status
|
|
1225
|
-
const status = error.response.status;
|
|
1226
|
-
const errorData = error.response.data;
|
|
1227
|
-
const errorMessage = errorData?.error?.message ||
|
|
1228
|
-
errorData?.message ||
|
|
1229
|
-
`HTTP ${status}: ${error.response.statusText}`;
|
|
1230
|
-
|
|
1231
|
-
let errorCode = JSON_RPC_ERROR_CODES.SERVER_ERROR;
|
|
1232
|
-
if (status === 401) {
|
|
1233
|
-
errorCode = CUSTOM_ERROR_CODES.AUTH_REQUIRED;
|
|
1234
|
-
} else if (status === 404) {
|
|
1235
|
-
errorCode = CUSTOM_ERROR_CODES.NOT_FOUND;
|
|
1236
|
-
} else if (status === 429) {
|
|
1237
|
-
errorCode = CUSTOM_ERROR_CODES.RATE_LIMIT;
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
// Check for WORKSPACE_REQUIRED
|
|
1241
|
-
const errorDataCode = errorData?.error?.code || errorData?.code;
|
|
1242
|
-
const errorMsgText = errorData?.error?.message || errorData?.message || errorMessage || '';
|
|
1243
|
-
const isWorkspaceRequired = errorDataCode === 'WORKSPACE_REQUIRED' ||
|
|
1244
|
-
errorMsgText.includes('Multiple workspaces available');
|
|
1245
|
-
|
|
1246
|
-
if (isWorkspaceRequired) {
|
|
1247
|
-
// Get tool name from request
|
|
1248
|
-
const toolName = request.params?.name;
|
|
1249
|
-
|
|
1250
|
-
// Special handling for tools that should work without workspace
|
|
1251
|
-
if (toolName === 'list_workspaces') {
|
|
1252
|
-
const message = `Backend requires workspace selection even for ${toolName}. Please set a default workspace using set_workspace tool first, or contact support if this persists.`;
|
|
1253
|
-
sendError(request.id, JSON_RPC_ERROR_CODES.SERVER_ERROR, message, {
|
|
1254
|
-
code: 'WORKSPACE_REQUIRED',
|
|
1255
|
-
hint: 'Try setting a default workspace first using set_workspace tool, or specify workspaceAlias/workspaceId in the tool call arguments.'
|
|
1256
|
-
});
|
|
1257
|
-
return;
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
const message = 'Multiple workspaces available. Use list_workspaces tool to get list, then use set_workspace to set default workspace, or specify workspaceAlias or workspaceId parameter in tool call.';
|
|
1261
|
-
sendError(request.id, JSON_RPC_ERROR_CODES.SERVER_ERROR, message, {
|
|
1262
|
-
code: 'WORKSPACE_REQUIRED',
|
|
1263
|
-
hint: 'Use list_workspaces tool to get available workspaces, then use set_workspace to set default, or specify workspaceAlias or workspaceId in tool call.'
|
|
1264
|
-
});
|
|
1265
|
-
return;
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
// Check for AUTH_REQUIRED
|
|
1269
|
-
if (errorDataCode === 'AUTH_REQUIRED') {
|
|
1270
|
-
const message = 'Authentication required. Run: npx @meldocio/mcp-stdio-proxy@latest auth login';
|
|
1271
|
-
sendError(request.id, CUSTOM_ERROR_CODES.AUTH_REQUIRED, message, {
|
|
1272
|
-
code: 'AUTH_REQUIRED',
|
|
1273
|
-
hint: 'Use auth_login_instructions tool to get login command'
|
|
1274
|
-
});
|
|
1275
|
-
return;
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
log(LOG_LEVELS.WARN, `HTTP error ${status}: ${errorMessage}`);
|
|
1279
|
-
sendError(request.id, errorCode, errorMessage, {
|
|
1280
|
-
status,
|
|
1281
|
-
code: errorData?.error?.code || errorDataCode || `HTTP_${status}`
|
|
1282
|
-
});
|
|
1283
|
-
} else if (error.request) {
|
|
1284
|
-
// Request was made but no response received
|
|
1285
|
-
log(LOG_LEVELS.ERROR, `Network error: ${error.message || 'No response from server'}`);
|
|
1286
|
-
sendError(request.id, JSON_RPC_ERROR_CODES.INTERNAL_ERROR,
|
|
1287
|
-
`Network error: ${error.message || 'No response from server'}`, {
|
|
1288
|
-
code: 'NETWORK_ERROR'
|
|
1289
|
-
});
|
|
1290
|
-
} else if (error.code === 'ECONNABORTED') {
|
|
1291
|
-
// Timeout
|
|
1292
|
-
log(LOG_LEVELS.WARN, `Request timeout after ${REQUEST_TIMEOUT}ms`);
|
|
1293
|
-
sendError(request.id, JSON_RPC_ERROR_CODES.INTERNAL_ERROR,
|
|
1294
|
-
`Request timeout after ${REQUEST_TIMEOUT}ms`, {
|
|
1295
|
-
code: 'TIMEOUT'
|
|
1296
|
-
});
|
|
1297
|
-
} else {
|
|
1298
|
-
// Other errors
|
|
1299
|
-
log(LOG_LEVELS.ERROR, `Internal error: ${error.message || 'Unknown error'}`);
|
|
1300
|
-
log(LOG_LEVELS.DEBUG, `Error stack: ${error.stack || 'No stack trace'}`);
|
|
1301
|
-
sendError(request.id, JSON_RPC_ERROR_CODES.INTERNAL_ERROR,
|
|
1302
|
-
`Internal error: ${error.message || 'Unknown error'}`, {
|
|
1303
|
-
code: 'INTERNAL_ERROR'
|
|
1304
|
-
});
|
|
1305
|
-
}
|
|
187
|
+
process.stdout.on('error', (error) => {
|
|
188
|
+
// Exit gracefully on EPIPE (stdout closed)
|
|
189
|
+
if (error.code === 'EPIPE') {
|
|
190
|
+
process.exit(0);
|
|
1306
191
|
}
|
|
1307
|
-
}
|
|
192
|
+
});
|
|
1308
193
|
|
|
1309
194
|
/**
|
|
1310
|
-
*
|
|
195
|
+
* Graceful shutdown handling
|
|
1311
196
|
*/
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
if (
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
const errorResponse = {
|
|
1322
|
-
jsonrpc: '2.0',
|
|
1323
|
-
id: id,
|
|
1324
|
-
error: {
|
|
1325
|
-
code: code,
|
|
1326
|
-
message: message
|
|
1327
|
-
}
|
|
1328
|
-
};
|
|
1329
|
-
|
|
1330
|
-
// Add details if provided (for debugging)
|
|
1331
|
-
if (details && LOG_LEVEL >= LOG_LEVELS.DEBUG) {
|
|
1332
|
-
errorResponse.error.data = details;
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
process.stdout.write(JSON.stringify(errorResponse) + '\n');
|
|
1336
|
-
// Flush stdout to ensure data is sent immediately
|
|
1337
|
-
if (process.stdout.isTTY) {
|
|
1338
|
-
process.stdout.flush();
|
|
1339
|
-
}
|
|
197
|
+
let isShuttingDown = false;
|
|
198
|
+
|
|
199
|
+
function gracefulShutdown(signal) {
|
|
200
|
+
if (isShuttingDown) return;
|
|
201
|
+
isShuttingDown = true;
|
|
202
|
+
log(LOG_LEVELS.INFO, `Received ${signal}, shutting down gracefully...`);
|
|
203
|
+
setTimeout(() => process.exit(0), 100);
|
|
1340
204
|
}
|
|
205
|
+
|
|
206
|
+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
207
|
+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
208
|
+
|
|
209
|
+
// Start message
|
|
210
|
+
log(LOG_LEVELS.DEBUG, 'Meldoc MCP Stdio Proxy started');
|