genudo-mcp-client 1.0.0 → 1.0.2

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 +92 -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,36 @@ 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
+
20
+ // Server-level guidance returned in the `initialize` handshake. MCP clients
21
+ // (Claude Code, Codex, Cursor, ...) inject this into the model's context, so it
22
+ // teaches any client how to get value fast and avoid the common failure modes —
23
+ // without the user having to write prompts. Keep it concise and high-signal.
24
+ const SERVER_INSTRUCTIONS = [
25
+ 'Genudo MCP: control + analytics for AI sales/support pipelines (pipelines, stages, actions/webhooks, variables, contacts, opportunities, messages, analytics). 21 tools, read + write.',
26
+ '',
27
+ 'PICK A PATTERN:',
28
+ '- Audit a pipeline: list_pipelines -> list_pipeline_stages -> list_variables -> list_opportunities -> list_contacts -> list_messages',
29
+ '- Build from scratch: start_pipeline_journey (ALWAYS first) -> get_pipeline_options (valid IDs) -> create_pipeline -> create_stage (xN) -> create_variable -> create_action',
30
+ '- Add an integration: list_pipelines -> list_pipeline_stages -> list_variables -> create_variable -> create_action',
31
+ '- Report activity: get_account_summary -> get_ai_performance -> list_opportunities -> get_messaging_stats',
32
+ '',
33
+ 'RULES THAT PREVENT FAILURES:',
34
+ '1. Discover IDs before writing: get_pipeline_options (agent_type_id, ai_model_id, language_id, channel_id); list_pipelines (pipeline_id); list_pipeline_stages (stage_id); list_variables (variable_id).',
35
+ '2. Actions CANNOT use raw system placeholders. Never put {{opportunity.contact_email}} in an action url/headers/payload. Instead create_variable {type:"from_system", value:"opportunity.contact_email"} and reference {{its_name}}. Variable types: fixed | from_system | from_action | from_ai.',
36
+ '3. create_pipeline: if is_model_routing_enabled=true, model_pool is required with exactly 4 tiers (router, simple, moderate, complex). persona + instructions drive quality — ask the user for a 1-2 sentence business description, then offer to write them.',
37
+ '4. create_stage nature in {neutral, won, lost}. create_action fixed_trigger in {stage_started, on_any_message, on_user_message, custom}; omit stage_id for a pipeline-wide action.',
38
+ '5. Immutable after create: pipeline agent_type; action fixed_trigger can only change to on_user_message/custom on update; update_opportunities stage moves must stay within the same pipeline.',
39
+ '6. Not exposed (do not attempt): listing actions, deleting actions/stages/pipelines, KB management, sending manual messages, reading plan limits.',
40
+ '',
41
+ 'Confirm before bulk writes (update_opportunities is bulk). The backend can be slow on the first call — retries are automatic.'
42
+ ].join('\n');
43
+
14
44
  // HTTPS agent configuration
15
45
  // For local development with self-signed certificates, set GENUDO_ALLOW_INSECURE_SSL=true
16
46
  const httpsAgent = new https.Agent({
@@ -80,26 +110,46 @@ async function forwardRequest(jsonRpcRequest) {
80
110
 
81
111
  debug('Forwarding request:', jsonRpcRequest.method, 'id:', jsonRpcRequest.id);
82
112
 
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
- });
113
+ let lastError;
114
+ for (let attempt = 1; attempt <= REQUEST_RETRIES; attempt++) {
115
+ const controller = new AbortController();
116
+ const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT);
117
+ try {
118
+ const response = await fetch(messageEndpoint, {
119
+ method: 'POST',
120
+ headers: {
121
+ 'Content-Type': 'application/json',
122
+ 'Api-Key': API_KEY
123
+ },
124
+ body: JSON.stringify(jsonRpcRequest),
125
+ agent: httpsAgent,
126
+ signal: controller.signal
127
+ });
128
+ clearTimeout(timer);
92
129
 
93
- if (!response.ok) {
94
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
95
- }
130
+ if (!response.ok) {
131
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
132
+ }
96
133
 
97
- // Handle 204 No Content (for notifications)
98
- if (response.status === 204) {
99
- return null;
100
- }
134
+ // Handle 204 No Content (for notifications)
135
+ if (response.status === 204) {
136
+ return null;
137
+ }
101
138
 
102
- return await response.json();
139
+ return await response.json();
140
+ } catch (error) {
141
+ clearTimeout(timer);
142
+ lastError = error;
143
+ const reason = error.name === 'AbortError'
144
+ ? `timeout after ${REQUEST_TIMEOUT}ms`
145
+ : error.message;
146
+ debug(`Attempt ${attempt}/${REQUEST_RETRIES} failed (${reason})`);
147
+ if (attempt < REQUEST_RETRIES) {
148
+ await new Promise((r) => setTimeout(r, 500));
149
+ }
150
+ }
151
+ }
152
+ throw lastError;
103
153
  }
104
154
 
105
155
  /**
@@ -110,6 +160,31 @@ async function processInput(line) {
110
160
  const request = JSON.parse(line);
111
161
  debug('Received request:', request.method);
112
162
 
163
+ // Answer the MCP handshake locally so Claude's startup never blocks on
164
+ // backend cold-start latency. The Genudo server accepts tool calls without a
165
+ // forwarded initialize (verified), so we synthesize the response and warm the
166
+ // backend in the background for the first real request.
167
+ if (request.method === 'initialize') {
168
+ console.log(JSON.stringify({
169
+ jsonrpc: '2.0',
170
+ id: request.id,
171
+ result: {
172
+ protocolVersion: (request.params && request.params.protocolVersion) || '2024-11-05',
173
+ capabilities: { tools: {} },
174
+ serverInfo: { name: 'Genudo', version: '1.0.2' },
175
+ instructions: SERVER_INSTRUCTIONS
176
+ }
177
+ }));
178
+ forwardRequest({ jsonrpc: '2.0', id: 'warmup', method: 'tools/list', params: {} })
179
+ .catch(() => {});
180
+ return;
181
+ }
182
+
183
+ // Handshake is handled locally, so there is no server session to notify.
184
+ if (request.method === 'notifications/initialized') {
185
+ return;
186
+ }
187
+
113
188
  const response = await forwardRequest(request);
114
189
 
115
190
  // 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.2",
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",