@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.
@@ -1,337 +1,109 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const axios = require('axios');
4
- const https = require('https');
5
- const { URL } = require('url');
6
- const path = require('path');
7
- const fs = require('fs');
8
- const chalk = require('chalk');
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
- // Handle CLI commands - cli.js will handle and exit
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
- // Get package info - try multiple paths for different installation scenarios
24
- let pkg;
25
- try {
26
- // Try relative path first (development)
27
- pkg = require('../package.json');
28
- } catch (e) {
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
- process.on('SIGINT', () => gracefulShutdown('SIGINT'));
187
- process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
38
+ const LOG_LEVEL = getLogLevel();
188
39
 
189
40
  /**
190
- * Handle a single line from stdin
41
+ * Log message to stderr
191
42
  */
192
- function handleLine(line) {
193
- // Skip empty lines
194
- if (!line || !line.trim()) {
195
- return;
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
- * Validate JSON-RPC request
51
+ * Process single request to backend
212
52
  */
213
- function validateRequest(request) {
214
- if (!request || typeof request !== 'object') {
215
- return { valid: false, error: 'Request must be an object' };
216
- }
217
-
218
- // Allow requests without jsonrpc for compatibility (some MCP clients may omit it)
219
- if (request.jsonrpc && request.jsonrpc !== '2.0') {
220
- return { valid: false, error: 'jsonrpc must be "2.0"' };
221
- }
222
-
223
- // Allow requests without method if they're notifications (id is null/undefined)
224
- // But batch requests must be arrays
225
- if (!request.method && !Array.isArray(request) && request.id !== null && request.id !== undefined) {
226
- return { valid: false, error: 'Request must have a method or be a batch array' };
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
- // Handle null/undefined requests
238
- if (!request) {
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 (array of requests)
245
- if (Array.isArray(request)) {
246
- // Process batch requests sequentially
247
- for (const req of request) {
248
- if (req) {
249
- try {
250
- // Check if this is a protocol method that should be handled locally
251
- const method = req.method;
252
- if (method === 'initialize') {
253
- handleInitialize(req);
254
- } else if (method === 'initialized' || method === 'notifications/initialized') {
255
- // Notification - no response needed
256
- continue;
257
- } else if (method === 'notifications/cancelled') {
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
- return;
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
- `Internal error: ${error.message || 'Unknown error'}`, {
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 MCP initialize method
343
- * This is called by Claude Desktop to establish the connection
114
+ * Handle a single JSON-RPC request
344
115
  */
345
- function handleInitialize(request) {
346
- const response = {
347
- jsonrpc: '2.0',
348
- id: request.id,
349
- result: {
350
- protocolVersion: '2025-06-18',
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
- * Handle MCP ping method (keep-alive)
371
- */
372
- function handlePing(request) {
373
- const response = {
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
- * Handle resources/list method
387
- * Returns empty list as resources are not supported yet
388
- */
389
- function handleResourcesList(request) {
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
- * Get static list of all available tools
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 tools/list method
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 handleToolsList(request) {
147
+ function handleLine(line) {
148
+ if (!line || !line.trim()) return;
149
+
627
150
  try {
628
- const tools = getToolsList();
629
-
630
- // Log tool names for debugging
631
- const toolNames = tools.map(t => t.name).join(', ');
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
- * Handle tools/call method
669
- * Checks if it's a local tool first, otherwise forwards to backend
159
+ * Setup stdin/stdout handling
670
160
  */
671
- async function handleToolsCall(request) {
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
- // Start interactive login
836
- await interactiveLogin({
837
- autoOpen: true,
838
- showQR: false,
839
- timeout: timeout,
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
- const response = {
845
- jsonrpc: '2.0',
846
- id: request.id,
847
- result: {
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
- // All other tools are forwarded to backend
889
- log(LOG_LEVELS.DEBUG, `Forwarding tool ${toolName} to backend (not a local tool)`);
890
- await processSingleRequest(request);
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
- * Attempt automatic authentication if conditions are met
905
- * @returns {Promise<boolean>} True if authentication was attempted and succeeded
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
- * Process a single JSON-RPC request
956
- * Forwards the request to the backend MCP API
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
- * Send JSON-RPC error response
195
+ * Graceful shutdown handling
1311
196
  */
1312
- function sendError(id, code, message, details) {
1313
- // Only send error response if id is defined (not for notifications)
1314
- // Claude Desktop's Zod schema doesn't accept null for id
1315
- if (id === undefined || id === null) {
1316
- // For notifications or parse errors without id, don't send response
1317
- // or send without id field
1318
- return;
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');