@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,185 @@
1
+ /**
2
+ * MCP Protocol Method Handlers
3
+ *
4
+ * Handles MCP protocol methods (initialize, ping, tools/list, etc.)
5
+ * These are handled locally and not proxied to the backend.
6
+ */
7
+
8
+ const { sendResponse, sendError } = require('../protocol/json-rpc');
9
+ const { getToolsList } = require('../protocol/tools-schema');
10
+ const { JSON_RPC_ERROR_CODES } = require('../protocol/error-codes');
11
+ const { LOG_LEVELS, MCP_PROTOCOL_VERSION, SERVER_CAPABILITIES } = require('../core/constants');
12
+
13
+ /**
14
+ * Get log level
15
+ */
16
+ function getLogLevel() {
17
+ const level = (process.env.LOG_LEVEL || 'ERROR').toUpperCase();
18
+ return LOG_LEVELS[level] !== undefined ? LOG_LEVELS[level] : LOG_LEVELS.ERROR;
19
+ }
20
+
21
+ const LOG_LEVEL = getLogLevel();
22
+
23
+ /**
24
+ * Log message
25
+ */
26
+ function log(level, message) {
27
+ if (LOG_LEVEL >= level) {
28
+ const levelName = Object.keys(LOG_LEVELS)[level] || 'UNKNOWN';
29
+ process.stderr.write(`[${levelName}] ${message}\n`);
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Get package info
35
+ */
36
+ function getPackageInfo() {
37
+ try {
38
+ const pkg = require('../../package.json');
39
+ return { name: pkg.name, version: pkg.version };
40
+ } catch (error) {
41
+ return { name: '@meldocio/mcp-stdio-proxy', version: '1.0.0' };
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Handle MCP initialize method
47
+ * @param {Object} request - JSON-RPC request
48
+ */
49
+ function handleInitialize(request) {
50
+ const pkg = getPackageInfo();
51
+
52
+ const result = {
53
+ protocolVersion: MCP_PROTOCOL_VERSION,
54
+ capabilities: {
55
+ tools: SERVER_CAPABILITIES.TOOLS ? {} : undefined,
56
+ resources: SERVER_CAPABILITIES.RESOURCES ? {} : undefined,
57
+ prompts: SERVER_CAPABILITIES.PROMPTS ? {} : undefined,
58
+ logging: SERVER_CAPABILITIES.LOGGING ? {} : undefined
59
+ },
60
+ serverInfo: {
61
+ name: pkg.name,
62
+ version: pkg.version
63
+ }
64
+ };
65
+
66
+ // Remove undefined capabilities
67
+ Object.keys(result.capabilities).forEach(key => {
68
+ if (result.capabilities[key] === undefined) {
69
+ delete result.capabilities[key];
70
+ }
71
+ });
72
+
73
+ log(LOG_LEVELS.DEBUG, 'Initialize request received');
74
+ sendResponse(request.id, result);
75
+ }
76
+
77
+ /**
78
+ * Handle MCP ping method (keep-alive)
79
+ * @param {Object} request - JSON-RPC request
80
+ */
81
+ function handlePing(request) {
82
+ sendResponse(request.id, {});
83
+ }
84
+
85
+ /**
86
+ * Handle resources/list method
87
+ * Returns empty list as resources are not supported yet
88
+ * @param {Object} request - JSON-RPC request
89
+ */
90
+ function handleResourcesList(request) {
91
+ sendResponse(request.id, {
92
+ resources: []
93
+ });
94
+ }
95
+
96
+ /**
97
+ * Handle tools/list method
98
+ * Always returns static list locally, never proxies to backend
99
+ * @param {Object} request - JSON-RPC request
100
+ */
101
+ function handleToolsList(request) {
102
+ try {
103
+ const tools = getToolsList();
104
+
105
+ // Log tool names for debugging
106
+ const toolNames = tools.map(t => t.name).join(', ');
107
+ log(LOG_LEVELS.INFO, `Returning ${tools.length} tools locally: ${toolNames}`);
108
+
109
+ sendResponse(request.id, {
110
+ tools: tools
111
+ });
112
+ } catch (error) {
113
+ // This should never happen, but if it does, send error response
114
+ log(LOG_LEVELS.ERROR, `Unexpected error in handleToolsList: ${error.message || 'Unknown error'}`);
115
+ log(LOG_LEVELS.DEBUG, `Error stack: ${error.stack || 'No stack trace'}`);
116
+
117
+ sendError(request.id, JSON_RPC_ERROR_CODES.INTERNAL_ERROR,
118
+ `Failed to get tools list: ${error.message || 'Unknown error'}`, {
119
+ code: 'INTERNAL_ERROR'
120
+ });
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Check if a method should be handled locally
126
+ * @param {string} method - Method name
127
+ * @returns {boolean} True if should be handled locally
128
+ */
129
+ function isLocalMethod(method) {
130
+ const localMethods = [
131
+ 'initialize',
132
+ 'initialized',
133
+ 'notifications/initialized',
134
+ 'notifications/cancelled',
135
+ 'ping',
136
+ 'resources/list',
137
+ 'tools/list'
138
+ ];
139
+ return localMethods.includes(method);
140
+ }
141
+
142
+ /**
143
+ * Route request to appropriate local handler
144
+ * @param {Object} request - JSON-RPC request
145
+ * @returns {boolean} True if handled, false if should be proxied
146
+ */
147
+ function handleLocalMethod(request) {
148
+ const method = request.method;
149
+
150
+ switch (method) {
151
+ case 'initialize':
152
+ handleInitialize(request);
153
+ return true;
154
+
155
+ case 'initialized':
156
+ case 'notifications/initialized':
157
+ case 'notifications/cancelled':
158
+ // Notifications - no response needed
159
+ return true;
160
+
161
+ case 'ping':
162
+ handlePing(request);
163
+ return true;
164
+
165
+ case 'resources/list':
166
+ handleResourcesList(request);
167
+ return true;
168
+
169
+ case 'tools/list':
170
+ handleToolsList(request);
171
+ return true;
172
+
173
+ default:
174
+ return false; // Not a local method
175
+ }
176
+ }
177
+
178
+ module.exports = {
179
+ handleInitialize,
180
+ handlePing,
181
+ handleResourcesList,
182
+ handleToolsList,
183
+ isLocalMethod,
184
+ handleLocalMethod
185
+ };
@@ -0,0 +1,179 @@
1
+ /**
2
+ * MCP tools/call Handler
3
+ *
4
+ * Handles tools/call requests - routes to local tools or backend.
5
+ * Local tools: set_workspace, get_workspace, auth_status, auth_login_instructions
6
+ */
7
+
8
+ const { sendResponse, sendError } = require('../protocol/json-rpc');
9
+ const { JSON_RPC_ERROR_CODES } = require('../protocol/error-codes');
10
+ const { getAuthStatus } = require('../core/auth');
11
+ const { setWorkspaceAlias, getWorkspaceAlias } = require('../core/config');
12
+ const { LOG_LEVELS } = require('../core/constants');
13
+
14
+ /**
15
+ * Get log level
16
+ */
17
+ function getLogLevel() {
18
+ const level = (process.env.LOG_LEVEL || 'ERROR').toUpperCase();
19
+ return LOG_LEVELS[level] !== undefined ? LOG_LEVELS[level] : LOG_LEVELS.ERROR;
20
+ }
21
+
22
+ const LOG_LEVEL = getLogLevel();
23
+
24
+ /**
25
+ * Log message
26
+ */
27
+ function log(level, message) {
28
+ if (LOG_LEVEL >= level) {
29
+ const levelName = Object.keys(LOG_LEVELS)[level] || 'UNKNOWN';
30
+ process.stderr.write(`[${levelName}] ${message}\n`);
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Create MCP tool response with text content
36
+ * @param {string} text - Text content
37
+ * @returns {Object} MCP result object
38
+ */
39
+ function createToolResponse(text) {
40
+ return {
41
+ content: [
42
+ {
43
+ type: 'text',
44
+ text: text
45
+ }
46
+ ]
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Handle set_workspace tool
52
+ * @param {Object} request - JSON-RPC request
53
+ * @param {Object} arguments_ - Tool arguments
54
+ */
55
+ function handleSetWorkspace(request, arguments_) {
56
+ const alias = arguments_.alias;
57
+ if (!alias || typeof alias !== 'string') {
58
+ sendError(request.id, JSON_RPC_ERROR_CODES.INVALID_PARAMS, 'alias parameter is required and must be a string');
59
+ return;
60
+ }
61
+
62
+ try {
63
+ setWorkspaceAlias(alias);
64
+ sendResponse(request.id, createToolResponse(`Workspace alias set to: ${alias}`));
65
+ } catch (error) {
66
+ sendError(request.id, JSON_RPC_ERROR_CODES.INTERNAL_ERROR, `Failed to set workspace alias: ${error.message}`);
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Handle get_workspace tool
72
+ * @param {Object} request - JSON-RPC request
73
+ */
74
+ function handleGetWorkspace(request) {
75
+ try {
76
+ const workspaceAlias = getWorkspaceAlias();
77
+ const result = {
78
+ workspaceAlias: workspaceAlias || null,
79
+ source: workspaceAlias ? 'config' : 'not_found',
80
+ message: workspaceAlias ? `Current workspace: ${workspaceAlias}` : 'No workspace set in config'
81
+ };
82
+ sendResponse(request.id, createToolResponse(JSON.stringify(result, null, 2)));
83
+ } catch (error) {
84
+ sendError(request.id, JSON_RPC_ERROR_CODES.INTERNAL_ERROR, `Failed to get workspace: ${error.message}`);
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Handle auth_status tool
90
+ * @param {Object} request - JSON-RPC request
91
+ */
92
+ async function handleAuthStatus(request) {
93
+ try {
94
+ const authStatus = await getAuthStatus();
95
+ if (!authStatus) {
96
+ const result = {
97
+ authenticated: false,
98
+ message: 'Not authenticated. Run: npx @meldocio/mcp-stdio-proxy@latest auth login'
99
+ };
100
+ sendResponse(request.id, createToolResponse(JSON.stringify(result, null, 2)));
101
+ return;
102
+ }
103
+
104
+ sendResponse(request.id, createToolResponse(JSON.stringify(authStatus, null, 2)));
105
+ } catch (error) {
106
+ sendError(request.id, JSON_RPC_ERROR_CODES.INTERNAL_ERROR, `Failed to get auth status: ${error.message}`);
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Handle auth_login_instructions tool
112
+ * @param {Object} request - JSON-RPC request
113
+ */
114
+ function handleAuthLoginInstructions(request) {
115
+ const text = 'To authenticate, run the following command:\n\n```bash\nnpx @meldocio/mcp-stdio-proxy@latest auth login\n```';
116
+ sendResponse(request.id, createToolResponse(text));
117
+ }
118
+
119
+ /**
120
+ * Check if tool should be handled locally
121
+ * @param {string} toolName - Tool name
122
+ * @returns {boolean} True if local tool
123
+ */
124
+ function isLocalTool(toolName) {
125
+ const localTools = [
126
+ 'set_workspace',
127
+ 'get_workspace',
128
+ 'auth_status',
129
+ 'auth_login_instructions'
130
+ ];
131
+ return localTools.includes(toolName);
132
+ }
133
+
134
+ /**
135
+ * Handle tools/call for local tools
136
+ * @param {Object} request - JSON-RPC request
137
+ * @returns {Promise<boolean>} True if handled locally, false if should be proxied
138
+ */
139
+ async function handleToolsCall(request) {
140
+ const toolName = request.params?.name;
141
+ const arguments_ = request.params?.arguments || {};
142
+
143
+ log(LOG_LEVELS.DEBUG, `handleToolsCall: toolName=${toolName}`);
144
+
145
+ if (!isLocalTool(toolName)) {
146
+ return false; // Not a local tool, should be proxied
147
+ }
148
+
149
+ // Handle local tools
150
+ try {
151
+ switch (toolName) {
152
+ case 'set_workspace':
153
+ handleSetWorkspace(request, arguments_);
154
+ break;
155
+ case 'get_workspace':
156
+ handleGetWorkspace(request);
157
+ break;
158
+ case 'auth_status':
159
+ await handleAuthStatus(request);
160
+ break;
161
+ case 'auth_login_instructions':
162
+ handleAuthLoginInstructions(request);
163
+ break;
164
+ }
165
+ return true; // Handled
166
+ } catch (error) {
167
+ log(LOG_LEVELS.ERROR, `Unexpected error in handleToolsCall: ${error.message || 'Unknown error'}`);
168
+ sendError(request.id, JSON_RPC_ERROR_CODES.INTERNAL_ERROR,
169
+ `Error executing tool: ${error.message || 'Unknown error'}`, {
170
+ code: 'INTERNAL_ERROR'
171
+ });
172
+ return true; // Error handled
173
+ }
174
+ }
175
+
176
+ module.exports = {
177
+ handleToolsCall,
178
+ isLocalTool
179
+ };
@@ -0,0 +1,143 @@
1
+ /**
2
+ * JSON-RPC and Custom Error Codes
3
+ *
4
+ * This module centralizes all error code definitions and error message formatting
5
+ * for the Meldoc MCP proxy.
6
+ */
7
+
8
+ /**
9
+ * Standard JSON-RPC 2.0 error codes
10
+ * @see https://www.jsonrpc.org/specification#error_object
11
+ */
12
+ const JSON_RPC_ERROR_CODES = {
13
+ PARSE_ERROR: -32700, // Invalid JSON was received by the server
14
+ INVALID_REQUEST: -32600, // The JSON sent is not a valid Request object
15
+ METHOD_NOT_FOUND: -32601, // The method does not exist / is not available
16
+ INVALID_PARAMS: -32602, // Invalid method parameter(s)
17
+ INTERNAL_ERROR: -32603, // Internal JSON-RPC error
18
+ SERVER_ERROR: -32000 // Generic server error (implementation-defined)
19
+ };
20
+
21
+ /**
22
+ * Custom application error codes
23
+ * Using the reserved range -32000 to -32099 for server errors
24
+ */
25
+ const CUSTOM_ERROR_CODES = {
26
+ AUTH_REQUIRED: -32001, // Authentication required but not provided
27
+ NOT_FOUND: -32002, // Requested resource not found
28
+ RATE_LIMIT: -32003 // Rate limit exceeded
29
+ };
30
+
31
+ /**
32
+ * All error codes combined
33
+ */
34
+ const ALL_ERROR_CODES = {
35
+ ...JSON_RPC_ERROR_CODES,
36
+ ...CUSTOM_ERROR_CODES
37
+ };
38
+
39
+ /**
40
+ * HTTP status code mappings for common errors
41
+ */
42
+ const HTTP_STATUS_CODES = {
43
+ OK: 200,
44
+ BAD_REQUEST: 400,
45
+ UNAUTHORIZED: 401,
46
+ FORBIDDEN: 403,
47
+ NOT_FOUND: 404,
48
+ RATE_LIMITED: 429,
49
+ INTERNAL_ERROR: 500,
50
+ SERVICE_UNAVAILABLE: 503
51
+ };
52
+
53
+ /**
54
+ * Get a human-readable error name from error code
55
+ * @param {number} code - The error code
56
+ * @returns {string} Error name or 'UNKNOWN_ERROR'
57
+ */
58
+ function getErrorName(code) {
59
+ const entry = Object.entries(ALL_ERROR_CODES).find(([_, value]) => value === code);
60
+ return entry ? entry[0] : 'UNKNOWN_ERROR';
61
+ }
62
+
63
+ /**
64
+ * Check if an error code represents an authentication error
65
+ * @param {number} code - The error code
66
+ * @returns {boolean} True if authentication error
67
+ */
68
+ function isAuthError(code) {
69
+ return code === CUSTOM_ERROR_CODES.AUTH_REQUIRED ||
70
+ code === HTTP_STATUS_CODES.UNAUTHORIZED ||
71
+ code === HTTP_STATUS_CODES.FORBIDDEN;
72
+ }
73
+
74
+ /**
75
+ * Check if an error code represents a client error (4xx)
76
+ * @param {number} code - The error code
77
+ * @returns {boolean} True if client error
78
+ */
79
+ function isClientError(code) {
80
+ return code >= 400 && code < 500;
81
+ }
82
+
83
+ /**
84
+ * Check if an error code represents a server error (5xx)
85
+ * @param {number} code - The error code
86
+ * @returns {boolean} True if server error
87
+ */
88
+ function isServerError(code) {
89
+ return code >= 500 && code < 600;
90
+ }
91
+
92
+ /**
93
+ * Format error message with additional context
94
+ * @param {string} message - Base error message
95
+ * @param {Object} details - Additional error details
96
+ * @returns {string} Formatted error message
97
+ */
98
+ function formatErrorMessage(message, details = {}) {
99
+ let formatted = message;
100
+
101
+ if (details.code) {
102
+ formatted += ` (code: ${details.code})`;
103
+ }
104
+
105
+ if (details.hint) {
106
+ formatted += `\nHint: ${details.hint}`;
107
+ }
108
+
109
+ return formatted;
110
+ }
111
+
112
+ /**
113
+ * Create standard error data object
114
+ * @param {string} code - Error code identifier
115
+ * @param {string} [hint] - Optional hint for resolving the error
116
+ * @param {Object} [additional] - Additional error context
117
+ * @returns {Object} Error data object
118
+ */
119
+ function createErrorData(code, hint = null, additional = {}) {
120
+ const data = {
121
+ code,
122
+ ...additional
123
+ };
124
+
125
+ if (hint) {
126
+ data.hint = hint;
127
+ }
128
+
129
+ return data;
130
+ }
131
+
132
+ module.exports = {
133
+ JSON_RPC_ERROR_CODES,
134
+ CUSTOM_ERROR_CODES,
135
+ ALL_ERROR_CODES,
136
+ HTTP_STATUS_CODES,
137
+ getErrorName,
138
+ isAuthError,
139
+ isClientError,
140
+ isServerError,
141
+ formatErrorMessage,
142
+ createErrorData
143
+ };
@@ -0,0 +1,183 @@
1
+ /**
2
+ * JSON-RPC 2.0 Protocol Utilities
3
+ *
4
+ * Handles JSON-RPC response formatting and sending.
5
+ * Eliminates 8+ duplications of stdout write patterns throughout the codebase.
6
+ */
7
+
8
+ const { LOG_LEVELS } = require('../core/constants');
9
+
10
+ /**
11
+ * Get current log level from environment
12
+ */
13
+ function getLogLevel() {
14
+ const level = (process.env.LOG_LEVEL || 'ERROR').toUpperCase();
15
+ return LOG_LEVELS[level] !== undefined ? LOG_LEVELS[level] : LOG_LEVELS.ERROR;
16
+ }
17
+
18
+ const LOG_LEVEL = getLogLevel();
19
+
20
+ /**
21
+ * Send JSON-RPC success response
22
+ * @param {string|number} id - Request ID
23
+ * @param {any} result - Result data
24
+ */
25
+ function sendResponse(id, result) {
26
+ if (id === undefined || id === null) {
27
+ // Notification - no response needed
28
+ return;
29
+ }
30
+
31
+ const response = {
32
+ jsonrpc: '2.0',
33
+ id: id,
34
+ result: result
35
+ };
36
+
37
+ writeResponse(response);
38
+ }
39
+
40
+ /**
41
+ * Send JSON-RPC error response
42
+ * @param {string|number} id - Request ID
43
+ * @param {number} code - Error code
44
+ * @param {string} message - Error message
45
+ * @param {Object} [details] - Optional error details
46
+ */
47
+ function sendError(id, code, message, details) {
48
+ // Only send error response if id is defined (not for notifications)
49
+ // Claude Desktop's Zod schema doesn't accept null for id
50
+ if (id === undefined || id === null) {
51
+ // For notifications or parse errors without id, don't send response
52
+ return;
53
+ }
54
+
55
+ const errorResponse = {
56
+ jsonrpc: '2.0',
57
+ id: id,
58
+ error: {
59
+ code: code,
60
+ message: message
61
+ }
62
+ };
63
+
64
+ // Add details if provided and debug logging is enabled
65
+ if (details && LOG_LEVEL >= LOG_LEVELS.DEBUG) {
66
+ errorResponse.error.data = details;
67
+ }
68
+
69
+ writeResponse(errorResponse);
70
+ }
71
+
72
+ /**
73
+ * Write JSON-RPC response to stdout
74
+ * Handles flushing and error cases
75
+ * @param {Object} response - Response object to send
76
+ */
77
+ function writeResponse(response) {
78
+ try {
79
+ const responseStr = JSON.stringify(response);
80
+ process.stdout.write(responseStr + '\n');
81
+
82
+ // Flush stdout to ensure data is sent immediately
83
+ if (process.stdout.isTTY) {
84
+ process.stdout.flush();
85
+ }
86
+ } catch (error) {
87
+ // If stdout write fails, we can't do much - just log to stderr
88
+ if (process.stderr) {
89
+ process.stderr.write(`Failed to write response: ${error.message}\n`);
90
+ }
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Create a success response object (without sending)
96
+ * @param {string|number} id - Request ID
97
+ * @param {any} result - Result data
98
+ * @returns {Object} Response object
99
+ */
100
+ function createResponse(id, result) {
101
+ return {
102
+ jsonrpc: '2.0',
103
+ id: id,
104
+ result: result
105
+ };
106
+ }
107
+
108
+ /**
109
+ * Create an error response object (without sending)
110
+ * @param {string|number} id - Request ID
111
+ * @param {number} code - Error code
112
+ * @param {string} message - Error message
113
+ * @param {Object} [details] - Optional error details
114
+ * @returns {Object} Error response object
115
+ */
116
+ function createErrorResponse(id, code, message, details) {
117
+ const errorResponse = {
118
+ jsonrpc: '2.0',
119
+ id: id,
120
+ error: {
121
+ code: code,
122
+ message: message
123
+ }
124
+ };
125
+
126
+ if (details && LOG_LEVEL >= LOG_LEVELS.DEBUG) {
127
+ errorResponse.error.data = details;
128
+ }
129
+
130
+ return errorResponse;
131
+ }
132
+
133
+ /**
134
+ * Validate JSON-RPC request structure
135
+ * @param {any} request - Request to validate
136
+ * @returns {Object} { valid: boolean, error?: string }
137
+ */
138
+ function validateRequest(request) {
139
+ if (!request || typeof request !== 'object') {
140
+ return { valid: false, error: 'Request must be an object' };
141
+ }
142
+
143
+ // Allow requests without jsonrpc for compatibility
144
+ if (request.jsonrpc && request.jsonrpc !== '2.0') {
145
+ return { valid: false, error: 'jsonrpc must be "2.0"' };
146
+ }
147
+
148
+ // Allow requests without method if they're notifications
149
+ if (!request.method && !Array.isArray(request) && request.id !== null && request.id !== undefined) {
150
+ return { valid: false, error: 'Request must have a method or be a batch array' };
151
+ }
152
+
153
+ return { valid: true };
154
+ }
155
+
156
+ /**
157
+ * Check if a request is a notification (no id)
158
+ * @param {Object} request - Request object
159
+ * @returns {boolean} True if notification
160
+ */
161
+ function isNotification(request) {
162
+ return request.id === null || request.id === undefined;
163
+ }
164
+
165
+ /**
166
+ * Extract method name from request
167
+ * @param {Object} request - Request object
168
+ * @returns {string|null} Method name or null
169
+ */
170
+ function getMethodName(request) {
171
+ return request && request.method ? request.method : null;
172
+ }
173
+
174
+ module.exports = {
175
+ sendResponse,
176
+ sendError,
177
+ writeResponse,
178
+ createResponse,
179
+ createErrorResponse,
180
+ validateRequest,
181
+ isNotification,
182
+ getMethodName
183
+ };