grove-mcp 1.0.7 → 1.0.8

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 +103 -9
  2. package/package.json +2 -2
package/index.js CHANGED
@@ -13,6 +13,16 @@
13
13
  * 2. Server sends: event: endpoint data: /messages/?session_id=<uuid>
14
14
  * 3. stdin lines → POST to session endpoint (with affinity cookie if set)
15
15
  * 4. SSE message events → stdout lines
16
+ *
17
+ * Reconnect behaviour:
18
+ * On SSE disconnect the bridge reconnects automatically with exponential
19
+ * back-off. Each reconnect creates a new server-side MCP session that
20
+ * must be re-initialized before tool calls can proceed. The bridge stores
21
+ * the original `initialize` request and `notifications/initialized`
22
+ * notification from Claude Desktop and silently replays them on every
23
+ * reconnect so the server session is fully initialized before any queued
24
+ * tool calls are flushed. The replay response is intercepted and NOT
25
+ * forwarded to Claude Desktop, which already considers itself initialized.
16
26
  */
17
27
 
18
28
  const https = require('https');
@@ -33,6 +43,8 @@ if (process.env.GROVE_OUTLINE_TOKEN)
33
43
  TOKEN_HEADERS['Grove-Outline-Token'] = process.env.GROVE_OUTLINE_TOKEN;
34
44
  if (process.env.GROVE_MATTERMOST_TOKEN)
35
45
  TOKEN_HEADERS['Grove-Mattermost-Token'] = process.env.GROVE_MATTERMOST_TOKEN;
46
+ if (process.env.GROVE_NOCODB_TOKEN)
47
+ TOKEN_HEADERS['Grove-Nocodb-Token'] = process.env.GROVE_NOCODB_TOKEN;
36
48
 
37
49
  // Per-user session isolation: scopes Redis session keys so multiple users
38
50
  // on the shared 3-pod cluster never overwrite each other's auth tokens.
@@ -92,6 +104,24 @@ function stripMcpCompat(raw) {
92
104
  }
93
105
  }
94
106
 
107
+ // ── MCP initialization replay state ─────────────────────────────────────────
108
+ //
109
+ // The MCP session lives on the server; each SSE reconnect creates a new one.
110
+ // Claude Desktop does NOT re-send `initialize` on reconnect — it assumes the
111
+ // session is continuous. We fix this by storing the original handshake and
112
+ // replaying it silently on every reconnect before flushing queued messages.
113
+
114
+ // Raw JSON lines captured from stdin on first connection.
115
+ let storedInitRequest = null; // {"method":"initialize", ...}
116
+ let storedInitNotification = null; // {"method":"notifications/initialized"}
117
+
118
+ // Bridge-internal init replay tracking.
119
+ let bridgeInitId = null; // ID we assign to the replayed initialize request
120
+ let awaitingBridgeInit = false; // true while waiting for the server's init response
121
+ let heldPending = []; // messages queued while awaiting bridge init response
122
+
123
+ // ── Transport state ──────────────────────────────────────────────────────────
124
+
95
125
  let sessionPath = null;
96
126
  let affinityCookie = null; // sticky-session cookie from SSE response headers
97
127
  const pending = [];
@@ -157,13 +187,57 @@ function connect() {
157
187
  // Fallback: server sent a plain path instead of JSON
158
188
  sessionPath = raw;
159
189
  }
160
- // Flush any stdin lines that arrived before the endpoint was ready
161
- for (const msg of pending) sendToServer(msg);
162
- pending.length = 0;
190
+
191
+ if (storedInitRequest !== null) {
192
+ // ── Reconnect path ─────────────────────────────────────────
193
+ // New server session — must replay the initialize handshake
194
+ // before any tool calls can proceed. Use a bridge-internal
195
+ // ID so we can intercept the response without forwarding it
196
+ // to Claude Desktop (which already considers itself initialized).
197
+ process.stderr.write('grove-mcp-bridge: reconnected — replaying MCP init\n');
198
+ bridgeInitId = '__bridge_init_' + Date.now();
199
+ awaitingBridgeInit = true;
200
+ // Move all pending messages to held; they'll be flushed after
201
+ // the server acknowledges initialization.
202
+ heldPending = pending.splice(0);
203
+ try {
204
+ const initMsg = JSON.parse(storedInitRequest);
205
+ sendToServer(JSON.stringify(Object.assign({}, initMsg, { id: bridgeInitId })));
206
+ } catch (_) {
207
+ // Malformed stored request — fall back to plain flush
208
+ awaitingBridgeInit = false;
209
+ bridgeInitId = null;
210
+ for (const msg of heldPending) sendToServer(msg);
211
+ heldPending = [];
212
+ }
213
+ } else {
214
+ // ── First connection path ──────────────────────────────────
215
+ // Claude Desktop's real initialize request is in pending.
216
+ // Flush everything; the server will process them in order.
217
+ for (const msg of pending) sendToServer(msg);
218
+ pending.length = 0;
219
+ }
163
220
  } 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.
221
+ if (awaitingBridgeInit) {
222
+ // Check whether this is the response to our replayed initialize.
223
+ try {
224
+ const msg = JSON.parse(raw);
225
+ if (msg.id === bridgeInitId) {
226
+ // Server acknowledged init — complete the handshake and
227
+ // flush held messages. Do NOT forward to Claude Desktop.
228
+ awaitingBridgeInit = false;
229
+ bridgeInitId = null;
230
+ if (storedInitNotification) sendToServer(storedInitNotification);
231
+ for (const msg of heldPending) sendToServer(msg);
232
+ heldPending = [];
233
+ process.stderr.write('grove-mcp-bridge: MCP init replay complete\n');
234
+ eventType = '';
235
+ continue; // skip stdout.write
236
+ }
237
+ } catch (_) {}
238
+ }
239
+
240
+ // Normal message — strip compat fields and forward to Claude Desktop.
167
241
  process.stdout.write(stripMcpCompat(raw) + '\n');
168
242
  }
169
243
 
@@ -209,6 +283,14 @@ function scheduleReconnect() {
209
283
  // fired at a dead endpoint during the reconnect delay window.
210
284
  sessionPath = null;
211
285
  affinityCookie = null;
286
+ // Reset bridge-init state so a clean replay happens on the next connect().
287
+ awaitingBridgeInit = false;
288
+ bridgeInitId = null;
289
+ // Move any messages held mid-replay back to pending so they aren't lost.
290
+ if (heldPending.length > 0) {
291
+ pending.unshift(...heldPending);
292
+ heldPending = [];
293
+ }
212
294
  setTimeout(() => {
213
295
  connect();
214
296
  }, reconnectDelay);
@@ -256,9 +338,21 @@ const rl = readline.createInterface({
256
338
  rl.on('line', (line) => {
257
339
  if (!line.trim()) return;
258
340
 
259
- if (sessionPath === null) {
260
- // Session not yet established — queue the message
261
- pending.push(line);
341
+ // Capture the initialize handshake for reconnect replay.
342
+ try {
343
+ const msg = JSON.parse(line);
344
+ if (msg.method === 'initialize') storedInitRequest = line;
345
+ if (msg.method === 'notifications/initialized') storedInitNotification = line;
346
+ } catch (_) {}
347
+
348
+ if (sessionPath === null || awaitingBridgeInit) {
349
+ // Either: session not yet established, or we're mid-replay.
350
+ // In both cases queue the message — it'll be flushed once ready.
351
+ if (awaitingBridgeInit) {
352
+ heldPending.push(line);
353
+ } else {
354
+ pending.push(line);
355
+ }
262
356
  } else {
263
357
  sendToServer(line);
264
358
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "grove-mcp",
3
- "version": "1.0.7",
4
- "description": "Claude Desktop bridge for the Grove MCP Server (Mattermost + Outline)",
3
+ "version": "1.0.8",
4
+ "description": "Claude Desktop bridge for the Grove MCP Server (Mattermost + Outline + NocoDB)",
5
5
  "main": "index.js",
6
6
  "bin": {
7
7
  "grove-mcp": "index.js"