claude-ide-bridge 2.16.1 → 2.16.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-ide-bridge",
3
- "version": "2.16.1",
3
+ "version": "2.16.2",
4
4
  "description": "Standalone MCP bridge for Claude Code IDE integration with any editor — 136+ tools for LSP, debugging, terminals, Git, GitHub, and more",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -110,6 +110,75 @@ const RECONNECT_DEBOUNCE_MS = 500;
110
110
  const POLL_INTERVAL_MS = 3000; // fallback poll when disconnected
111
111
  const MAX_PENDING_LINES = 1000;
112
112
 
113
+ // Backoff state — differentiated retry delays by error category
114
+ const MAX_UNREACHABLE_MS =
115
+ Number(process.env.SHIM_MAX_UNREACHABLE_MS) || 5 * 60 * 1000;
116
+ let firstUnreachableAt = 0; // epoch ms of first consecutive failure in current streak
117
+ let currentBackoffMs = 0; // 0 = not in a backoff sequence
118
+ let backoffTimer = null; // pending setTimeout for next reconnect attempt
119
+
120
+ // --- Backoff helpers ---
121
+
122
+ /**
123
+ * Returns the next retry delay (ms) for a given error category and mutates
124
+ * currentBackoffMs so successive calls produce exponential growth.
125
+ *
126
+ * Categories:
127
+ * "429" – rate-limited: start 1s, double, cap 30s, full jitter
128
+ * "unreachable" – ECONNREFUSED / ETIMEDOUT: start 1s, double, cap 30s
129
+ * "other" – falls back to flat POLL_INTERVAL_MS
130
+ */
131
+ function nextBackoffMs(errorType) {
132
+ if (errorType === "other") {
133
+ currentBackoffMs = 0;
134
+ return POLL_INTERVAL_MS;
135
+ }
136
+ const CAP_MS = 30_000;
137
+ currentBackoffMs =
138
+ currentBackoffMs === 0 ? 1000 : Math.min(currentBackoffMs * 2, CAP_MS);
139
+ // Full jitter for 429 (avoids thundering herd on a recovering bridge)
140
+ return errorType === "429"
141
+ ? Math.floor(Math.random() * currentBackoffMs)
142
+ : currentBackoffMs;
143
+ }
144
+
145
+ function resetBackoff() {
146
+ currentBackoffMs = 0;
147
+ firstUnreachableAt = 0;
148
+ if (backoffTimer) {
149
+ clearTimeout(backoffTimer);
150
+ backoffTimer = null;
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Schedule a reconnect attempt after a computed backoff delay.
156
+ * Replaces any pending backoffTimer. Does NOT call startPoll().
157
+ */
158
+ function scheduleBackoffReconnect(errorType) {
159
+ if (backoffTimer) clearTimeout(backoffTimer);
160
+ const delay = nextBackoffMs(errorType);
161
+ process.stderr.write(
162
+ `mcp-stdio-shim: Will retry in ${Math.round(delay / 1000)}s (reason: ${errorType}).\n`,
163
+ );
164
+ backoffTimer = setTimeout(() => {
165
+ backoffTimer = null;
166
+ const lockFile = findLockFile();
167
+ if (!lockFile) {
168
+ startPoll();
169
+ return;
170
+ }
171
+ let parsed;
172
+ try {
173
+ parsed = parseLock(lockFile);
174
+ } catch {
175
+ startPoll();
176
+ return;
177
+ }
178
+ connect(parsed.port, parsed.authToken);
179
+ }, delay);
180
+ }
181
+
113
182
  // --- Explicit args (bypass auto-discover and watcher) ---
114
183
  const explicitPort =
115
184
  process.argv[2] && process.argv[3] ? Number(process.argv[2]) : null;
@@ -139,18 +208,76 @@ function connect(port, authToken) {
139
208
  headers: { "x-claude-code-ide-authorization": authToken },
140
209
  });
141
210
 
211
+ // Per-connect flag: set when the error handler classifies this attempt as 429
212
+ // so the close handler knows not to call startPoll() and instead backs off.
213
+ let lastErrorWas429 = false;
214
+
142
215
  ws.on("error", (err) => {
143
- process.stderr.write(`mcp-stdio-shim: WebSocket error: ${err.message}\n`);
144
- // In explicit mode exit immediately; in auto-discover mode wait for a new lock
145
- if (explicitPort !== null) process.exit(1);
216
+ const code = err.code; // e.g. "ECONNREFUSED", "ETIMEDOUT", or undefined
217
+ process.stderr.write(
218
+ `mcp-stdio-shim: WebSocket error [${code ?? "unknown"}]: ${err.message}\n`,
219
+ );
220
+
221
+ if (explicitPort !== null) {
222
+ process.exit(1);
223
+ }
224
+
225
+ if (code === "ECONNREFUSED" || code === "ETIMEDOUT") {
226
+ if (firstUnreachableAt === 0) firstUnreachableAt = Date.now();
227
+ if (Date.now() - firstUnreachableAt > MAX_UNREACHABLE_MS) {
228
+ process.stderr.write(
229
+ "mcp-stdio-shim: Bridge unreachable for >5 minutes — giving up.\n",
230
+ );
231
+ process.exit(1);
232
+ }
233
+ scheduleBackoffReconnect("unreachable");
234
+ } else if (err.message?.includes("429")) {
235
+ lastErrorWas429 = true;
236
+ if (firstUnreachableAt === 0) firstUnreachableAt = Date.now();
237
+ if (Date.now() - firstUnreachableAt > MAX_UNREACHABLE_MS) {
238
+ process.stderr.write(
239
+ "mcp-stdio-shim: Bridge rate-limiting for >5 minutes — giving up.\n",
240
+ );
241
+ process.exit(1);
242
+ }
243
+ scheduleBackoffReconnect("429");
244
+ } else if (
245
+ err.message &&
246
+ (err.message.includes("401") || err.message.includes("403"))
247
+ ) {
248
+ process.stderr.write(
249
+ "mcp-stdio-shim: Auth failure (401/403) — exiting.\n",
250
+ );
251
+ process.exit(1);
252
+ }
253
+ // Other errors fall through to the close handler
146
254
  });
147
255
 
148
256
  ws.on("close", (code, reason) => {
257
+ const reasonStr = reason?.toString() || "";
149
258
  process.stderr.write(
150
- `mcp-stdio-shim: Connection closed (${code} ${reason})\n`,
259
+ `mcp-stdio-shim: Connection closed (${code} ${reasonStr})\n`,
151
260
  );
152
261
  if (explicitPort !== null) process.exit(0);
153
- // Auto-discover mode: start polling as fallback in case fs.watch misses the event
262
+
263
+ // Auth rejected — exit immediately, retrying will not help
264
+ if (code === 4001) {
265
+ process.stderr.write(
266
+ "mcp-stdio-shim: Authorization rejected (4001) — token mismatch. Exiting.\n",
267
+ );
268
+ process.exit(1);
269
+ }
270
+
271
+ // If the error handler already scheduled a backoff reconnect, don't also start polling
272
+ if (backoffTimer) return;
273
+
274
+ // 1006 = abnormal closure (often accompanies a 429 rejection before handshake)
275
+ if (lastErrorWas429) {
276
+ scheduleBackoffReconnect("429");
277
+ return;
278
+ }
279
+
280
+ // Normal closure or other codes: resume flat polling (existing behavior)
154
281
  startPoll();
155
282
  });
156
283
 
@@ -162,6 +289,7 @@ function connect(port, authToken) {
162
289
  ws.on("open", () => {
163
290
  process.stderr.write("mcp-stdio-shim: Connected.\n");
164
291
  stopPoll();
292
+ resetBackoff();
165
293
  // On reconnect (after a prior successful connection), drop stale pending messages —
166
294
  // they reference old session state (e.g. the previous initialize handshake) and must
167
295
  // not be replayed. A failed first attempt does NOT count — pendingLines must be kept
@@ -280,7 +408,16 @@ process.stdin.on("data", (chunk) => {
280
408
  const trimmed = line.trim();
281
409
  if (!trimmed) continue;
282
410
  if (ws && ws.readyState === WebSocket.OPEN) {
283
- ws.send(trimmed);
411
+ try {
412
+ ws.send(trimmed);
413
+ } catch (sendErr) {
414
+ process.stderr.write(
415
+ `mcp-stdio-shim: ws.send failed (${sendErr.message}) — queuing message.\n`,
416
+ );
417
+ if (pendingLines.length < MAX_PENDING_LINES) {
418
+ pendingLines.push(trimmed);
419
+ }
420
+ }
284
421
  } else if (pendingLines.length < MAX_PENDING_LINES) {
285
422
  pendingLines.push(trimmed);
286
423
  } else {
@@ -292,5 +429,30 @@ process.stdin.on("data", (chunk) => {
292
429
  });
293
430
 
294
431
  process.stdin.on("end", () => {
295
- if (ws) ws.close();
432
+ if (ws) {
433
+ try {
434
+ ws.close();
435
+ } catch {
436
+ /* ignore */
437
+ }
438
+ }
439
+ process.exit(0);
440
+ });
441
+
442
+ process.stdin.on("error", (err) => {
443
+ // EPIPE / ERR_STREAM_DESTROYED means the MCP host closed the pipe — clean shutdown.
444
+ if (err.code === "EPIPE" || err.code === "ERR_STREAM_DESTROYED") {
445
+ process.stderr.write(
446
+ `mcp-stdio-shim: stdin closed (${err.code}) — shutting down.\n`,
447
+ );
448
+ if (ws) {
449
+ try {
450
+ ws.close();
451
+ } catch {
452
+ /* ignore */
453
+ }
454
+ }
455
+ process.exit(0);
456
+ }
457
+ process.stderr.write(`mcp-stdio-shim: stdin error: ${err.message}\n`);
296
458
  });