@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/bin/cli.js +33 -1088
- package/bin/meldoc-mcp-proxy.js +134 -1264
- package/lib/cli/commands.js +277 -0
- package/lib/cli/formatters.js +137 -0
- package/lib/core/constants.js +98 -0
- package/lib/http/client.js +61 -0
- package/lib/http/error-handler.js +195 -0
- package/lib/install/config-manager.js +203 -0
- package/lib/install/config-paths.js +198 -0
- package/lib/install/installers.js +328 -0
- package/lib/install/templates.js +266 -0
- package/lib/mcp/handlers.js +185 -0
- package/lib/mcp/tools-call.js +179 -0
- package/lib/protocol/error-codes.js +143 -0
- package/lib/protocol/json-rpc.js +183 -0
- package/lib/protocol/tools-schema.js +239 -0
- package/package.json +1 -1
- package/lib/constants.js +0 -31
- /package/lib/{auth.js → core/auth.js} +0 -0
- /package/lib/{config.js → core/config.js} +0 -0
- /package/lib/{credentials.js → core/credentials.js} +0 -0
- /package/lib/{device-flow.js → core/device-flow.js} +0 -0
- /package/lib/{logger.js → core/logger.js} +0 -0
- /package/lib/{workspace.js → core/workspace.js} +0 -0
|
@@ -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
|
+
};
|