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.
Files changed (3) hide show
  1. package/README.md +2 -0
  2. package/index.js +67 -17
  3. 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
- const response = await fetch(messageEndpoint, {
84
- method: 'POST',
85
- headers: {
86
- 'Content-Type': 'application/json',
87
- 'Api-Key': API_KEY
88
- },
89
- body: JSON.stringify(jsonRpcRequest),
90
- agent: httpsAgent
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
- if (!response.ok) {
94
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
95
- }
110
+ // Handle 204 No Content (for notifications)
111
+ if (response.status === 204) {
112
+ return null;
113
+ }
96
114
 
97
- // Handle 204 No Content (for notifications)
98
- if (response.status === 204) {
99
- return null;
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.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",