@aiwerk/mcp-bridge 1.6.0 → 1.6.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/README.md CHANGED
@@ -228,6 +228,24 @@ Control which tools are visible and callable per server:
228
228
  - If both: allowed tools minus denied ones
229
229
  - Applied in both tool listing and execution (defense in depth)
230
230
 
231
+ #### Max Result Size
232
+
233
+ Prevent oversized responses from consuming your context:
234
+
235
+ ```json
236
+ {
237
+ "maxResultChars": 50000,
238
+ "servers": {
239
+ "verbose-server": {
240
+ "maxResultChars": 10000
241
+ }
242
+ }
243
+ }
244
+ ```
245
+
246
+ - Global default + per-server override
247
+ - Truncated results include `_truncated: true` and `_originalLength`
248
+
231
249
  ### Adaptive Promotion
232
250
 
233
251
  Frequently used tools can be automatically "promoted" to standalone tools alongside the `mcp` meta-tool. The promotion system tracks usage and reports which tools qualify — the host environment (e.g., OpenClaw plugin) decides how to register them.
@@ -257,24 +275,6 @@ mcp(action="promotions")
257
275
 
258
276
  Returns promoted tools (sorted by frequency) and full usage stats. All tracking is in-memory — promotion rebuilds naturally from usage after restart.
259
277
 
260
- #### Max Result Size
261
-
262
- Prevent oversized responses from consuming your context:
263
-
264
- ```json
265
- {
266
- "maxResultChars": 50000,
267
- "servers": {
268
- "verbose-server": {
269
- "maxResultChars": 10000
270
- }
271
- }
272
- }
273
- ```
274
-
275
- - Global default + per-server override
276
- - Truncated results include `_truncated: true` and `_originalLength`
277
-
278
278
  ### Modes
279
279
 
280
280
  | Mode | Tools exposed | Best for |
@@ -3,14 +3,23 @@ import { join, dirname } from "path";
3
3
  import { fileURLToPath } from "url";
4
4
  const __filename = fileURLToPath(import.meta.url);
5
5
  const __dirname = dirname(__filename);
6
- export const PACKAGE_VERSION = (() => {
7
- try {
8
- return JSON.parse(readFileSync(join(__dirname, "..", "..", "package.json"), "utf-8")).version;
9
- }
10
- catch {
11
- return "0.0.0";
6
+ function loadPackageVersion() {
7
+ const candidates = [
8
+ join(__dirname, "..", "package.json"),
9
+ join(__dirname, "..", "..", "package.json"),
10
+ join(__dirname, "..", "..", "..", "package.json"),
11
+ ];
12
+ for (const p of candidates) {
13
+ try {
14
+ const pkg = JSON.parse(readFileSync(p, "utf-8"));
15
+ if (pkg.version)
16
+ return pkg.version;
17
+ }
18
+ catch { /* try next candidate */ }
12
19
  }
13
- })();
20
+ return "0.0.0";
21
+ }
22
+ export const PACKAGE_VERSION = loadPackageVersion();
14
23
  export async function initializeProtocol(transport, version) {
15
24
  const initRequest = {
16
25
  jsonrpc: "2.0",
@@ -121,7 +121,19 @@ export function applyTrustLevel(result, serverName, serverConfig) {
121
121
  */
122
122
  export function processResult(result, serverName, serverConfig, clientConfig) {
123
123
  let processed = applyMaxResultSize(result, serverConfig, clientConfig);
124
+ const wasTruncated = processed !== null && typeof processed === "object" && processed._truncated === true;
124
125
  // Sanitize step (only for trust=sanitize, handled inside applyTrustLevel)
125
126
  processed = applyTrustLevel(processed, serverName, serverConfig);
127
+ // If both truncated and untrusted, flatten the metadata to top level
128
+ const trust = serverConfig.trust ?? "trusted";
129
+ if (wasTruncated && trust === "untrusted") {
130
+ return {
131
+ _trust: "untrusted",
132
+ _server: serverName,
133
+ _truncated: true,
134
+ _originalLength: processed.result?._originalLength,
135
+ result: processed.result?.result,
136
+ };
137
+ }
126
138
  return processed;
127
139
  }
@@ -62,15 +62,15 @@ export declare class SmartFilter {
62
62
  /**
63
63
  * Extract meaningful intent from last 1-3 user turns
64
64
  */
65
- private synthesizeQuery;
66
- private extractMeaningfulContent;
65
+ static synthesizeQuery(userTurns: UserTurn[]): string;
66
+ private static extractMeaningfulContent;
67
67
  private prepareFilterableServers;
68
68
  private normalizeKeywords;
69
69
  /**
70
70
  * Score servers using weighted overlap scoring
71
71
  */
72
72
  private scoreServers;
73
- private tokenize;
73
+ static tokenize(text: string): string[];
74
74
  private calculateServerScore;
75
75
  private getSemanticScore;
76
76
  private countOverlap;
@@ -88,15 +88,8 @@ export declare class SmartFilter {
88
88
  private logTelemetry;
89
89
  }
90
90
  export declare const DEFAULTS: Required<SmartFilterConfig>;
91
- /** Lowercase, split on whitespace + punctuation, preserve numbers, drop empties. */
92
- export declare function tokenize(text: string): string[];
93
91
  /** Normalize keywords: lowercase, trim, dedup, strip empties, cap at MAX_KEYWORDS. */
94
92
  export declare function validateKeywords(raw: string[]): string[];
95
- /**
96
- * Extract a meaningful intent string from the last 1-3 user turns.
97
- * Returns null if no meaningful query can be extracted.
98
- */
99
- export declare function synthesizeQuery(userTurns: string[]): string | null;
100
93
  export interface ServerScore {
101
94
  name: string;
102
95
  score: number;
@@ -36,15 +36,22 @@ export class SmartFilter {
36
36
  let timeoutOccurred = false;
37
37
  try {
38
38
  // Set up timeout
39
+ let timeoutId;
39
40
  const timeoutPromise = new Promise((resolve) => {
40
- setTimeout(() => {
41
+ timeoutId = setTimeout(() => {
41
42
  timeoutOccurred = true;
42
43
  this.logger.warn(`[smart-filter] Filter timeout after ${this.config.timeoutMs}ms, falling back to show all`);
43
44
  resolve(this.createUnfilteredResult(servers, allTools, "keyword"));
44
45
  }, this.config.timeoutMs);
45
46
  });
46
47
  const filterPromise = this.performFilter(servers, allTools, userTurns);
47
- const result = await Promise.race([filterPromise, timeoutPromise]);
48
+ let result;
49
+ try {
50
+ result = await Promise.race([filterPromise, timeoutPromise]);
51
+ }
52
+ finally {
53
+ clearTimeout(timeoutId);
54
+ }
48
55
  result.metadata.timeoutOccurred = timeoutOccurred;
49
56
  const duration = Date.now() - startTime;
50
57
  if (this.config.telemetry) {
@@ -61,7 +68,7 @@ export class SmartFilter {
61
68
  }
62
69
  async performFilter(servers, allTools, userTurns) {
63
70
  // Step 1: Query synthesis
64
- const query = this.synthesizeQuery(userTurns);
71
+ const query = SmartFilter.synthesizeQuery(userTurns);
65
72
  if (!query) {
66
73
  this.logger.debug("[smart-filter] No meaningful query found, showing all servers");
67
74
  return this.createUnfilteredResult(servers, allTools, "keyword", "");
@@ -89,7 +96,7 @@ export class SmartFilter {
89
96
  /**
90
97
  * Extract meaningful intent from last 1-3 user turns
91
98
  */
92
- synthesizeQuery(userTurns) {
99
+ static synthesizeQuery(userTurns) {
93
100
  if (!userTurns || userTurns.length === 0) {
94
101
  return "";
95
102
  }
@@ -99,20 +106,20 @@ export class SmartFilter {
99
106
  .reverse()
100
107
  .map(turn => turn.content.trim());
101
108
  for (const content of recentTurns) {
102
- const cleanedQuery = this.extractMeaningfulContent(content);
109
+ const cleanedQuery = SmartFilter.extractMeaningfulContent(content);
103
110
  if (cleanedQuery.length >= 3) {
104
111
  return cleanedQuery;
105
112
  }
106
113
  }
107
114
  // If all recent turns are too short, try combining them
108
115
  const combined = recentTurns
109
- .map(content => this.extractMeaningfulContent(content))
116
+ .map(content => SmartFilter.extractMeaningfulContent(content))
110
117
  .filter(content => content.length > 0)
111
118
  .join(" ")
112
119
  .trim();
113
120
  return combined.length >= 3 ? combined : "";
114
121
  }
115
- extractMeaningfulContent(content) {
122
+ static extractMeaningfulContent(content) {
116
123
  // Remove metadata patterns
117
124
  const cleaned = content
118
125
  .replace(/\[.*?\]/g, "") // [timestamps], [commands]
@@ -157,13 +164,13 @@ export class SmartFilter {
157
164
  * Score servers using weighted overlap scoring
158
165
  */
159
166
  scoreServers(query, servers) {
160
- const queryWords = this.tokenize(query.toLowerCase());
167
+ const queryWords = SmartFilter.tokenize(query.toLowerCase());
161
168
  return servers.map(server => ({
162
169
  server,
163
170
  score: this.calculateServerScore(queryWords, server),
164
171
  }));
165
172
  }
166
- tokenize(text) {
173
+ static tokenize(text) {
167
174
  return text
168
175
  .toLowerCase()
169
176
  .replace(/[^\w\s]/g, " ")
@@ -173,7 +180,7 @@ export class SmartFilter {
173
180
  calculateServerScore(queryWords, server) {
174
181
  if (queryWords.length === 0)
175
182
  return 0;
176
- const descriptionWords = this.tokenize(server.description);
183
+ const descriptionWords = SmartFilter.tokenize(server.description);
177
184
  const keywordWords = server.keywords;
178
185
  const allServerWords = [...descriptionWords, ...keywordWords];
179
186
  // Calculate overlaps
@@ -311,7 +318,7 @@ export class SmartFilter {
311
318
  * Filter tools within selected servers
312
319
  */
313
320
  filterTools(query, selectedServers) {
314
- const queryWords = this.tokenize(query);
321
+ const queryWords = SmartFilter.tokenize(query);
315
322
  const allTools = [];
316
323
  for (const { server } of selectedServers) {
317
324
  for (const tool of server.tools) {
@@ -330,8 +337,8 @@ export class SmartFilter {
330
337
  calculateToolScore(queryWords, tool) {
331
338
  if (queryWords.length === 0)
332
339
  return 0;
333
- const nameWords = this.tokenize(tool.name);
334
- const descWords = this.tokenize(tool.description || "");
340
+ const nameWords = SmartFilter.tokenize(tool.name);
341
+ const descWords = SmartFilter.tokenize(tool.description || "");
335
342
  const nameMatches = this.countOverlap(queryWords, nameWords);
336
343
  const descMatches = this.countOverlap(queryWords, descWords) - this.countOverlap(queryWords, nameWords);
337
344
  // Weighted: description 1.0x, name 0.5x (name is less descriptive usually)
@@ -382,11 +389,6 @@ export class SmartFilter {
382
389
  }
383
390
  // ── Standalone utility exports (for testing and external use) ────────────────
384
391
  const MAX_KEYWORDS = 30;
385
- const NOISE_WORDS = new Set([
386
- "yes", "no", "ok", "okay", "sure", "yep", "nope", "yeah", "nah",
387
- "do", "it", "please", "thanks", "thank", "you", "hi", "hello",
388
- "hey", "right", "alright", "fine", "got", "hmm", "hm",
389
- ]);
390
392
  export const DEFAULTS = {
391
393
  enabled: true,
392
394
  embedding: "keyword",
@@ -400,13 +402,6 @@ export const DEFAULTS = {
400
402
  timeoutMs: 500,
401
403
  telemetry: false,
402
404
  };
403
- /** Lowercase, split on whitespace + punctuation, preserve numbers, drop empties. */
404
- export function tokenize(text) {
405
- return text
406
- .toLowerCase()
407
- .split(/[\s\p{P}]+/u)
408
- .filter(t => t.length > 0);
409
- }
410
405
  /** Normalize keywords: lowercase, trim, dedup, strip empties, cap at MAX_KEYWORDS. */
411
406
  export function validateKeywords(raw) {
412
407
  const seen = new Set();
@@ -422,20 +417,6 @@ export function validateKeywords(raw) {
422
417
  }
423
418
  return out;
424
419
  }
425
- /**
426
- * Extract a meaningful intent string from the last 1-3 user turns.
427
- * Returns null if no meaningful query can be extracted.
428
- */
429
- export function synthesizeQuery(userTurns) {
430
- const recent = userTurns.slice(-3).reverse();
431
- for (const turn of recent) {
432
- const tokens = tokenize(turn).filter(t => !NOISE_WORDS.has(t));
433
- if (tokens.length >= 2) {
434
- return tokens.join(" ");
435
- }
436
- }
437
- return null;
438
- }
439
420
  /**
440
421
  * Score a single server against a query using weighted word overlap.
441
422
  * desc_matches * 1.0 + kw_only_matches * 0.5, normalized by query length.
@@ -443,10 +424,10 @@ export function synthesizeQuery(userTurns) {
443
424
  export function scoreServer(queryTokens, serverName, description, keywords) {
444
425
  if (queryTokens.length === 0)
445
426
  return 0;
446
- const descTokens = new Set(tokenize(description));
447
- for (const t of tokenize(serverName))
427
+ const descTokens = new Set(SmartFilter.tokenize(description));
428
+ for (const t of SmartFilter.tokenize(serverName))
448
429
  descTokens.add(t);
449
- const kwTokens = new Set(validateKeywords(keywords).flatMap(kw => tokenize(kw)));
430
+ const kwTokens = new Set(validateKeywords(keywords).flatMap(kw => SmartFilter.tokenize(kw)));
450
431
  let descMatches = 0;
451
432
  let kwOnlyMatches = 0;
452
433
  for (const qt of queryTokens) {
@@ -521,14 +502,15 @@ export function filterServers(servers, userTurns, config, logger) {
521
502
  try {
522
503
  const merged = { ...DEFAULTS, ...config };
523
504
  const startTime = Date.now();
524
- const query = synthesizeQuery(userTurns);
505
+ const userTurnObjects = userTurns.map(content => ({ content, timestamp: Date.now() }));
506
+ const query = SmartFilter.synthesizeQuery(userTurnObjects) || null;
525
507
  if (!query)
526
508
  return showAll("no-query");
527
509
  if (Date.now() - startTime > merged.timeoutMs) {
528
510
  logger?.warn("[smart-filter] Timeout during query synthesis");
529
511
  return showAll("timeout", query);
530
512
  }
531
- const queryTokens = tokenize(query);
513
+ const queryTokens = SmartFilter.tokenize(query);
532
514
  if (queryTokens.length === 0)
533
515
  return showAll("no-query");
534
516
  const scores = scoreAllServers(queryTokens, servers);
@@ -13,7 +13,8 @@ export declare class StandaloneServer {
13
13
  private directConnections;
14
14
  constructor(config: BridgeConfig, logger: Logger);
15
15
  private isRouterMode;
16
- /** Start stdio mode: read JSON-RPC from stdin, write responses to stdout. */
16
+ /** Start stdio mode: read JSON-RPC from stdin, write responses to stdout.
17
+ * Supports both newline-delimited JSON and LSP Content-Length framing. */
17
18
  startStdio(): Promise<void>;
18
19
  private processLine;
19
20
  private writeResponse;
@@ -27,19 +27,73 @@ export class StandaloneServer {
27
27
  isRouterMode() {
28
28
  return (this.config.mode ?? "router") === "router";
29
29
  }
30
- /** Start stdio mode: read JSON-RPC from stdin, write responses to stdout. */
30
+ /** Start stdio mode: read JSON-RPC from stdin, write responses to stdout.
31
+ * Supports both newline-delimited JSON and LSP Content-Length framing. */
31
32
  async startStdio() {
32
33
  const stdin = process.stdin;
33
34
  const stdout = process.stdout;
34
35
  stdin.setEncoding("utf8");
35
36
  let buffer = "";
37
+ // LSP framing state
38
+ let lspContentLength = -1; // -1 means not in LSP mode for current message
39
+ let lspHeadersDone = false;
36
40
  stdin.on("data", (chunk) => {
37
41
  buffer += chunk;
38
- const lines = buffer.split("\n");
39
- buffer = lines.pop() || "";
40
- for (const line of lines) {
42
+ // Process buffer in a loop — it may contain multiple messages
43
+ let progress = true;
44
+ while (progress) {
45
+ progress = false;
46
+ // If we're reading an LSP body, check if we have enough bytes
47
+ if (lspContentLength >= 0 && lspHeadersDone) {
48
+ if (buffer.length >= lspContentLength) {
49
+ const body = buffer.slice(0, lspContentLength);
50
+ buffer = buffer.slice(lspContentLength);
51
+ lspContentLength = -1;
52
+ lspHeadersDone = false;
53
+ const trimmed = body.trim();
54
+ if (trimmed) {
55
+ this.processLine(trimmed, stdout);
56
+ }
57
+ progress = true;
58
+ continue;
59
+ }
60
+ // Not enough data yet — wait for more
61
+ break;
62
+ }
63
+ // Look for complete lines to detect framing
64
+ const newlineIdx = buffer.indexOf("\n");
65
+ if (newlineIdx === -1)
66
+ break;
67
+ const line = buffer.slice(0, newlineIdx);
41
68
  const trimmed = line.trim();
42
- if (!trimmed)
69
+ // LSP header detection
70
+ if (lspContentLength >= 0 && !lspHeadersDone) {
71
+ // We're reading LSP headers — consume until empty line
72
+ buffer = buffer.slice(newlineIdx + 1);
73
+ progress = true;
74
+ if (trimmed === "") {
75
+ // End of headers — next read the body
76
+ lspHeadersDone = true;
77
+ }
78
+ // Ignore other headers (Content-Type, etc.)
79
+ continue;
80
+ }
81
+ if (trimmed.startsWith("Content-Length:")) {
82
+ // Start of LSP-framed message
83
+ const lengthStr = trimmed.slice("Content-Length:".length).trim();
84
+ const length = parseInt(lengthStr, 10);
85
+ if (!isNaN(length) && length > 0) {
86
+ lspContentLength = length;
87
+ lspHeadersDone = false;
88
+ buffer = buffer.slice(newlineIdx + 1);
89
+ progress = true;
90
+ continue;
91
+ }
92
+ }
93
+ // Newline-delimited JSON: consume the line
94
+ buffer = buffer.slice(newlineIdx + 1);
95
+ progress = true;
96
+ if (!trimmed || !trimmed.startsWith("{"))
43
97
  continue;
44
98
  this.processLine(trimmed, stdout);
45
99
  }
@@ -185,7 +239,8 @@ export class StandaloneServer {
185
239
  jsonrpc: "2.0",
186
240
  id,
187
241
  result: {
188
- content: [{ type: "text", text: JSON.stringify(result) }]
242
+ content: [{ type: "text", text: JSON.stringify(result) }],
243
+ isError: true
189
244
  }
190
245
  };
191
246
  }
@@ -54,7 +54,7 @@ export declare abstract class BaseTransport implements McpTransport {
54
54
  * @param contextDescription - Human-readable context for error messages (e.g. 'header "Authorization"')
55
55
  * @param extraEnv - Additional env vars to check before process.env (e.g. merged child process env)
56
56
  */
57
- export declare function resolveEnvVars(value: string, contextDescription: string, extraEnv?: Record<string, string | undefined>): string;
57
+ export declare function resolveEnvVars(value: string, contextDescription: string, extraEnv?: Record<string, string | undefined>, envFallback?: () => Record<string, string>): string;
58
58
  /**
59
59
  * Resolve ${VAR} placeholders in all values of a Record<string, string>.
60
60
  *
@@ -62,14 +62,14 @@ export declare function resolveEnvVars(value: string, contextDescription: string
62
62
  * @param contextPrefix - Prefix for error context (e.g. "header", "env key")
63
63
  * @param extraEnv - Additional env vars to check before process.env
64
64
  */
65
- export declare function resolveEnvRecord(record: Record<string, string>, contextPrefix: string, extraEnv?: Record<string, string | undefined>): Record<string, string>;
65
+ export declare function resolveEnvRecord(record: Record<string, string>, contextPrefix: string, extraEnv?: Record<string, string | undefined>, envFallback?: () => Record<string, string>): Record<string, string>;
66
66
  /**
67
67
  * Resolve ${VAR} placeholders in an array of command arguments.
68
68
  *
69
69
  * @param args - Array of argument strings with potential ${VAR} placeholders
70
70
  * @param extraEnv - Additional env vars to check before process.env
71
71
  */
72
- export declare function resolveArgs(args: string[], extraEnv?: Record<string, string | undefined>): string[];
72
+ export declare function resolveArgs(args: string[], extraEnv?: Record<string, string | undefined>, envFallback?: () => Record<string, string>): string[];
73
73
  /**
74
74
  * Warn if a URL uses non-TLS HTTP to a remote (non-localhost) host.
75
75
  */
@@ -117,16 +117,17 @@ export class BaseTransport {
117
117
  * @param contextDescription - Human-readable context for error messages (e.g. 'header "Authorization"')
118
118
  * @param extraEnv - Additional env vars to check before process.env (e.g. merged child process env)
119
119
  */
120
- export function resolveEnvVars(value, contextDescription, extraEnv) {
120
+ export function resolveEnvVars(value, contextDescription, extraEnv, envFallback) {
121
121
  return value.replace(/\$\{(\w+)\}/g, (_, varName) => {
122
122
  const resolved = extraEnv?.[varName] ?? process.env[varName];
123
- // If resolved is undefined or empty string, try the OpenClaw .env fallback.
124
- // This handles the case where dotenv(override:false) didn't overwrite a
125
- // pre-existing empty env var in process.env.
123
+ // If resolved is undefined or empty string, try the env fallback.
124
+ // Default fallback is loadOpenClawDotEnvFallback (handles the case where
125
+ // dotenv(override:false) didn't overwrite a pre-existing empty env var).
126
126
  if (resolved === undefined || resolved === "") {
127
- const fallback = loadOpenClawDotEnvFallback()[varName];
128
- if (fallback !== undefined && fallback !== "") {
129
- return fallback;
127
+ const fallbackFn = envFallback ?? loadOpenClawDotEnvFallback;
128
+ const fallbackVal = fallbackFn()[varName];
129
+ if (fallbackVal !== undefined && fallbackVal !== "") {
130
+ return fallbackVal;
130
131
  }
131
132
  }
132
133
  if (resolved === undefined) {
@@ -142,10 +143,10 @@ export function resolveEnvVars(value, contextDescription, extraEnv) {
142
143
  * @param contextPrefix - Prefix for error context (e.g. "header", "env key")
143
144
  * @param extraEnv - Additional env vars to check before process.env
144
145
  */
145
- export function resolveEnvRecord(record, contextPrefix, extraEnv) {
146
+ export function resolveEnvRecord(record, contextPrefix, extraEnv, envFallback) {
146
147
  const resolved = {};
147
148
  for (const [key, value] of Object.entries(record)) {
148
- resolved[key] = resolveEnvVars(value, `${contextPrefix} "${key}"`, extraEnv);
149
+ resolved[key] = resolveEnvVars(value, `${contextPrefix} "${key}"`, extraEnv, envFallback);
149
150
  }
150
151
  return resolved;
151
152
  }
@@ -155,8 +156,8 @@ export function resolveEnvRecord(record, contextPrefix, extraEnv) {
155
156
  * @param args - Array of argument strings with potential ${VAR} placeholders
156
157
  * @param extraEnv - Additional env vars to check before process.env
157
158
  */
158
- export function resolveArgs(args, extraEnv) {
159
- return args.map(arg => resolveEnvVars(arg, `arg "${arg}"`, extraEnv));
159
+ export function resolveArgs(args, extraEnv, envFallback) {
160
+ return args.map(arg => resolveEnvVars(arg, `arg "${arg}"`, extraEnv, envFallback));
160
161
  }
161
162
  /**
162
163
  * Warn if a URL uses non-TLS HTTP to a remote (non-localhost) host.
@@ -85,7 +85,8 @@ export class SseTransport extends BaseTransport {
85
85
  return;
86
86
  }
87
87
  if (trimmed.startsWith("data:")) {
88
- state.dataBuffer.push(trimmed.substring(5).trimStart());
88
+ const rawData = trimmed.substring(5);
89
+ state.dataBuffer.push(rawData.startsWith(" ") ? rawData.substring(1) : rawData);
89
90
  return;
90
91
  }
91
92
  if (trimmed === "") {
@@ -100,7 +101,7 @@ export class SseTransport extends BaseTransport {
100
101
  const base = new URL(this.config.url);
101
102
  this.endpointUrl = `${base.origin}${data}`;
102
103
  }
103
- else if (data.startsWith("http")) {
104
+ else if (data.startsWith("http://") || data.startsWith("https://")) {
104
105
  if (!this.isSameOrigin(data)) {
105
106
  this.logger.warn(`[mcp-bridge] Rejected SSE endpoint with mismatched origin: ${data}`);
106
107
  return;
@@ -40,6 +40,7 @@ export interface McpClientConfig {
40
40
  minScore?: number;
41
41
  };
42
42
  maxResultChars?: number;
43
+ envFallback?: () => Record<string, string>;
43
44
  adaptivePromotion?: {
44
45
  enabled?: boolean;
45
46
  maxPromoted?: number;
package/dist/src/types.js CHANGED
@@ -1,4 +1,5 @@
1
1
  let globalRequestId = 1;
2
2
  export function nextRequestId() {
3
- return globalRequestId++;
3
+ globalRequestId = (globalRequestId + 1) % Number.MAX_SAFE_INTEGER;
4
+ return globalRequestId;
4
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwerk/mcp-bridge",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
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",