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.
Files changed (2) hide show
  1. package/index.js +107 -72
  2. 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
- // Capture nginx sticky-session cookie so POSTs reach the same pod
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
- let buf = '';
125
- let eventType = '';
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
- res.on('data', (chunk) => {
128
- buf += chunk.toString('utf8');
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
- let nl;
131
- while ((nl = buf.indexOf('\n')) !== -1) {
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
- if (line.startsWith('event:')) {
136
- eventType = line.slice(6).trim();
137
- } else if (line.startsWith('data:')) {
138
- const raw = line.slice(5).trim();
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
- if (eventType === 'endpoint') {
141
- try {
142
- sessionPath = JSON.parse(raw).uri;
143
- } catch (_) {
144
- // Fallback: server sent a plain path instead of JSON
145
- sessionPath = raw;
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
- eventType = '';
158
- } else if (line === '') {
159
- eventType = '';
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
- res.on('error', (err) => {
170
- process.stderr.write(`grove-mcp-bridge: SSE error: ${err.message}\n`);
171
- process.exit(1);
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
- sseReq.on('error', (err) => {
177
- process.stderr.write(
178
- `grove-mcp-bridge: cannot reach ${HOSTNAME}: ${err.message}\n`
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.end();
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grove-mcp",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "Claude Desktop bridge for the Grove MCP Server (Mattermost + Outline)",
5
5
  "main": "index.js",
6
6
  "bin": {