genudo-mcp-client 1.0.0 → 1.0.1
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/README.md +2 -0
- package/index.js +67 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -185,6 +185,8 @@ node index.js
|
|
|
185
185
|
| `GENUDO_API_KEY` | **Yes** | - | Your Genudo API key from account settings |
|
|
186
186
|
| `GENUDO_BASE_URL` | No | `https://api.genudo.ai` | Base URL for self-hosted Genudo instances |
|
|
187
187
|
| `GENUDO_ALLOW_INSECURE_SSL` | No | `false` | Allow self-signed SSL certificates (local development only) |
|
|
188
|
+
| `GENUDO_REQUEST_TIMEOUT` | No | `8000` | Per-attempt request timeout in ms before retrying |
|
|
189
|
+
| `GENUDO_REQUEST_RETRIES` | No | `4` | Number of attempts for a request before giving up |
|
|
188
190
|
|
|
189
191
|
## Development
|
|
190
192
|
|
package/index.js
CHANGED
|
@@ -11,6 +11,12 @@ const SSE_URL = `${BASE_URL}/api/user/mcp/sse`;
|
|
|
11
11
|
const API_KEY = process.env.GENUDO_API_KEY;
|
|
12
12
|
const ALLOW_INSECURE_SSL = process.env.GENUDO_ALLOW_INSECURE_SSL === 'true';
|
|
13
13
|
|
|
14
|
+
// The Genudo backend can be slow to answer the first request after connecting
|
|
15
|
+
// (cold start — observed 1s to 30s+). Rather than let one slow attempt hang the
|
|
16
|
+
// whole MCP handshake, we time out each attempt fast and retry.
|
|
17
|
+
const REQUEST_TIMEOUT = parseInt(process.env.GENUDO_REQUEST_TIMEOUT || '8000', 10);
|
|
18
|
+
const REQUEST_RETRIES = parseInt(process.env.GENUDO_REQUEST_RETRIES || '4', 10);
|
|
19
|
+
|
|
14
20
|
// HTTPS agent configuration
|
|
15
21
|
// For local development with self-signed certificates, set GENUDO_ALLOW_INSECURE_SSL=true
|
|
16
22
|
const httpsAgent = new https.Agent({
|
|
@@ -80,26 +86,46 @@ async function forwardRequest(jsonRpcRequest) {
|
|
|
80
86
|
|
|
81
87
|
debug('Forwarding request:', jsonRpcRequest.method, 'id:', jsonRpcRequest.id);
|
|
82
88
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
let lastError;
|
|
90
|
+
for (let attempt = 1; attempt <= REQUEST_RETRIES; attempt++) {
|
|
91
|
+
const controller = new AbortController();
|
|
92
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT);
|
|
93
|
+
try {
|
|
94
|
+
const response = await fetch(messageEndpoint, {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
headers: {
|
|
97
|
+
'Content-Type': 'application/json',
|
|
98
|
+
'Api-Key': API_KEY
|
|
99
|
+
},
|
|
100
|
+
body: JSON.stringify(jsonRpcRequest),
|
|
101
|
+
agent: httpsAgent,
|
|
102
|
+
signal: controller.signal
|
|
103
|
+
});
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
108
|
+
}
|
|
92
109
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
110
|
+
// Handle 204 No Content (for notifications)
|
|
111
|
+
if (response.status === 204) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
96
114
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
115
|
+
return await response.json();
|
|
116
|
+
} catch (error) {
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
lastError = error;
|
|
119
|
+
const reason = error.name === 'AbortError'
|
|
120
|
+
? `timeout after ${REQUEST_TIMEOUT}ms`
|
|
121
|
+
: error.message;
|
|
122
|
+
debug(`Attempt ${attempt}/${REQUEST_RETRIES} failed (${reason})`);
|
|
123
|
+
if (attempt < REQUEST_RETRIES) {
|
|
124
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
100
127
|
}
|
|
101
|
-
|
|
102
|
-
return await response.json();
|
|
128
|
+
throw lastError;
|
|
103
129
|
}
|
|
104
130
|
|
|
105
131
|
/**
|
|
@@ -110,6 +136,30 @@ async function processInput(line) {
|
|
|
110
136
|
const request = JSON.parse(line);
|
|
111
137
|
debug('Received request:', request.method);
|
|
112
138
|
|
|
139
|
+
// Answer the MCP handshake locally so Claude's startup never blocks on
|
|
140
|
+
// backend cold-start latency. The Genudo server accepts tool calls without a
|
|
141
|
+
// forwarded initialize (verified), so we synthesize the response and warm the
|
|
142
|
+
// backend in the background for the first real request.
|
|
143
|
+
if (request.method === 'initialize') {
|
|
144
|
+
console.log(JSON.stringify({
|
|
145
|
+
jsonrpc: '2.0',
|
|
146
|
+
id: request.id,
|
|
147
|
+
result: {
|
|
148
|
+
protocolVersion: (request.params && request.params.protocolVersion) || '2024-11-05',
|
|
149
|
+
capabilities: { tools: {} },
|
|
150
|
+
serverInfo: { name: 'Genudo', version: '1.0.0' }
|
|
151
|
+
}
|
|
152
|
+
}));
|
|
153
|
+
forwardRequest({ jsonrpc: '2.0', id: 'warmup', method: 'tools/list', params: {} })
|
|
154
|
+
.catch(() => {});
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Handshake is handled locally, so there is no server session to notify.
|
|
159
|
+
if (request.method === 'notifications/initialized') {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
113
163
|
const response = await forwardRequest(request);
|
|
114
164
|
|
|
115
165
|
// Only write response if there is one (notifications don't get responses)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genudo-mcp-client",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "A lightweight bridge connecting Claude Code to Genudo's Model Context Protocol (MCP) server for AI-powered workflow automation",
|
|
5
5
|
"mcpName": "io.github.genudo-ai/genudo_mcp",
|
|
6
6
|
"main": "index.js",
|