@aiwerk/mcp-bridge 2.6.8 → 2.7.1

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/README.md CHANGED
@@ -4,7 +4,9 @@
4
4
  [![npm version](https://img.shields.io/npm/v/@aiwerk/mcp-bridge.svg)](https://www.npmjs.com/package/@aiwerk/mcp-bridge)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
6
 
7
- Multiplex multiple MCP servers into one interface. One config, one connection, all your tools.
7
+ **Your AI, Connected to Everything.** Multiplex multiple MCP servers into one interface. One config, one connection, all your tools.
8
+
9
+ 🌐 **[aiwerkmcp.com](https://aiwerkmcp.com)** — Learn more about the AIWerk MCP Platform
8
10
 
9
11
  Works with **Claude Desktop**, **Cursor**, **Windsurf**, **Cline**, **OpenClaw**, or any MCP client.
10
12
 
@@ -591,6 +593,7 @@ For production deployments with high security requirements, consider adding an e
591
593
  | ✅ | OAuth2 Client Credentials | 2.1.0 |
592
594
  | ✅ | OAuth2 Authorization Code + PKCE | 2.5.0 |
593
595
  | ✅ | OAuth2 Device Code flow (headless) | 2.6.0 |
596
+ | 🔜 | Auto-discovery (zero-config server registration) | planned |
594
597
  | 🔜 | Hosted bridge (bridge.aiwerk.ch) | planned |
595
598
  | 🔜 | Remote catalog integration | planned |
596
599
  | 🔜 | OpenTelemetry / Prometheus metrics | planned |
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync, existsSync } from "fs";
3
- import { join, dirname, resolve } from "path";
2
+ import { readFileSync, existsSync, writeFileSync } from "fs";
3
+ import { join, dirname, resolve, extname } from "path";
4
4
  import { fileURLToPath } from "url";
5
- import { platform } from "os";
5
+ import { platform, homedir } from "os";
6
6
  import { execFileSync } from "child_process";
7
7
  import { loadConfig, initConfigDir } from "../src/config.js";
8
8
  import { StandaloneServer } from "../src/standalone-server.js";
@@ -10,6 +10,7 @@ import { PACKAGE_VERSION } from "../src/protocol.js";
10
10
  import { checkForUpdate, runUpdate } from "../src/update-checker.js";
11
11
  import { FileTokenStore } from "../src/token-store.js";
12
12
  import { performAuthCodeLogin, performDeviceCodeLogin } from "../src/cli-auth.js";
13
+ import { RateLimiter } from "../src/rate-limiter.js";
13
14
  const __filename = fileURLToPath(import.meta.url);
14
15
  const __dirname = dirname(__filename);
15
16
  // After tsc, this file lives at dist/bin/mcp-bridge.js.
@@ -96,6 +97,22 @@ function parseArgs(argv) {
96
97
  case "--offline":
97
98
  args.offline = true;
98
99
  break;
100
+ case "--daily":
101
+ i++;
102
+ args.daily = parseInt(argv[i], 10);
103
+ if (isNaN(args.daily)) {
104
+ process.stderr.write("Error: --daily requires a number\n");
105
+ process.exit(1);
106
+ }
107
+ break;
108
+ case "--monthly":
109
+ i++;
110
+ args.monthly = parseInt(argv[i], 10);
111
+ if (isNaN(args.monthly)) {
112
+ process.stderr.write("Error: --monthly requires a number\n");
113
+ process.exit(1);
114
+ }
115
+ break;
99
116
  case "init":
100
117
  args.command = "init";
101
118
  break;
@@ -114,6 +131,12 @@ function parseArgs(argv) {
114
131
  case "update":
115
132
  args.command = "update";
116
133
  break;
134
+ case "usage":
135
+ args.command = "usage";
136
+ break;
137
+ case "limit":
138
+ args.command = "limit";
139
+ break;
117
140
  case "auth":
118
141
  args.command = "auth";
119
142
  // Consume subcommand
@@ -152,6 +175,9 @@ Usage:
152
175
  mcp-bridge catalog [--offline] List available servers
153
176
  mcp-bridge servers List configured servers
154
177
  mcp-bridge search <query> Search catalog by keyword
178
+ mcp-bridge usage Show current per-server call usage
179
+ mcp-bridge limit <server> [--daily N] [--monthly N]
180
+ Set per-server rate limits (0 = unlimited)
155
181
  mcp-bridge update [--check] Check for / install updates
156
182
  mcp-bridge auth login <server> Authenticate with an OAuth2 server
157
183
  mcp-bridge auth logout <server> Remove stored token for a server
@@ -235,6 +261,118 @@ function cmdSearch(query, logger) {
235
261
  });
236
262
  process.stdout.write("\n");
237
263
  }
264
+ function resolveConfigPath(configPath) {
265
+ if (!configPath) {
266
+ return join(homedir(), ".mcp-bridge", "config.json");
267
+ }
268
+ if (configPath.endsWith("/") || configPath.endsWith("\\") || !extname(configPath)) {
269
+ return join(configPath, "config.json");
270
+ }
271
+ return configPath;
272
+ }
273
+ function cmdUsage(configPath, logger) {
274
+ try {
275
+ const limiter = new RateLimiter();
276
+ const usage = limiter.getAllUsage();
277
+ let configServers = {};
278
+ try {
279
+ const config = loadConfig({ configPath, logger });
280
+ configServers = config.servers ?? {};
281
+ }
282
+ catch {
283
+ // Show usage files even when config is missing/unreadable.
284
+ }
285
+ const names = new Set([...Object.keys(usage), ...Object.keys(configServers)]);
286
+ if (names.size === 0) {
287
+ process.stdout.write("No usage data found.\n");
288
+ return;
289
+ }
290
+ process.stdout.write("\nRate limit usage (note: cached calls are not counted):\n\n");
291
+ process.stdout.write(" Server Daily Monthly Limits\n");
292
+ process.stdout.write(" " + "─".repeat(78) + "\n");
293
+ for (const server of [...names].sort((a, b) => a.localeCompare(b))) {
294
+ const counts = usage[server] ?? { daily: 0, monthly: 0 };
295
+ const limit = configServers[server]?.rateLimit;
296
+ const dailyLimit = typeof limit?.maxCallsPerDay === "number" && limit.maxCallsPerDay > 0
297
+ ? limit.maxCallsPerDay
298
+ : "-";
299
+ const monthlyLimit = typeof limit?.maxCallsPerMonth === "number" && limit.maxCallsPerMonth > 0
300
+ ? limit.maxCallsPerMonth
301
+ : "-";
302
+ process.stdout.write(` ${server.padEnd(16)}${`${counts.daily}`.padEnd(13)}${`${counts.monthly}`.padEnd(13)}daily=${dailyLimit} monthly=${monthlyLimit}\n`);
303
+ }
304
+ process.stdout.write("\n");
305
+ }
306
+ catch (err) {
307
+ logger.error(err instanceof Error ? err.message : String(err));
308
+ process.exit(1);
309
+ }
310
+ }
311
+ function cmdLimit(args, logger) {
312
+ const server = args.positional[0];
313
+ if (!server) {
314
+ process.stderr.write("Usage: mcp-bridge limit <server> --daily <n> --monthly <n>\n");
315
+ process.exit(1);
316
+ }
317
+ const hasDaily = typeof args.daily === "number";
318
+ const hasMonthly = typeof args.monthly === "number";
319
+ if (!hasDaily && !hasMonthly) {
320
+ process.stderr.write("Error: provide at least one limit flag (--daily or --monthly)\n");
321
+ process.exit(1);
322
+ }
323
+ if (hasDaily && args.daily < 0) {
324
+ process.stderr.write("Error: --daily must be >= 0\n");
325
+ process.exit(1);
326
+ }
327
+ if (hasMonthly && args.monthly < 0) {
328
+ process.stderr.write("Error: --monthly must be >= 0\n");
329
+ process.exit(1);
330
+ }
331
+ const resolvedPath = resolveConfigPath(args.configPath);
332
+ if (!existsSync(resolvedPath)) {
333
+ logger.error(`Config file not found: ${resolvedPath}`);
334
+ process.exit(1);
335
+ }
336
+ try {
337
+ const raw = JSON.parse(readFileSync(resolvedPath, "utf-8"));
338
+ if (!raw.servers || typeof raw.servers !== "object" || !raw.servers[server]) {
339
+ logger.error(`Server "${server}" not found in config`);
340
+ process.exit(1);
341
+ }
342
+ if (!raw.servers[server].rateLimit || typeof raw.servers[server].rateLimit !== "object") {
343
+ raw.servers[server].rateLimit = {};
344
+ }
345
+ if (hasDaily) {
346
+ if (args.daily === 0) {
347
+ delete raw.servers[server].rateLimit.maxCallsPerDay;
348
+ }
349
+ else {
350
+ raw.servers[server].rateLimit.maxCallsPerDay = args.daily;
351
+ }
352
+ }
353
+ if (hasMonthly) {
354
+ if (args.monthly === 0) {
355
+ delete raw.servers[server].rateLimit.maxCallsPerMonth;
356
+ }
357
+ else {
358
+ raw.servers[server].rateLimit.maxCallsPerMonth = args.monthly;
359
+ }
360
+ }
361
+ if (raw.servers[server].rateLimit.maxCallsPerDay === undefined &&
362
+ raw.servers[server].rateLimit.maxCallsPerMonth === undefined) {
363
+ delete raw.servers[server].rateLimit;
364
+ }
365
+ writeFileSync(resolvedPath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
366
+ const effectiveDaily = raw.servers[server].rateLimit?.maxCallsPerDay ?? "unlimited";
367
+ const effectiveMonthly = raw.servers[server].rateLimit?.maxCallsPerMonth ?? "unlimited";
368
+ process.stdout.write(`Updated limits for ${server}: daily=${effectiveDaily}, monthly=${effectiveMonthly}\n` +
369
+ `Check usage with: mcp-bridge usage\n`);
370
+ }
371
+ catch (err) {
372
+ logger.error(err instanceof Error ? err.message : String(err));
373
+ process.exit(1);
374
+ }
375
+ }
238
376
  function cmdInstall(serverName, logger) {
239
377
  const scriptDir = join(PACKAGE_ROOT, "scripts");
240
378
  try {
@@ -460,6 +598,12 @@ async function main() {
460
598
  }
461
599
  cmdSearch(args.positional[0], logger);
462
600
  break;
601
+ case "usage":
602
+ cmdUsage(args.configPath, logger);
603
+ break;
604
+ case "limit":
605
+ cmdLimit(args, logger);
606
+ break;
463
607
  case "install":
464
608
  if (args.positional.length === 0) {
465
609
  process.stderr.write("Usage: mcp-bridge install <server>\n");
@@ -12,6 +12,8 @@ export { McpRouter } from "./mcp-router.js";
12
12
  export type { RouterToolHint, RouterServerStatus, RouterDispatchResponse, RouterTransportRefs } from "./mcp-router.js";
13
13
  export { ResultCache, createResultCacheKey, stableStringify } from "./result-cache.js";
14
14
  export type { ResultCacheConfig, ResultCacheStats } from "./result-cache.js";
15
+ export { RateLimiter } from "./rate-limiter.js";
16
+ export type { RateLimitConfig, RateLimitResult } from "./rate-limiter.js";
15
17
  export { ToolResolver } from "./tool-resolution.js";
16
18
  export type { ToolResolutionResult, ToolResolutionCandidate } from "./tool-resolution.js";
17
19
  export { convertJsonSchemaToTypeBox, createToolParameters, setTypeBoxLoader, setSchemaLogger } from "./schema-convert.js";
package/dist/src/index.js CHANGED
@@ -13,6 +13,7 @@ export { performAuthCodeLogin, generateCodeVerifier, computeCodeChallenge } from
13
13
  export { McpRouter } from "./mcp-router.js";
14
14
  // Result cache
15
15
  export { ResultCache, createResultCacheKey, stableStringify } from "./result-cache.js";
16
+ export { RateLimiter } from "./rate-limiter.js";
16
17
  export { ToolResolver } from "./tool-resolution.js";
17
18
  // Schema conversion
18
19
  export { convertJsonSchemaToTypeBox, createToolParameters, setTypeBoxLoader, setSchemaLogger } from "./schema-convert.js";
@@ -39,6 +39,7 @@ export type RouterDispatchResponse = {
39
39
  tool: string;
40
40
  result: any;
41
41
  retries?: number;
42
+ warning?: string;
42
43
  } | {
43
44
  server: string;
44
45
  action: "schema";
@@ -110,6 +111,7 @@ export declare class McpRouter {
110
111
  private readonly states;
111
112
  private readonly toolResolver;
112
113
  private readonly tokenManager;
114
+ private readonly rateLimiter;
113
115
  private readonly requestIdState;
114
116
  private intentRouter;
115
117
  private promotion;
@@ -12,6 +12,7 @@ import { ResultCache, createResultCacheKey } from "./result-cache.js";
12
12
  import { ToolResolver } from "./tool-resolution.js";
13
13
  import { OAuth2TokenManager } from "./oauth2-token-manager.js";
14
14
  import { FileTokenStore } from "./token-store.js";
15
+ import { RateLimiter } from "./rate-limiter.js";
15
16
  const DEFAULT_IDLE_TIMEOUT_MS = 10 * 60 * 1000;
16
17
  const DEFAULT_CONNECT_ERROR_COOLDOWN_MS = 10 * 1000;
17
18
  const DEFAULT_MAX_CONCURRENT = 5;
@@ -30,6 +31,7 @@ export class McpRouter {
30
31
  states = new Map();
31
32
  toolResolver;
32
33
  tokenManager;
34
+ rateLimiter;
33
35
  requestIdState = { value: 0 };
34
36
  intentRouter = null;
35
37
  promotion = null;
@@ -55,6 +57,7 @@ export class McpRouter {
55
57
  this.maxBatchSize = clientConfig.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE;
56
58
  this.toolResolver = new ToolResolver(Object.keys(servers));
57
59
  this.tokenManager = new OAuth2TokenManager(logger, new FileTokenStore());
60
+ this.rateLimiter = new RateLimiter();
58
61
  if (clientConfig.adaptivePromotion?.enabled) {
59
62
  this.promotion = new AdaptivePromotion(clientConfig.adaptivePromotion, logger);
60
63
  }
@@ -249,6 +252,11 @@ export class McpRouter {
249
252
  return { server, action: "call", tool, result: cachedResult };
250
253
  }
251
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");
259
+ }
252
260
  this.markUsed(server);
253
261
  const callOutcome = await this.callToolWithRetry(server, tool, params ?? {}, state.transport);
254
262
  const response = callOutcome.response;
@@ -270,6 +278,7 @@ export class McpRouter {
270
278
  action: "call",
271
279
  tool,
272
280
  result,
281
+ ...(rateLimitResult.warning ? { warning: rateLimitResult.warning } : {}),
273
282
  ...(callOutcome.retries > 0 ? { retries: callOutcome.retries } : {})
274
283
  };
275
284
  }
@@ -0,0 +1,29 @@
1
+ export interface RateLimitConfig {
2
+ maxCallsPerDay?: number;
3
+ maxCallsPerMonth?: number;
4
+ }
5
+ export interface RateLimitResult {
6
+ allowed: boolean;
7
+ warning?: string;
8
+ error?: string;
9
+ }
10
+ export declare class RateLimiter {
11
+ private readonly usageDir;
12
+ constructor(usageDir?: string);
13
+ checkLimit(serverId: string, config?: RateLimitConfig): RateLimitResult;
14
+ checkAndIncrement(serverId: string, config?: RateLimitConfig): RateLimitResult;
15
+ getUsage(serverId: string): {
16
+ daily: number;
17
+ monthly: number;
18
+ };
19
+ getAllUsage(): Record<string, {
20
+ daily: number;
21
+ monthly: number;
22
+ dailyLimit?: number;
23
+ monthlyLimit?: number;
24
+ }>;
25
+ reset(serverId: string): void;
26
+ private loadUsage;
27
+ private saveUsage;
28
+ private serverFilePath;
29
+ }
@@ -0,0 +1,178 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
2
+ import { homedir } from "os";
3
+ import { join } from "path";
4
+ function utcDate(now) {
5
+ return now.toISOString().slice(0, 10);
6
+ }
7
+ function utcMonth(now) {
8
+ return now.toISOString().slice(0, 7);
9
+ }
10
+ function isPositiveLimit(value) {
11
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
12
+ }
13
+ function warningThreshold(limit) {
14
+ return Math.max(1, Math.ceil(limit * 0.8));
15
+ }
16
+ function nextSuggestedLimit(limit) {
17
+ return Math.ceil(limit * 1.5);
18
+ }
19
+ export class RateLimiter {
20
+ usageDir;
21
+ constructor(usageDir) {
22
+ this.usageDir = usageDir ?? join(homedir(), ".mcp-bridge", "usage");
23
+ }
24
+ checkLimit(serverId, config) {
25
+ const dailyLimit = isPositiveLimit(config?.maxCallsPerDay) ? config.maxCallsPerDay : undefined;
26
+ const monthlyLimit = isPositiveLimit(config?.maxCallsPerMonth) ? config.maxCallsPerMonth : undefined;
27
+ if (!dailyLimit && !monthlyLimit) {
28
+ return { allowed: true };
29
+ }
30
+ const { usage, changed } = this.loadUsage(serverId);
31
+ if (changed) {
32
+ this.saveUsage(serverId, usage);
33
+ }
34
+ if (dailyLimit && usage.daily.count >= dailyLimit) {
35
+ return {
36
+ allowed: false,
37
+ 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`
38
+ };
39
+ }
40
+ if (monthlyLimit && usage.monthly.count >= monthlyLimit) {
41
+ return {
42
+ allowed: false,
43
+ 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`
44
+ };
45
+ }
46
+ return { allowed: true };
47
+ }
48
+ checkAndIncrement(serverId, config) {
49
+ const dailyLimit = isPositiveLimit(config?.maxCallsPerDay) ? config.maxCallsPerDay : undefined;
50
+ const monthlyLimit = isPositiveLimit(config?.maxCallsPerMonth) ? config.maxCallsPerMonth : undefined;
51
+ if (!dailyLimit && !monthlyLimit) {
52
+ return { allowed: true };
53
+ }
54
+ // Single loadUsage call — check + increment in one pass (avoids double file read)
55
+ const { usage, changed } = this.loadUsage(serverId);
56
+ if (changed) {
57
+ this.saveUsage(serverId, usage);
58
+ }
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
+ usage.daily.count += 1;
72
+ usage.monthly.count += 1;
73
+ this.saveUsage(serverId, usage);
74
+ if (dailyLimit && usage.daily.count >= warningThreshold(dailyLimit) && usage.daily.count < dailyLimit) {
75
+ return {
76
+ allowed: true,
77
+ warning: `⚠️ ${serverId}: 80% of daily limit used (${usage.daily.count}/${dailyLimit}). Adjust with: mcp-bridge limit ${serverId} --daily <number>`
78
+ };
79
+ }
80
+ if (monthlyLimit && usage.monthly.count >= warningThreshold(monthlyLimit) && usage.monthly.count < monthlyLimit) {
81
+ return {
82
+ allowed: true,
83
+ warning: `⚠️ ${serverId}: 80% of monthly limit used (${usage.monthly.count}/${monthlyLimit}). Adjust with: mcp-bridge limit ${serverId} --monthly <number>`
84
+ };
85
+ }
86
+ return { allowed: true };
87
+ }
88
+ getUsage(serverId) {
89
+ const { usage, changed } = this.loadUsage(serverId);
90
+ if (changed) {
91
+ this.saveUsage(serverId, usage);
92
+ }
93
+ return { daily: usage.daily.count, monthly: usage.monthly.count };
94
+ }
95
+ getAllUsage() {
96
+ const all = {};
97
+ if (!existsSync(this.usageDir)) {
98
+ return all;
99
+ }
100
+ for (const fileName of readdirSync(this.usageDir)) {
101
+ if (!fileName.endsWith(".json"))
102
+ continue;
103
+ const encodedId = fileName.slice(0, -5);
104
+ let serverId;
105
+ try {
106
+ serverId = decodeURIComponent(encodedId);
107
+ }
108
+ catch {
109
+ serverId = encodedId;
110
+ }
111
+ const { usage, changed } = this.loadUsage(serverId);
112
+ if (changed) {
113
+ this.saveUsage(serverId, usage);
114
+ }
115
+ all[serverId] = {
116
+ daily: usage.daily.count,
117
+ monthly: usage.monthly.count
118
+ };
119
+ }
120
+ return all;
121
+ }
122
+ reset(serverId) {
123
+ const now = new Date();
124
+ this.saveUsage(serverId, {
125
+ daily: { date: utcDate(now), count: 0 },
126
+ monthly: { month: utcMonth(now), count: 0 }
127
+ });
128
+ }
129
+ loadUsage(serverId) {
130
+ const now = new Date();
131
+ const expectedDate = utcDate(now);
132
+ const expectedMonth = utcMonth(now);
133
+ const filePath = this.serverFilePath(serverId);
134
+ const fallback = {
135
+ daily: { date: expectedDate, count: 0 },
136
+ monthly: { month: expectedMonth, count: 0 }
137
+ };
138
+ if (!existsSync(filePath)) {
139
+ return { usage: fallback, changed: false };
140
+ }
141
+ let parsed;
142
+ try {
143
+ parsed = JSON.parse(readFileSync(filePath, "utf-8"));
144
+ }
145
+ catch {
146
+ return { usage: fallback, changed: true };
147
+ }
148
+ const dailyRaw = typeof parsed === "object" && parsed !== null ? parsed.daily : undefined;
149
+ const monthlyRaw = typeof parsed === "object" && parsed !== null ? parsed.monthly : undefined;
150
+ let changed = false;
151
+ const dailyDate = typeof dailyRaw?.date === "string" ? dailyRaw.date : expectedDate;
152
+ const dailyCount = typeof dailyRaw?.count === "number" && dailyRaw.count >= 0 ? dailyRaw.count : 0;
153
+ const monthlyMonth = typeof monthlyRaw?.month === "string" ? monthlyRaw.month : expectedMonth;
154
+ const monthlyCount = typeof monthlyRaw?.count === "number" && monthlyRaw.count >= 0 ? monthlyRaw.count : 0;
155
+ const usage = {
156
+ daily: { date: dailyDate, count: dailyCount },
157
+ monthly: { month: monthlyMonth, count: monthlyCount }
158
+ };
159
+ if (usage.daily.date !== expectedDate) {
160
+ usage.daily.date = expectedDate;
161
+ usage.daily.count = 0;
162
+ changed = true;
163
+ }
164
+ if (usage.monthly.month !== expectedMonth) {
165
+ usage.monthly.month = expectedMonth;
166
+ usage.monthly.count = 0;
167
+ changed = true;
168
+ }
169
+ return { usage, changed };
170
+ }
171
+ saveUsage(serverId, usage) {
172
+ mkdirSync(this.usageDir, { recursive: true });
173
+ writeFileSync(this.serverFilePath(serverId), JSON.stringify(usage, null, 2) + "\n", "utf-8");
174
+ }
175
+ serverFilePath(serverId) {
176
+ return join(this.usageDir, `${encodeURIComponent(serverId)}.json`);
177
+ }
178
+ }
@@ -98,7 +98,13 @@ export class StdioTransport extends BaseTransport {
98
98
  this.scheduleReconnect();
99
99
  }
100
100
  });
101
- const connectionTimeout = this.clientConfig.connectionTimeoutMs || 5000;
101
+ // npx-based servers need extra time for dependency resolution on first run.
102
+ const isNpx = this.config.command === "npx";
103
+ const defaultTimeout = isNpx ? 30000 : 5000;
104
+ const connectionTimeout = this.clientConfig.connectionTimeoutMs || defaultTimeout;
105
+ if (isNpx && !this.clientConfig.connectionTimeoutMs) {
106
+ this.logger.info(`[mcp-bridge] Using extended timeout (${defaultTimeout}ms) for npx-based server`);
107
+ }
102
108
  await new Promise((resolve, reject) => {
103
109
  let settled = false;
104
110
  let timeout;
@@ -59,6 +59,10 @@ export interface McpServerConfig {
59
59
  };
60
60
  maxResultChars?: number;
61
61
  retry?: RetryConfig;
62
+ rateLimit?: {
63
+ maxCallsPerDay?: number;
64
+ maxCallsPerMonth?: number;
65
+ };
62
66
  }
63
67
  export interface McpClientConfig {
64
68
  servers: Record<string, McpServerConfig>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwerk/mcp-bridge",
3
- "version": "2.6.8",
3
+ "version": "2.7.1",
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",
@@ -50,5 +50,20 @@
50
50
  "resources": false,
51
51
  "prompts": false,
52
52
  "sampling": false
53
+ },
54
+ "signature": {
55
+ "algorithm": "ed25519",
56
+ "publisherId": "aiwerk",
57
+ "value": "2eHbBy6v0MMBTdnIusU9oNReuNjg1CKKA7iknkS7FxQNTuisjZFAW2x5DFjUgN9FqjWmlwwhSLnk/qhwfV42Cw==",
58
+ "signedFields": [
59
+ "id",
60
+ "name",
61
+ "description",
62
+ "transports",
63
+ "auth",
64
+ "install",
65
+ "metadata"
66
+ ],
67
+ "signedAt": "2026-03-20T14:24:25.196Z"
53
68
  }
54
69
  }
@@ -9,7 +9,7 @@
9
9
  "type": "stdio",
10
10
  "command": "uvx",
11
11
  "args": [
12
- "mcp-atlassian"
12
+ "mcp-atlassian@2.1.0"
13
13
  ],
14
14
  "env": {
15
15
  "CONFLUENCE_URL": "${CONFLUENCE_URL}",
@@ -38,7 +38,7 @@
38
38
  "install": {
39
39
  "method": "uvx",
40
40
  "package": "mcp-atlassian",
41
- "version": "latest"
41
+ "version": "2.1.0"
42
42
  },
43
43
  "metadata": {
44
44
  "homepage": "https://github.com/sooperset/mcp-atlassian",
@@ -68,5 +68,20 @@
68
68
  "resources": false,
69
69
  "prompts": false,
70
70
  "sampling": false
71
+ },
72
+ "signature": {
73
+ "algorithm": "ed25519",
74
+ "publisherId": "aiwerk",
75
+ "value": "ysHKVAb4IwPRJF3XtEEtumZC2xjt32ouGu5Dt3VrMl7DIp6PWb56JzewqLe4zJ14lVlksIgmDVrFCrVnVJ1QDw==",
76
+ "signedFields": [
77
+ "id",
78
+ "name",
79
+ "description",
80
+ "transports",
81
+ "auth",
82
+ "install",
83
+ "metadata"
84
+ ],
85
+ "signedAt": "2026-03-20T14:24:29.672Z"
71
86
  }
72
87
  }
@@ -10,7 +10,7 @@
10
10
  "command": "npx",
11
11
  "args": [
12
12
  "-y",
13
- "chrome-devtools-mcp@0.20.0",
13
+ "chrome-devtools-mcp@0.20.3",
14
14
  "--autoConnect"
15
15
  ],
16
16
  "env": {}
@@ -55,5 +55,20 @@
55
55
  "resources": false,
56
56
  "prompts": false,
57
57
  "sampling": false
58
+ },
59
+ "signature": {
60
+ "algorithm": "ed25519",
61
+ "publisherId": "aiwerk",
62
+ "value": "fo/w/oYgd8T4ecaOnSUq3vE5nl82RVB31Mj+2v5W/j6qM+wK17xstVMdiZuiqH+a6YDQXXe30S9enRjLK0y/CA==",
63
+ "signedFields": [
64
+ "id",
65
+ "name",
66
+ "description",
67
+ "transports",
68
+ "auth",
69
+ "install",
70
+ "metadata"
71
+ ],
72
+ "signedAt": "2026-03-20T14:24:32.876Z"
58
73
  }
59
74
  }
@@ -10,7 +10,7 @@
10
10
  "command": "npx",
11
11
  "args": [
12
12
  "-y",
13
- "firecrawl-mcp@3.12.1"
13
+ "firecrawl-mcp@3.11.0"
14
14
  ],
15
15
  "env": {
16
16
  "FIRECRAWL_API_KEY": "${FIRECRAWL_API_KEY}"
@@ -60,5 +60,20 @@
60
60
  "resources": false,
61
61
  "prompts": false,
62
62
  "sampling": false
63
+ },
64
+ "signature": {
65
+ "algorithm": "ed25519",
66
+ "publisherId": "aiwerk",
67
+ "value": "s2daVrlfMhaE5+Tk/Y0CnXtG/S6qIVzP0/iHobQ4edC2RmfZFOfiaBUfOL+p6745VSoiiO8nMLDhuiRrUGQrDQ==",
68
+ "signedFields": [
69
+ "id",
70
+ "name",
71
+ "description",
72
+ "transports",
73
+ "auth",
74
+ "install",
75
+ "metadata"
76
+ ],
77
+ "signedAt": "2026-03-20T14:24:35.540Z"
63
78
  }
64
79
  }
@@ -63,5 +63,20 @@
63
63
  "resources": false,
64
64
  "prompts": false,
65
65
  "sampling": false
66
+ },
67
+ "signature": {
68
+ "algorithm": "ed25519",
69
+ "publisherId": "aiwerk",
70
+ "value": "PWJf2aV7iYDLZA9IXJilKO3tO7noz5yqw3U4Nghe/bIyflshLG3tAq0UqSMbN4L/J+c4CdAHiqAGLCm/eLTXBg==",
71
+ "signedFields": [
72
+ "id",
73
+ "name",
74
+ "description",
75
+ "transports",
76
+ "auth",
77
+ "install",
78
+ "metadata"
79
+ ],
80
+ "signedAt": "2026-03-20T14:24:38.175Z"
66
81
  }
67
82
  }
@@ -10,7 +10,7 @@
10
10
  "command": "npx",
11
11
  "args": [
12
12
  "-y",
13
- "@modelcontextprotocol/server-google-maps"
13
+ "@modelcontextprotocol/server-google-maps@0.6.2"
14
14
  ],
15
15
  "env": {
16
16
  "GOOGLE_MAPS_API_KEY": "${GOOGLE_MAPS_API_KEY}"
@@ -29,7 +29,7 @@
29
29
  "install": {
30
30
  "method": "npx",
31
31
  "package": "@modelcontextprotocol/server-google-maps",
32
- "version": "latest"
32
+ "version": "0.6.2"
33
33
  },
34
34
  "metadata": {
35
35
  "homepage": "https://developers.google.com/maps",
@@ -59,5 +59,20 @@
59
59
  "resources": false,
60
60
  "prompts": false,
61
61
  "sampling": false
62
+ },
63
+ "signature": {
64
+ "algorithm": "ed25519",
65
+ "publisherId": "aiwerk",
66
+ "value": "nc+tSwZmwP+MHs9z8VtyL5ZNf/iAsItoHdlygRezoZ5TjR2H++3nOlMmvp5JKC+Fq61P3iYFDRXTM0/VO6a3DA==",
67
+ "signedFields": [
68
+ "id",
69
+ "name",
70
+ "description",
71
+ "transports",
72
+ "auth",
73
+ "install",
74
+ "metadata"
75
+ ],
76
+ "signedAt": "2026-03-20T14:24:43.129Z"
62
77
  }
63
78
  }
@@ -59,5 +59,20 @@
59
59
  "resources": false,
60
60
  "prompts": false,
61
61
  "sampling": false
62
+ },
63
+ "signature": {
64
+ "algorithm": "ed25519",
65
+ "publisherId": "aiwerk",
66
+ "value": "WX6LE6XzBdkVW/uDv7MsUBT6GBE0JYNkfzNFoQqL59KFtivMXq5LmNvxL2JmyF0PlhulmQl0NGf09cSp8QagAw==",
67
+ "signedFields": [
68
+ "id",
69
+ "name",
70
+ "description",
71
+ "transports",
72
+ "auth",
73
+ "install",
74
+ "metadata"
75
+ ],
76
+ "signedAt": "2026-03-20T14:24:45.822Z"
62
77
  }
63
78
  }
@@ -9,7 +9,7 @@
9
9
  "command": "npx",
10
10
  "args": [
11
11
  "-y",
12
- "@anthropic-pb/hostinger-mcp-server"
12
+ "hostinger-api-mcp@0.1.28"
13
13
  ],
14
14
  "env": {
15
15
  "HOSTINGER_API_TOKEN": "${HOSTINGER_API_TOKEN}"
@@ -27,8 +27,8 @@
27
27
  },
28
28
  "install": {
29
29
  "method": "npx",
30
- "package": "@anthropic-pb/hostinger-mcp-server",
31
- "version": "latest"
30
+ "package": "hostinger-api-mcp",
31
+ "version": "0.1.28"
32
32
  },
33
33
  "metadata": {
34
34
  "homepage": "https://www.hostinger.com/",
@@ -58,5 +58,21 @@
58
58
  "resources": false,
59
59
  "prompts": false,
60
60
  "sampling": false
61
+ },
62
+ "repository": "https://github.com/hostinger/api-mcp-server",
63
+ "signature": {
64
+ "algorithm": "ed25519",
65
+ "publisherId": "aiwerk",
66
+ "value": "Btxn49bVCENtq5hgKDhSvYjU8KtbcxFKIAjclBfxl2W87YDFH68q2vkXzkqd1B9QHE74P0djp8KWt5hR0m7lBA==",
67
+ "signedFields": [
68
+ "id",
69
+ "name",
70
+ "description",
71
+ "transports",
72
+ "auth",
73
+ "install",
74
+ "metadata"
75
+ ],
76
+ "signedAt": "2026-03-20T14:25:53.457Z"
61
77
  }
62
78
  }
@@ -34,7 +34,7 @@
34
34
  "command": "npx",
35
35
  "args": [
36
36
  "-y",
37
- "@aiwerk/mcp-server-imap"
37
+ "@aiwerk/mcp-server-imap@1.1.8"
38
38
  ],
39
39
  "env": {
40
40
  "IMAP_HOST": "${IMAP_HOST}",
@@ -65,7 +65,8 @@
65
65
  "install": {
66
66
  "method": "npx",
67
67
  "package": "@aiwerk/mcp-server-imap",
68
- "version": "latest"
68
+ "version": "1.1.8",
69
+ "toolsHash": "sha256-6ae071b777345740e7edaf319c71b57e65c657c145b68b96e6f6bcd6c0c1730a"
69
70
  },
70
71
  "capabilities": {
71
72
  "toolCount": 10,
@@ -82,5 +83,21 @@
82
83
  "email_attachment"
83
84
  ],
84
85
  "sideEffects": "external-write"
86
+ },
87
+ "repository": "https://github.com/AIWerk/mcp-server-imap",
88
+ "signature": {
89
+ "algorithm": "ed25519",
90
+ "publisherId": "aiwerk",
91
+ "value": "IJQAle97X6wb3x+b6tPne74fwOUhif34GLkd4QII9ziwPyYI7r4Tv6eiV8gBc06d+H2mxMcqn/LE7qxMrxpDDg==",
92
+ "signedFields": [
93
+ "id",
94
+ "name",
95
+ "description",
96
+ "transports",
97
+ "auth",
98
+ "install",
99
+ "metadata"
100
+ ],
101
+ "signedAt": "2026-03-20T15:15:39.408Z"
85
102
  }
86
103
  }
@@ -59,5 +59,20 @@
59
59
  "resources": false,
60
60
  "prompts": false,
61
61
  "sampling": false
62
+ },
63
+ "signature": {
64
+ "algorithm": "ed25519",
65
+ "publisherId": "aiwerk",
66
+ "value": "Q3yXbOmJTN7V2+fUmXbxjtxv4ScPMGsQRbffDYYzsAjQGYllz8DwmuvZeHpLSGtP1Fl6g2X0iVnzlxfTzl3FCg==",
67
+ "signedFields": [
68
+ "id",
69
+ "name",
70
+ "description",
71
+ "transports",
72
+ "auth",
73
+ "install",
74
+ "metadata"
75
+ ],
76
+ "signedAt": "2026-03-20T14:24:58.098Z"
62
77
  }
63
78
  }
@@ -10,7 +10,7 @@
10
10
  "command": "npx",
11
11
  "args": [
12
12
  "-y",
13
- "@llmindset/mcp-miro",
13
+ "@llmindset/mcp-miro@0.1.1",
14
14
  "--token",
15
15
  "${MIRO_API_TOKEN}"
16
16
  ],
@@ -31,7 +31,7 @@
31
31
  "install": {
32
32
  "method": "npx",
33
33
  "package": "@llmindset/mcp-miro",
34
- "version": "latest"
34
+ "version": "0.1.1"
35
35
  },
36
36
  "metadata": {
37
37
  "homepage": "https://miro.com/",
@@ -61,5 +61,20 @@
61
61
  "resources": false,
62
62
  "prompts": false,
63
63
  "sampling": false
64
+ },
65
+ "signature": {
66
+ "algorithm": "ed25519",
67
+ "publisherId": "aiwerk",
68
+ "value": "ekptOzlrjh4EmGVDQMBPpCJNchPEsRIEqFOSkMH2lM4cbMtpqVy9jWvErRTZwkLIFi4nQx2T3SGapmpBDjxhDA==",
69
+ "signedFields": [
70
+ "id",
71
+ "name",
72
+ "description",
73
+ "transports",
74
+ "auth",
75
+ "install",
76
+ "metadata"
77
+ ],
78
+ "signedAt": "2026-03-20T14:25:02.500Z"
64
79
  }
65
80
  }
@@ -10,7 +10,7 @@
10
10
  "command": "npx",
11
11
  "args": [
12
12
  "-y",
13
- "@notionhq/notion-mcp-server"
13
+ "@notionhq/notion-mcp-server@2.2.1"
14
14
  ],
15
15
  "env": {
16
16
  "NOTION_TOKEN": "${NOTION_API_KEY}"
@@ -29,7 +29,7 @@
29
29
  "install": {
30
30
  "method": "npx",
31
31
  "package": "@notionhq/notion-mcp-server",
32
- "version": "latest"
32
+ "version": "2.2.1"
33
33
  },
34
34
  "metadata": {
35
35
  "homepage": "https://www.notion.so/",
@@ -59,5 +59,20 @@
59
59
  "resources": false,
60
60
  "prompts": false,
61
61
  "sampling": false
62
+ },
63
+ "signature": {
64
+ "algorithm": "ed25519",
65
+ "publisherId": "aiwerk",
66
+ "value": "d1UG7xnQ3ZR5qWqjMYrgDf7mju2etp6otv7RJgXsBMWw/Ia4ARwLXCDg7NTBpWL3QPlJGD+231bZx6yOhIY5AQ==",
67
+ "signedFields": [
68
+ "id",
69
+ "name",
70
+ "description",
71
+ "transports",
72
+ "auth",
73
+ "install",
74
+ "metadata"
75
+ ],
76
+ "signedAt": "2026-03-20T14:25:07.431Z"
62
77
  }
63
78
  }
@@ -10,7 +10,7 @@
10
10
  "command": "npx",
11
11
  "args": [
12
12
  "-y",
13
- "@stripe/mcp",
13
+ "@stripe/mcp@0.3.1",
14
14
  "--tools=all",
15
15
  "--api-key=${STRIPE_API_KEY}"
16
16
  ],
@@ -31,7 +31,7 @@
31
31
  "install": {
32
32
  "method": "npx",
33
33
  "package": "@stripe/mcp",
34
- "version": "latest"
34
+ "version": "0.3.1"
35
35
  },
36
36
  "metadata": {
37
37
  "homepage": "https://stripe.com/",
@@ -61,5 +61,20 @@
61
61
  "resources": false,
62
62
  "prompts": false,
63
63
  "sampling": false
64
+ },
65
+ "signature": {
66
+ "algorithm": "ed25519",
67
+ "publisherId": "aiwerk",
68
+ "value": "K+265McBYk3Y8RHHEXDSUILier1i2G5MVCcXFRGjTGJjvj/rjKSPAb9yB0BkCNhEbo3Lf6+wwpfrcdNBUyXSAg==",
69
+ "signedFields": [
70
+ "id",
71
+ "name",
72
+ "description",
73
+ "transports",
74
+ "auth",
75
+ "install",
76
+ "metadata"
77
+ ],
78
+ "signedAt": "2026-03-20T14:25:11.866Z"
64
79
  }
65
80
  }
@@ -59,5 +59,20 @@
59
59
  "resources": false,
60
60
  "prompts": false,
61
61
  "sampling": false
62
+ },
63
+ "signature": {
64
+ "algorithm": "ed25519",
65
+ "publisherId": "aiwerk",
66
+ "value": "a4nc94mlhdcxxQ38NKJ3ez++KBp5KyoPsS05uyzCfoR1YaIpejFRKYJ/5D3YlgI/BKQrOo6uzcBfWXMtxHGuBA==",
67
+ "signedFields": [
68
+ "id",
69
+ "name",
70
+ "description",
71
+ "transports",
72
+ "auth",
73
+ "install",
74
+ "metadata"
75
+ ],
76
+ "signedAt": "2026-03-20T14:25:14.930Z"
62
77
  }
63
78
  }
@@ -10,7 +10,7 @@
10
10
  "command": "npx",
11
11
  "args": [
12
12
  "-y",
13
- "@doist/todoist-ai"
13
+ "@doist/todoist-ai@8.4.0"
14
14
  ],
15
15
  "env": {
16
16
  "TODOIST_API_KEY": "${TODOIST_API_TOKEN}"
@@ -29,7 +29,7 @@
29
29
  "install": {
30
30
  "method": "npx",
31
31
  "package": "@doist/todoist-ai",
32
- "version": "latest"
32
+ "version": "8.4.0"
33
33
  },
34
34
  "metadata": {
35
35
  "homepage": "https://todoist.com/",
@@ -59,5 +59,20 @@
59
59
  "resources": false,
60
60
  "prompts": false,
61
61
  "sampling": false
62
+ },
63
+ "signature": {
64
+ "algorithm": "ed25519",
65
+ "publisherId": "aiwerk",
66
+ "value": "soScAYYYd+k/xETeH6kSytJGW5dLW7qkPUL8ulYP0VvOwmCXlvntW0uS4TvtoJC3+FvjE7DhPUhHmW7CUfDbAw==",
67
+ "signedFields": [
68
+ "id",
69
+ "name",
70
+ "description",
71
+ "transports",
72
+ "auth",
73
+ "install",
74
+ "metadata"
75
+ ],
76
+ "signedAt": "2026-03-20T14:25:19.646Z"
62
77
  }
63
78
  }
@@ -59,5 +59,20 @@
59
59
  "resources": false,
60
60
  "prompts": false,
61
61
  "sampling": false
62
+ },
63
+ "signature": {
64
+ "algorithm": "ed25519",
65
+ "publisherId": "aiwerk",
66
+ "value": "GNdSmM2mKmejTTHTt8RwfVl4QzM/Y9f582OAuE1EUvPFS53BBze5wZQInkuRNlc3GtwsFJrR61lLZfP5Dn/QCQ==",
67
+ "signedFields": [
68
+ "id",
69
+ "name",
70
+ "description",
71
+ "transports",
72
+ "auth",
73
+ "install",
74
+ "metadata"
75
+ ],
76
+ "signedAt": "2026-03-20T14:25:22.915Z"
62
77
  }
63
78
  }