@meldocio/mcp-stdio-proxy 1.0.5 → 1.0.6

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