@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 +327 -0
- package/bin/meldoc-mcp-proxy.js +761 -68
- package/package.json +1 -1
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
|
+
};
|
package/bin/meldoc-mcp-proxy.js
CHANGED
|
@@ -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
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
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
|
|
99
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
|
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
|
-
//
|
|
350
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
401
|
-
|
|
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
|
-
|
|
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'
|