@aiwerk/mcp-bridge 2.7.2 → 2.7.4

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.
@@ -70,7 +70,7 @@ export class AdaptivePromotion {
70
70
  server: entry.server,
71
71
  tool: entry.tool,
72
72
  callCount: recentCalls.length,
73
- lastCall: Math.max(...entry.callTimestamps)
73
+ lastCall: entry.callTimestamps.reduce((a, b) => a > b ? a : b, 0)
74
74
  });
75
75
  }
76
76
  }
@@ -85,7 +85,7 @@ export class AdaptivePromotion {
85
85
  entry.callTimestamps = entry.callTimestamps.filter(t => t > windowCutoff);
86
86
  // Remove entire entry if no calls within decay period
87
87
  const lastCall = entry.callTimestamps.length > 0
88
- ? Math.max(...entry.callTimestamps)
88
+ ? entry.callTimestamps.reduce((a, b) => a > b ? a : b, 0)
89
89
  : 0;
90
90
  if (lastCall === 0 || lastCall < decayCutoff) {
91
91
  this.usage.delete(k);
@@ -252,10 +252,10 @@ export class McpRouter {
252
252
  return { server, action: "call", tool, result: cachedResult };
253
253
  }
254
254
  }
255
- // Rate limit check BEFORE markUsed rejected calls should not keep connection alive
256
- const rateLimitResult = this.rateLimiter.checkAndIncrement(server, serverConfig.rateLimit);
257
- if (!rateLimitResult.allowed) {
258
- return this.error("mcp_error", rateLimitResult.error || "Rate limit reached");
255
+ // Rate limit: check BEFORE call, increment AFTER success
256
+ const rateLimitCheck = this.rateLimiter.checkLimit(server, serverConfig.rateLimit);
257
+ if (!rateLimitCheck.allowed) {
258
+ return this.error("mcp_error", rateLimitCheck.error || "Rate limit reached");
259
259
  }
260
260
  this.markUsed(server);
261
261
  const callOutcome = await this.callToolWithRetry(server, tool, params ?? {}, state.transport);
@@ -263,6 +263,8 @@ export class McpRouter {
263
263
  if (response.error) {
264
264
  return this.error("mcp_error", response.error.message, undefined, response.error.code);
265
265
  }
266
+ // Only increment usage counter on successful calls
267
+ const rateLimitIncrement = this.rateLimiter.increment(server, serverConfig.rateLimit);
266
268
  // Record usage for adaptive promotion
267
269
  if (this.promotion) {
268
270
  this.promotion.recordCall(server, tool);
@@ -278,7 +280,7 @@ export class McpRouter {
278
280
  action: "call",
279
281
  tool,
280
282
  result,
281
- ...(rateLimitResult.warning ? { warning: rateLimitResult.warning } : {}),
283
+ ...(rateLimitIncrement.warning ? { warning: rateLimitIncrement.warning } : {}),
282
284
  ...(callOutcome.retries > 0 ? { retries: callOutcome.retries } : {})
283
285
  };
284
286
  }
@@ -10,7 +10,15 @@ export interface RateLimitResult {
10
10
  export declare class RateLimiter {
11
11
  private readonly usageDir;
12
12
  constructor(usageDir?: string);
13
+ /**
14
+ * Check if a call is allowed (without incrementing). Use with increment() after success.
15
+ */
13
16
  checkLimit(serverId: string, config?: RateLimitConfig): RateLimitResult;
17
+ /**
18
+ * Increment usage counters after a successful call. Returns warning if near limit.
19
+ */
20
+ increment(serverId: string, config?: RateLimitConfig): RateLimitResult;
21
+ /** @deprecated Use checkLimit() + increment() separately. Kept for backward compatibility. */
14
22
  checkAndIncrement(serverId: string, config?: RateLimitConfig): RateLimitResult;
15
23
  getUsage(serverId: string): {
16
24
  daily: number;
@@ -21,6 +21,9 @@ export class RateLimiter {
21
21
  constructor(usageDir) {
22
22
  this.usageDir = usageDir ?? join(homedir(), ".mcp-bridge", "usage");
23
23
  }
24
+ /**
25
+ * Check if a call is allowed (without incrementing). Use with increment() after success.
26
+ */
24
27
  checkLimit(serverId, config) {
25
28
  const dailyLimit = isPositiveLimit(config?.maxCallsPerDay) ? config.maxCallsPerDay : undefined;
26
29
  const monthlyLimit = isPositiveLimit(config?.maxCallsPerMonth) ? config.maxCallsPerMonth : undefined;
@@ -45,29 +48,19 @@ export class RateLimiter {
45
48
  }
46
49
  return { allowed: true };
47
50
  }
48
- checkAndIncrement(serverId, config) {
51
+ /**
52
+ * Increment usage counters after a successful call. Returns warning if near limit.
53
+ */
54
+ increment(serverId, config) {
49
55
  const dailyLimit = isPositiveLimit(config?.maxCallsPerDay) ? config.maxCallsPerDay : undefined;
50
56
  const monthlyLimit = isPositiveLimit(config?.maxCallsPerMonth) ? config.maxCallsPerMonth : undefined;
51
57
  if (!dailyLimit && !monthlyLimit) {
52
58
  return { allowed: true };
53
59
  }
54
- // Single loadUsage call — check + increment in one pass (avoids double file read)
55
60
  const { usage, changed } = this.loadUsage(serverId);
56
61
  if (changed) {
57
62
  this.saveUsage(serverId, usage);
58
63
  }
59
- if (dailyLimit && usage.daily.count >= dailyLimit) {
60
- return {
61
- allowed: false,
62
- error: `❌ Rate limit reached for ${serverId}: ${usage.daily.count}/${dailyLimit} daily calls used. Resets at midnight UTC. To adjust: mcp-bridge limit ${serverId} --daily ${nextSuggestedLimit(dailyLimit)}. To check usage: mcp-bridge usage. To disable limit: mcp-bridge limit ${serverId} --daily 0`
63
- };
64
- }
65
- if (monthlyLimit && usage.monthly.count >= monthlyLimit) {
66
- return {
67
- allowed: false,
68
- error: `❌ Rate limit reached for ${serverId}: ${usage.monthly.count}/${monthlyLimit} monthly calls used. Resets on the 1st of each month at midnight UTC. To adjust: mcp-bridge limit ${serverId} --monthly ${nextSuggestedLimit(monthlyLimit)}. To check usage: mcp-bridge usage. To disable limit: mcp-bridge limit ${serverId} --monthly 0`
69
- };
70
- }
71
64
  usage.daily.count += 1;
72
65
  usage.monthly.count += 1;
73
66
  this.saveUsage(serverId, usage);
@@ -85,6 +78,13 @@ export class RateLimiter {
85
78
  }
86
79
  return { allowed: true };
87
80
  }
81
+ /** @deprecated Use checkLimit() + increment() separately. Kept for backward compatibility. */
82
+ checkAndIncrement(serverId, config) {
83
+ const check = this.checkLimit(serverId, config);
84
+ if (!check.allowed)
85
+ return check;
86
+ return this.increment(serverId, config);
87
+ }
88
88
  getUsage(serverId) {
89
89
  const { usage, changed } = this.loadUsage(serverId);
90
90
  if (changed) {
@@ -54,7 +54,7 @@ export class FileTokenStore {
54
54
  }
55
55
  tokenPath(serverName) {
56
56
  // Sanitize server name to prevent path traversal
57
- const safe = serverName.replace(/[^a-zA-Z0-9_-]/g, "_");
57
+ const safe = encodeURIComponent(serverName);
58
58
  return join(this.tokensDir, `${safe}.json`);
59
59
  }
60
60
  ensureDir() {
@@ -259,6 +259,7 @@ export class SseTransport extends BaseTransport {
259
259
  this.sseAbortController.abort();
260
260
  this.sseAbortController = null;
261
261
  }
262
+ this.endpointUrl = null;
262
263
  for (const [, controller] of this.pendingRequestControllers) {
263
264
  controller.abort();
264
265
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwerk/mcp-bridge",
3
- "version": "2.7.2",
3
+ "version": "2.7.4",
4
4
  "description": "Standalone MCP server that multiplexes multiple MCP servers into one interface",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",
@@ -44,7 +44,7 @@
44
44
  "build": "tsc",
45
45
  "test": "node --import tsx --test tests/*.test.ts",
46
46
  "typecheck": "tsc --noEmit",
47
- "prepublishOnly": "tsc && bash scripts/validate-recipes.sh",
47
+ "prepublishOnly": "tsc && bash scripts/validate-recipes.sh && node -e \"const v=require('./package.json').version;const fs=require('fs');const cl=fs.readFileSync('CHANGELOG.md','utf8');if(!cl.includes('['+v+']')){console.error('ERROR: CHANGELOG.md missing entry for v'+v);process.exit(1)}\"",
48
48
  "validate-recipe": "npx tsx bin/validate-recipe.ts",
49
49
  "lint": "eslint src/",
50
50
  "format": "prettier --write src/",
@@ -317,7 +317,32 @@ elif [[ -f "$ENV_VARS_FILE" ]] && [[ -s "$ENV_VARS_FILE" ]]; then
317
317
  ENV_VAR_NAME="$(head -n 1 "$ENV_VARS_FILE" | tr -d '[:space:]')"
318
318
  fi
319
319
 
320
- if [[ -n "$ENV_VAR_NAME" ]]; then
320
+ # Check if this is an OAuth2 Authorization Code server (browser login, not API key)
321
+ OAUTH2_AUTH_CODE="false"
322
+ if [[ "$RECIPE_FORMAT" == "v2" ]]; then
323
+ OAUTH2_AUTH_CODE=$(python3 -c "
324
+ import json, sys
325
+ with open(sys.argv[1]) as f:
326
+ r = json.load(f)
327
+ auth = r.get('auth', {})
328
+ # OAuth2 auth code: explicitly requires grantType=authorization_code
329
+ # and no envVars (login via browser, not API key)
330
+ if auth.get('type') == 'oauth2' and auth.get('grantType') == 'authorization_code':
331
+ if not auth.get('envVars'):
332
+ print('true')
333
+ else:
334
+ print('false')
335
+ else:
336
+ print('false')
337
+ " "$RECIPE_FILE" 2>/dev/null)
338
+ fi
339
+
340
+ if [[ "$OAUTH2_AUTH_CODE" == "true" ]]; then
341
+ echo ""
342
+ echo "🔐 This server uses OAuth2 browser login (no API key needed)."
343
+ echo "After config is saved, we'll open your browser for authentication."
344
+ SKIP_TOKEN_PROMPT="true"
345
+ elif [[ -n "$ENV_VAR_NAME" ]]; then
321
346
  TOKEN_URL="$(get_token_url)"
322
347
  [[ -n "$TOKEN_URL" ]] && echo "Get your API token here: ${TOKEN_URL}"
323
348
 
@@ -416,7 +441,38 @@ with open(config_path, "w", encoding="utf-8") as f:
416
441
  print(f"✅ Configuration merged for: {server_name} (recipe {recipe_format})")
417
442
  PY
418
443
 
419
- # 5. Gateway restart
444
+ # 5. OAuth2 browser login (if applicable)
445
+ if [[ "$OAUTH2_AUTH_CODE" == "true" ]]; then
446
+ echo ""
447
+ echo "🔐 Starting OAuth2 login for ${SERVER_TITLE}..."
448
+
449
+ # Find the mcp-bridge CLI
450
+ MCP_BRIDGE_BIN=""
451
+ if command -v mcp-bridge &>/dev/null; then
452
+ MCP_BRIDGE_BIN="mcp-bridge"
453
+ elif [[ -x "$(dirname "$0")/../dist/bin/mcp-bridge.js" ]]; then
454
+ MCP_BRIDGE_BIN="node $(dirname "$0")/../dist/bin/mcp-bridge.js"
455
+ elif command -v npx &>/dev/null; then
456
+ MCP_BRIDGE_BIN="npx @aiwerk/mcp-bridge"
457
+ fi
458
+
459
+ if [[ -n "$MCP_BRIDGE_BIN" ]]; then
460
+ # Detect if we have a browser available
461
+ if command -v xdg-open &>/dev/null || command -v open &>/dev/null || command -v wslview &>/dev/null; then
462
+ echo "Opening browser for authentication..."
463
+ $MCP_BRIDGE_BIN auth login "$SERVER_NAME"
464
+ else
465
+ echo "No browser detected (headless environment)."
466
+ echo "Using device code flow — follow the instructions below:"
467
+ $MCP_BRIDGE_BIN auth login "$SERVER_NAME" --device-code
468
+ fi
469
+ else
470
+ echo "⚠️ mcp-bridge CLI not found. Run manually after install:"
471
+ echo " mcp-bridge auth login ${SERVER_NAME}"
472
+ fi
473
+ fi
474
+
475
+ # 6. Gateway restart
420
476
  echo ""
421
477
  echo "✅ ${SERVER_TITLE} MCP Server installed."
422
478
  echo "Restart mcp-bridge to pick up the new server configuration."