grove-mcp 1.0.6 → 1.0.7
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/index.js +107 -72
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -96,91 +96,126 @@ let sessionPath = null;
|
|
|
96
96
|
let affinityCookie = null; // sticky-session cookie from SSE response headers
|
|
97
97
|
const pending = [];
|
|
98
98
|
|
|
99
|
-
// ── SSE connection
|
|
100
|
-
|
|
101
|
-
const sseReq = https.request(
|
|
102
|
-
{
|
|
103
|
-
hostname: HOSTNAME,
|
|
104
|
-
path: SSE_PATH,
|
|
105
|
-
method: 'GET',
|
|
106
|
-
headers: { Accept: 'text/event-stream' },
|
|
107
|
-
},
|
|
108
|
-
(res) => {
|
|
109
|
-
if (res.statusCode !== 200) {
|
|
110
|
-
process.stderr.write(
|
|
111
|
-
`grove-mcp-bridge: SSE connect failed (HTTP ${res.statusCode})\n`
|
|
112
|
-
);
|
|
113
|
-
process.exit(1);
|
|
114
|
-
}
|
|
99
|
+
// ── SSE connection with reconnect ───────────────────────────────────────────
|
|
115
100
|
|
|
116
|
-
|
|
117
|
-
const setCookie = res.headers['set-cookie'];
|
|
118
|
-
if (setCookie) {
|
|
119
|
-
const cookies = Array.isArray(setCookie) ? setCookie : [setCookie];
|
|
120
|
-
const parts = cookies.map((c) => c.split(';')[0].trim());
|
|
121
|
-
affinityCookie = parts.join('; ');
|
|
122
|
-
}
|
|
101
|
+
let reconnectDelay = 1000; // ms; doubles on each failure, capped at 30s
|
|
123
102
|
|
|
124
|
-
|
|
125
|
-
|
|
103
|
+
function connect() {
|
|
104
|
+
// Reset session state so stdin messages are queued until the new endpoint arrives
|
|
105
|
+
sessionPath = null;
|
|
106
|
+
affinityCookie = null;
|
|
126
107
|
|
|
127
|
-
|
|
128
|
-
|
|
108
|
+
const sseReq = https.request(
|
|
109
|
+
{
|
|
110
|
+
hostname: HOSTNAME,
|
|
111
|
+
path: SSE_PATH,
|
|
112
|
+
method: 'GET',
|
|
113
|
+
headers: { Accept: 'text/event-stream' },
|
|
114
|
+
},
|
|
115
|
+
(res) => {
|
|
116
|
+
if (res.statusCode !== 200) {
|
|
117
|
+
process.stderr.write(
|
|
118
|
+
`grove-mcp-bridge: SSE connect failed (HTTP ${res.statusCode}), retrying in ${reconnectDelay}ms\n`
|
|
119
|
+
);
|
|
120
|
+
res.on('error', () => {}); // prevent unhandled-error crash while draining
|
|
121
|
+
res.resume();
|
|
122
|
+
scheduleReconnect();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
129
125
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const line = buf.slice(0, nl).replace(/\r$/, '');
|
|
133
|
-
buf = buf.slice(nl + 1);
|
|
126
|
+
// Successful connection — reset backoff
|
|
127
|
+
reconnectDelay = 1000;
|
|
134
128
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
129
|
+
// Capture nginx sticky-session cookie so POSTs reach the same pod
|
|
130
|
+
const setCookie = res.headers['set-cookie'];
|
|
131
|
+
if (setCookie) {
|
|
132
|
+
const cookies = Array.isArray(setCookie) ? setCookie : [setCookie];
|
|
133
|
+
const parts = cookies.map((c) => c.split(';')[0].trim());
|
|
134
|
+
affinityCookie = parts.join('; ');
|
|
135
|
+
}
|
|
139
136
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
137
|
+
let buf = '';
|
|
138
|
+
let eventType = '';
|
|
139
|
+
|
|
140
|
+
res.on('data', (chunk) => {
|
|
141
|
+
buf += chunk.toString('utf8');
|
|
142
|
+
|
|
143
|
+
let nl;
|
|
144
|
+
while ((nl = buf.indexOf('\n')) !== -1) {
|
|
145
|
+
const line = buf.slice(0, nl).replace(/\r$/, '');
|
|
146
|
+
buf = buf.slice(nl + 1);
|
|
147
|
+
|
|
148
|
+
if (line.startsWith('event:')) {
|
|
149
|
+
eventType = line.slice(6).trim();
|
|
150
|
+
} else if (line.startsWith('data:')) {
|
|
151
|
+
const raw = line.slice(5).trim();
|
|
152
|
+
|
|
153
|
+
if (eventType === 'endpoint') {
|
|
154
|
+
try {
|
|
155
|
+
sessionPath = JSON.parse(raw).uri;
|
|
156
|
+
} catch (_) {
|
|
157
|
+
// Fallback: server sent a plain path instead of JSON
|
|
158
|
+
sessionPath = raw;
|
|
159
|
+
}
|
|
160
|
+
// Flush any stdin lines that arrived before the endpoint was ready
|
|
161
|
+
for (const msg of pending) sendToServer(msg);
|
|
162
|
+
pending.length = 0;
|
|
163
|
+
} else if (eventType === 'message' && raw) {
|
|
164
|
+
// Strip modern MCP spec fields that cause Claude Desktop to silently
|
|
165
|
+
// drop all tools (anthropics/claude-code#25081): FastMCP 3.x emits
|
|
166
|
+
// outputSchema in tools/list which Claude Desktop doesn't understand.
|
|
167
|
+
process.stdout.write(stripMcpCompat(raw) + '\n');
|
|
146
168
|
}
|
|
147
|
-
// Flush any stdin lines that arrived before the endpoint was ready
|
|
148
|
-
for (const msg of pending) sendToServer(msg);
|
|
149
|
-
pending.length = 0;
|
|
150
|
-
} else if (eventType === 'message' && raw) {
|
|
151
|
-
// Strip modern MCP spec fields that cause Claude Desktop to silently
|
|
152
|
-
// drop all tools (anthropics/claude-code#25081): FastMCP 3.x emits
|
|
153
|
-
// outputSchema in tools/list which Claude Desktop doesn't understand.
|
|
154
|
-
process.stdout.write(stripMcpCompat(raw) + '\n');
|
|
155
|
-
}
|
|
156
169
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
170
|
+
eventType = '';
|
|
171
|
+
} else if (line === '') {
|
|
172
|
+
eventType = '';
|
|
173
|
+
}
|
|
160
174
|
}
|
|
161
|
-
}
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
res.on('end', () => {
|
|
165
|
-
process.stderr.write('grove-mcp-bridge: SSE stream ended\n');
|
|
166
|
-
process.exit(0);
|
|
167
|
-
});
|
|
175
|
+
});
|
|
168
176
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
);
|
|
177
|
+
res.on('end', () => {
|
|
178
|
+
process.stderr.write(`grove-mcp-bridge: SSE stream ended, reconnecting in ${reconnectDelay}ms\n`);
|
|
179
|
+
scheduleReconnect();
|
|
180
|
+
});
|
|
175
181
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
182
|
+
res.on('error', (err) => {
|
|
183
|
+
process.stderr.write(`grove-mcp-bridge: SSE error: ${err.message}, reconnecting in ${reconnectDelay}ms\n`);
|
|
184
|
+
scheduleReconnect();
|
|
185
|
+
});
|
|
186
|
+
}
|
|
179
187
|
);
|
|
180
|
-
process.exit(1);
|
|
181
|
-
});
|
|
182
188
|
|
|
183
|
-
sseReq.
|
|
189
|
+
sseReq.on('error', (err) => {
|
|
190
|
+
process.stderr.write(
|
|
191
|
+
`grove-mcp-bridge: cannot reach ${HOSTNAME}: ${err.message}, retrying in ${reconnectDelay}ms\n`
|
|
192
|
+
);
|
|
193
|
+
scheduleReconnect();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Server sends keepalive pings every 30s. If nothing arrives in 75s the
|
|
197
|
+
// connection is stale (server crash, network partition) — destroy and reconnect.
|
|
198
|
+
sseReq.setTimeout(75000);
|
|
199
|
+
sseReq.on('timeout', () => {
|
|
200
|
+
process.stderr.write(`grove-mcp-bridge: SSE idle timeout, reconnecting in ${reconnectDelay}ms\n`);
|
|
201
|
+
sseReq.destroy(); // triggers sseReq.on('error') or res.on('error') → scheduleReconnect()
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
sseReq.end();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function scheduleReconnect() {
|
|
208
|
+
// Null the session immediately so stdin messages are queued rather than
|
|
209
|
+
// fired at a dead endpoint during the reconnect delay window.
|
|
210
|
+
sessionPath = null;
|
|
211
|
+
affinityCookie = null;
|
|
212
|
+
setTimeout(() => {
|
|
213
|
+
connect();
|
|
214
|
+
}, reconnectDelay);
|
|
215
|
+
reconnectDelay = Math.min(reconnectDelay * 2, 30000);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
connect();
|
|
184
219
|
|
|
185
220
|
// ── stdin → POST ────────────────────────────────────────────────────────────
|
|
186
221
|
|