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