@meldocio/mcp-stdio-proxy 1.0.22 → 1.0.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,195 @@
1
+ /**
2
+ * HTTP Error Handler
3
+ *
4
+ * Handles workspace-required and authentication errors from backend.
5
+ * Consolidates error handling logic that was duplicated 3+ times in proxy.js
6
+ */
7
+
8
+ const { JSON_RPC_ERROR_CODES, CUSTOM_ERROR_CODES } = require('../protocol/error-codes');
9
+ const { sendError, writeResponse } = require('../protocol/json-rpc');
10
+ const { LOG_LEVELS } = require('../core/constants');
11
+
12
+ /**
13
+ * Get current log level
14
+ */
15
+ function getLogLevel() {
16
+ const level = (process.env.LOG_LEVEL || 'ERROR').toUpperCase();
17
+ return LOG_LEVELS[level] !== undefined ? LOG_LEVELS[level] : LOG_LEVELS.ERROR;
18
+ }
19
+
20
+ const LOG_LEVEL = getLogLevel();
21
+
22
+ /**
23
+ * Log message based on level
24
+ */
25
+ function log(level, message) {
26
+ if (LOG_LEVEL >= level) {
27
+ const levelName = Object.keys(LOG_LEVELS)[level] || 'UNKNOWN';
28
+ process.stderr.write(`[${levelName}] ${message}\n`);
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Check if error indicates workspace is required
34
+ * @param {Object} error - Error object from response
35
+ * @returns {boolean} True if workspace required
36
+ */
37
+ function isWorkspaceRequiredError(error) {
38
+ if (!error) return false;
39
+
40
+ const errorCode = error.code;
41
+ const errorMessage = String(error.message || '');
42
+ const errorData = error.data || {};
43
+
44
+ // Check for explicit workspace required code
45
+ if (errorCode === 'WORKSPACE_REQUIRED' || errorData.code === 'WORKSPACE_REQUIRED') {
46
+ return true;
47
+ }
48
+
49
+ // Check for workspace message in error
50
+ const hasWorkspaceMessage =
51
+ errorMessage.includes('Multiple workspaces available') ||
52
+ errorMessage.includes('Specify workspace');
53
+
54
+ // Server error with workspace message
55
+ if (errorCode === JSON_RPC_ERROR_CODES.SERVER_ERROR && hasWorkspaceMessage) {
56
+ return true;
57
+ }
58
+
59
+ return false;
60
+ }
61
+
62
+ /**
63
+ * Check if error indicates authentication is required
64
+ * @param {Object} error - Error object from response
65
+ * @returns {boolean} True if auth required
66
+ */
67
+ function isAuthRequiredError(error) {
68
+ if (!error) return false;
69
+
70
+ const errorCode = error.code;
71
+ const errorData = error.data || {};
72
+
73
+ return errorCode === 'AUTH_REQUIRED' || errorData.code === 'AUTH_REQUIRED';
74
+ }
75
+
76
+ /**
77
+ * Handle workspace required error
78
+ * @param {string|number} requestId - Request ID
79
+ * @param {string} [toolName] - Tool name being called
80
+ */
81
+ function handleWorkspaceRequiredError(requestId, toolName) {
82
+ log(LOG_LEVELS.DEBUG, `Detected WORKSPACE_REQUIRED error for tool: ${toolName || 'unknown'}`);
83
+
84
+ // Special handling for list_workspaces tool
85
+ if (toolName === 'list_workspaces') {
86
+ 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.`;
87
+ sendError(requestId, JSON_RPC_ERROR_CODES.SERVER_ERROR, message, {
88
+ code: 'WORKSPACE_REQUIRED',
89
+ hint: 'Try setting a default workspace first using set_workspace tool, or specify workspaceAlias/workspaceId in the tool call arguments.'
90
+ });
91
+ return;
92
+ }
93
+
94
+ // General workspace required message
95
+ 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.';
96
+ sendError(requestId, JSON_RPC_ERROR_CODES.SERVER_ERROR, message, {
97
+ code: 'WORKSPACE_REQUIRED',
98
+ hint: 'Use list_workspaces tool to get available workspaces, then use set_workspace to set default, or specify workspaceAlias or workspaceId in tool call.'
99
+ });
100
+ }
101
+
102
+ /**
103
+ * Handle authentication required error
104
+ * @param {string|number} requestId - Request ID
105
+ */
106
+ function handleAuthRequiredError(requestId) {
107
+ const message = 'Authentication required. Run: npx @meldocio/mcp-stdio-proxy@latest auth login';
108
+ sendError(requestId, CUSTOM_ERROR_CODES.AUTH_REQUIRED, message, {
109
+ code: 'AUTH_REQUIRED',
110
+ hint: 'Use auth_login_instructions tool to get login command'
111
+ });
112
+ }
113
+
114
+ /**
115
+ * Process backend response and handle special errors
116
+ * @param {Object} response - Axios response object
117
+ * @param {Object} request - Original request
118
+ * @returns {boolean} True if error was handled, false if should be forwarded
119
+ */
120
+ function handleBackendResponse(response, request) {
121
+ const responseData = response.data || {};
122
+
123
+ // Successful HTTP response (200, 201, etc.)
124
+ if (response.status >= 200 && response.status < 300) {
125
+ // Ensure response has id
126
+ if (responseData && !responseData.id && request.id !== undefined) {
127
+ responseData.id = request.id;
128
+ }
129
+
130
+ // Check for error in response data
131
+ if (responseData.error) {
132
+ const error = responseData.error;
133
+ const toolName = request.params?.name;
134
+
135
+ log(LOG_LEVELS.DEBUG, `Error response: code=${error.code}, message="${error.message}", toolName=${toolName}`);
136
+
137
+ // Check for workspace required
138
+ if (isWorkspaceRequiredError(error)) {
139
+ handleWorkspaceRequiredError(request.id, toolName);
140
+ return true; // Handled
141
+ }
142
+
143
+ // Check for auth required
144
+ if (isAuthRequiredError(error)) {
145
+ handleAuthRequiredError(request.id);
146
+ return true; // Handled
147
+ }
148
+
149
+ // Forward unhandled errors as-is
150
+ log(LOG_LEVELS.DEBUG, `Forwarding unhandled error: ${JSON.stringify(error)}`);
151
+ writeResponse(responseData);
152
+ return true; // Handled (forwarded)
153
+ }
154
+
155
+ // Success response - forward as-is
156
+ writeResponse(responseData);
157
+ return true; // Handled
158
+ }
159
+
160
+ // HTTP error status (400, 401, 404, etc.)
161
+ log(LOG_LEVELS.DEBUG, `HTTP error status ${response.status}`);
162
+
163
+ const errorMessage = responseData.error?.message ||
164
+ responseData.message ||
165
+ `HTTP ${response.status}: ${response.statusText}`;
166
+
167
+ // Check for workspace/auth errors in HTTP error responses
168
+ const error = responseData.error || responseData;
169
+
170
+ if (isWorkspaceRequiredError(error)) {
171
+ const toolName = request.params?.name;
172
+ handleWorkspaceRequiredError(request.id, toolName);
173
+ return true;
174
+ }
175
+
176
+ if (isAuthRequiredError(error)) {
177
+ handleAuthRequiredError(request.id);
178
+ return true;
179
+ }
180
+
181
+ // Forward other HTTP errors
182
+ sendError(request.id, JSON_RPC_ERROR_CODES.SERVER_ERROR, errorMessage, {
183
+ httpStatus: response.status,
184
+ code: error.code || 'HTTP_ERROR'
185
+ });
186
+ return true;
187
+ }
188
+
189
+ module.exports = {
190
+ isWorkspaceRequiredError,
191
+ isAuthRequiredError,
192
+ handleWorkspaceRequiredError,
193
+ handleAuthRequiredError,
194
+ handleBackendResponse
195
+ };
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Configuration File Manager
3
+ *
4
+ * Handles reading, writing, and validating MCP configuration files.
5
+ * Provides safe file operations with error handling.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ /**
12
+ * Check if a file exists
13
+ * @param {string} filePath - Path to file
14
+ * @returns {boolean} True if file exists
15
+ */
16
+ function fileExists(filePath) {
17
+ try {
18
+ return fs.existsSync(filePath);
19
+ } catch (error) {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Read and parse JSON configuration file
26
+ * @param {string} filePath - Path to config file
27
+ * @returns {Object|null} Parsed configuration object, or null if file doesn't exist or parse fails
28
+ */
29
+ function readConfig(filePath) {
30
+ try {
31
+ if (!fileExists(filePath)) {
32
+ return null;
33
+ }
34
+
35
+ const content = fs.readFileSync(filePath, 'utf8');
36
+ return JSON.parse(content);
37
+ } catch (error) {
38
+ // Return null on any error (file not found, parse error, etc.)
39
+ return null;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Write configuration object to JSON file
45
+ * Creates parent directories if they don't exist
46
+ * @param {string} filePath - Path to config file
47
+ * @param {Object} config - Configuration object to write
48
+ * @throws {Error} If write fails
49
+ */
50
+ function writeConfig(filePath, config) {
51
+ const dir = path.dirname(filePath);
52
+
53
+ // Create directory if it doesn't exist
54
+ if (!fileExists(dir)) {
55
+ fs.mkdirSync(dir, { recursive: true });
56
+ }
57
+
58
+ // Write config file with pretty formatting
59
+ const content = JSON.stringify(config, null, 2);
60
+ fs.writeFileSync(filePath, content, 'utf8');
61
+ }
62
+
63
+ /**
64
+ * Safely read configuration with fallback to empty object
65
+ * @param {string} filePath - Path to config file
66
+ * @returns {Object} Configuration object (empty if file doesn't exist or is invalid)
67
+ */
68
+ function readConfigSafe(filePath) {
69
+ const config = readConfig(filePath);
70
+ return config || {};
71
+ }
72
+
73
+ /**
74
+ * Backup configuration file before modification
75
+ * @param {string} filePath - Path to config file
76
+ * @returns {string|null} Path to backup file, or null if backup failed
77
+ */
78
+ function backupConfig(filePath) {
79
+ try {
80
+ if (!fileExists(filePath)) {
81
+ return null;
82
+ }
83
+
84
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
85
+ const backupPath = `${filePath}.backup-${timestamp}`;
86
+
87
+ fs.copyFileSync(filePath, backupPath);
88
+ return backupPath;
89
+ } catch (error) {
90
+ return null;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Validate that config is a valid JSON object
96
+ * @param {any} config - Configuration to validate
97
+ * @returns {boolean} True if valid
98
+ */
99
+ function isValidConfig(config) {
100
+ return config !== null && typeof config === 'object' && !Array.isArray(config);
101
+ }
102
+
103
+ /**
104
+ * Get file modification time
105
+ * @param {string} filePath - Path to file
106
+ * @returns {Date|null} Modification time, or null if file doesn't exist
107
+ */
108
+ function getFileModTime(filePath) {
109
+ try {
110
+ if (!fileExists(filePath)) {
111
+ return null;
112
+ }
113
+ const stats = fs.statSync(filePath);
114
+ return stats.mtime;
115
+ } catch (error) {
116
+ return null;
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Ensure parent directory exists for a file path
122
+ * @param {string} filePath - Path to file
123
+ */
124
+ function ensureDirectoryExists(filePath) {
125
+ const dir = path.dirname(filePath);
126
+ if (!fileExists(dir)) {
127
+ fs.mkdirSync(dir, { recursive: true });
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Delete configuration file
133
+ * @param {string} filePath - Path to config file
134
+ * @returns {boolean} True if file was deleted, false if it didn't exist
135
+ */
136
+ function deleteConfig(filePath) {
137
+ try {
138
+ if (!fileExists(filePath)) {
139
+ return false;
140
+ }
141
+ fs.unlinkSync(filePath);
142
+ return true;
143
+ } catch (error) {
144
+ throw new Error(`Failed to delete config file: ${error.message}`);
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Read, modify, and write config in a single operation
150
+ * @param {string} filePath - Path to config file
151
+ * @param {Function} modifier - Function that takes config and returns modified config
152
+ * @param {boolean} [createBackup=false] - Whether to create backup before writing
153
+ * @returns {Object} { success: boolean, backupPath?: string, error?: string }
154
+ */
155
+ function modifyConfig(filePath, modifier, createBackup = false) {
156
+ try {
157
+ // Read existing config
158
+ const config = readConfigSafe(filePath);
159
+
160
+ // Apply modifications
161
+ const modifiedConfig = modifier(config);
162
+
163
+ // Validate result
164
+ if (!isValidConfig(modifiedConfig)) {
165
+ return {
166
+ success: false,
167
+ error: 'Modified configuration is not a valid object'
168
+ };
169
+ }
170
+
171
+ // Create backup if requested
172
+ let backupPath = null;
173
+ if (createBackup && fileExists(filePath)) {
174
+ backupPath = backupConfig(filePath);
175
+ }
176
+
177
+ // Write modified config
178
+ writeConfig(filePath, modifiedConfig);
179
+
180
+ return {
181
+ success: true,
182
+ backupPath
183
+ };
184
+ } catch (error) {
185
+ return {
186
+ success: false,
187
+ error: error.message
188
+ };
189
+ }
190
+ }
191
+
192
+ module.exports = {
193
+ fileExists,
194
+ readConfig,
195
+ writeConfig,
196
+ readConfigSafe,
197
+ backupConfig,
198
+ isValidConfig,
199
+ getFileModTime,
200
+ ensureDirectoryExists,
201
+ deleteConfig,
202
+ modifyConfig
203
+ };
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Configuration Path Management
3
+ *
4
+ * Centralized configuration file path management for all MCP clients.
5
+ * Supports Claude Desktop, Cursor, Claude Code, and local installations.
6
+ */
7
+
8
+ const os = require('os');
9
+ const path = require('path');
10
+
11
+ /**
12
+ * Platform detection utilities
13
+ */
14
+ const PLATFORMS = {
15
+ MAC: 'darwin',
16
+ WINDOWS: 'win32',
17
+ LINUX: 'linux'
18
+ };
19
+
20
+ function getPlatform() {
21
+ return os.platform();
22
+ }
23
+
24
+ function isMac() {
25
+ return getPlatform() === PLATFORMS.MAC;
26
+ }
27
+
28
+ function isWindows() {
29
+ return getPlatform() === PLATFORMS.WINDOWS;
30
+ }
31
+
32
+ function isLinux() {
33
+ return getPlatform() === PLATFORMS.LINUX;
34
+ }
35
+
36
+ /**
37
+ * Get home directory
38
+ */
39
+ function getHomeDir() {
40
+ return os.homedir();
41
+ }
42
+
43
+ /**
44
+ * Get current working directory
45
+ */
46
+ function getCwd() {
47
+ return process.cwd();
48
+ }
49
+
50
+ /**
51
+ * Client configuration path resolvers
52
+ * Each function returns the appropriate config file path for the target client
53
+ */
54
+
55
+ /**
56
+ * Get Claude Desktop config file path (platform-specific)
57
+ * @returns {string} Path to claude_desktop_config.json
58
+ */
59
+ function getClaudeDesktopConfigPath() {
60
+ const homeDir = getHomeDir();
61
+
62
+ if (isMac()) {
63
+ return path.join(homeDir, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
64
+ }
65
+
66
+ if (isWindows()) {
67
+ const appData = process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming');
68
+ return path.join(appData, 'Claude', 'claude_desktop_config.json');
69
+ }
70
+
71
+ // Linux and others
72
+ return path.join(homeDir, '.config', 'Claude', 'claude_desktop_config.json');
73
+ }
74
+
75
+ /**
76
+ * Get Cursor project config file path (.cursor/mcp.json in current directory)
77
+ * @returns {string} Path to project-specific Cursor config
78
+ */
79
+ function getCursorProjectConfigPath() {
80
+ return path.join(getCwd(), '.cursor', 'mcp.json');
81
+ }
82
+
83
+ /**
84
+ * Get Cursor global config file path (~/.cursor/mcp.json)
85
+ * @returns {string} Path to global Cursor config
86
+ */
87
+ function getCursorGlobalConfigPath() {
88
+ return path.join(getHomeDir(), '.cursor', 'mcp.json');
89
+ }
90
+
91
+ /**
92
+ * Get Claude Code project config path (.mcp.json in current directory)
93
+ * @returns {string} Path to project-specific Claude Code config
94
+ */
95
+ function getClaudeCodeProjectConfigPath() {
96
+ return path.join(getCwd(), '.mcp.json');
97
+ }
98
+
99
+ /**
100
+ * Get Claude Code user config path (~/.claude.json)
101
+ * @returns {string} Path to user-level Claude Code config
102
+ */
103
+ function getClaudeCodeUserConfigPath() {
104
+ return path.join(getHomeDir(), '.claude.json');
105
+ }
106
+
107
+ /**
108
+ * Get Claude Code local config path (same as user, but with project context)
109
+ * Note: Local scope uses ~/.claude.json but stores project-specific paths
110
+ * @returns {string} Path to local Claude Code config
111
+ */
112
+ function getClaudeCodeLocalConfigPath() {
113
+ return path.join(getHomeDir(), '.claude.json');
114
+ }
115
+
116
+ /**
117
+ * Get local mcp.json path (in current directory)
118
+ * @returns {string} Path to generic local MCP config
119
+ */
120
+ function getLocalMcpJsonPath() {
121
+ return path.join(getCwd(), 'mcp.json');
122
+ }
123
+
124
+ /**
125
+ * Get config path by client type and scope
126
+ * @param {string} client - Client type: 'claude-desktop', 'cursor', 'claude-code', 'local'
127
+ * @param {string} [scope] - Installation scope: 'global', 'project', 'user', 'local'
128
+ * @returns {string} Config file path
129
+ * @throws {Error} If client type is unknown or scope is invalid
130
+ */
131
+ function getConfigPath(client, scope = 'project') {
132
+ switch (client) {
133
+ case 'claude-desktop':
134
+ return getClaudeDesktopConfigPath();
135
+
136
+ case 'cursor':
137
+ if (scope === 'global') {
138
+ return getCursorGlobalConfigPath();
139
+ }
140
+ return getCursorProjectConfigPath();
141
+
142
+ case 'claude-code':
143
+ if (scope === 'user') {
144
+ return getClaudeCodeUserConfigPath();
145
+ }
146
+ if (scope === 'local') {
147
+ return getClaudeCodeLocalConfigPath();
148
+ }
149
+ return getClaudeCodeProjectConfigPath();
150
+
151
+ case 'local':
152
+ return getLocalMcpJsonPath();
153
+
154
+ default:
155
+ throw new Error(`Unknown client type: ${client}`);
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Get friendly display name for client
161
+ * @param {string} client - Client type
162
+ * @returns {string} Display name
163
+ */
164
+ function getClientDisplayName(client) {
165
+ const names = {
166
+ 'claude-desktop': 'Claude Desktop',
167
+ 'cursor': 'Cursor',
168
+ 'claude-code': 'Claude Code',
169
+ 'local': 'Local MCP'
170
+ };
171
+ return names[client] || client;
172
+ }
173
+
174
+ module.exports = {
175
+ // Platform utilities
176
+ getPlatform,
177
+ isMac,
178
+ isWindows,
179
+ isLinux,
180
+ getHomeDir,
181
+ getCwd,
182
+
183
+ // Individual path getters (for backward compatibility)
184
+ getClaudeDesktopConfigPath,
185
+ getCursorProjectConfigPath,
186
+ getCursorGlobalConfigPath,
187
+ getClaudeCodeProjectConfigPath,
188
+ getClaudeCodeUserConfigPath,
189
+ getClaudeCodeLocalConfigPath,
190
+ getLocalMcpJsonPath,
191
+
192
+ // Unified interface
193
+ getConfigPath,
194
+ getClientDisplayName,
195
+
196
+ // Constants
197
+ PLATFORMS
198
+ };