@aiwerk/mcp-bridge 1.1.0 → 1.1.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.
@@ -53,11 +53,9 @@ function resolveConfigValue(value, extraEnv) {
53
53
  * 4. Validate required fields
54
54
  */
55
55
  export function loadConfig(options = {}) {
56
- const configDir = options.configPath
57
- ? join(options.configPath, "..") // If a file path is given, derive directory
58
- : DEFAULT_CONFIG_DIR;
56
+ const configDir = getConfigDir(options.configPath);
59
57
  const configPath = options.configPath || join(DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILE);
60
- const envPath = join(options.configPath ? join(options.configPath, "..") : DEFAULT_CONFIG_DIR, DEFAULT_ENV_FILE);
58
+ const envPath = join(configDir, DEFAULT_ENV_FILE);
61
59
  if (!existsSync(configPath)) {
62
60
  throw new Error(`Config file not found: ${configPath}\nRun 'mcp-bridge init' to set up.`);
63
61
  }
@@ -100,7 +98,13 @@ export function loadConfig(options = {}) {
100
98
  }
101
99
  /** Get the default config directory path. */
102
100
  export function getConfigDir(configPath) {
103
- return configPath ? join(configPath, "..") : DEFAULT_CONFIG_DIR;
101
+ if (!configPath)
102
+ return DEFAULT_CONFIG_DIR;
103
+ // If path ends with separator or has no extension, treat as directory
104
+ if (configPath.endsWith("/") || configPath.endsWith("\\") || !configPath.includes(".")) {
105
+ return configPath;
106
+ }
107
+ return join(configPath, "..");
104
108
  }
105
109
  /** Initialize the config directory with template files. */
106
110
  export function initConfigDir(logger) {
@@ -7,7 +7,7 @@ export type { RouterToolHint, RouterServerStatus, RouterDispatchResponse, Router
7
7
  export { convertJsonSchemaToTypeBox, createToolParameters, setTypeBoxLoader, setSchemaLogger } from "./schema-convert.js";
8
8
  export { initializeProtocol, fetchToolsList, PACKAGE_VERSION } from "./protocol.js";
9
9
  export { loadConfig, parseEnvFile, initConfigDir, getConfigDir } from "./config.js";
10
- export type { Logger, McpServerConfig, McpClientConfig, McpTool, McpRequest, McpResponse, McpTransport, McpServerConnection, BridgeConfig, } from "./types.js";
10
+ export type { Logger, McpServerConfig, McpClientConfig, McpTool, McpRequest, McpCallRequest, McpResponse, McpTransport, McpServerConnection, BridgeConfig, } from "./types.js";
11
11
  export { nextRequestId } from "./types.js";
12
12
  export { pickRegisteredToolName } from "./tool-naming.js";
13
13
  export { StandaloneServer } from "./standalone-server.js";
@@ -1,4 +1,4 @@
1
- import { McpClientConfig, McpServerConfig, McpTransport } from "./types.js";
1
+ import { McpClientConfig, McpServerConfig, McpTransport, Logger } from "./types.js";
2
2
  type RouterErrorCode = "unknown_server" | "unknown_tool" | "connection_failed" | "mcp_error" | "invalid_params";
3
3
  export interface RouterToolHint {
4
4
  name: string;
@@ -36,9 +36,9 @@ export type RouterDispatchResponse = {
36
36
  code?: number;
37
37
  };
38
38
  export interface RouterTransportRefs {
39
- sse: new (config: McpServerConfig, clientConfig: McpClientConfig, logger: any, onReconnected?: () => Promise<void>) => McpTransport;
40
- stdio: new (config: McpServerConfig, clientConfig: McpClientConfig, logger: any, onReconnected?: () => Promise<void>) => McpTransport;
41
- streamableHttp: new (config: McpServerConfig, clientConfig: McpClientConfig, logger: any, onReconnected?: () => Promise<void>) => McpTransport;
39
+ sse: new (config: McpServerConfig, clientConfig: McpClientConfig, logger: Logger, onReconnected?: () => Promise<void>) => McpTransport;
40
+ stdio: new (config: McpServerConfig, clientConfig: McpClientConfig, logger: Logger, onReconnected?: () => Promise<void>) => McpTransport;
41
+ streamableHttp: new (config: McpServerConfig, clientConfig: McpClientConfig, logger: Logger, onReconnected?: () => Promise<void>) => McpTransport;
42
42
  }
43
43
  export declare class McpRouter {
44
44
  private readonly servers;
@@ -48,7 +48,7 @@ export declare class McpRouter {
48
48
  private readonly idleTimeoutMs;
49
49
  private readonly maxConcurrent;
50
50
  private readonly states;
51
- constructor(servers: Record<string, McpServerConfig>, clientConfig: McpClientConfig, logger: any, transportRefs?: Partial<RouterTransportRefs>);
51
+ constructor(servers: Record<string, McpServerConfig>, clientConfig: McpClientConfig, logger: Logger, transportRefs?: Partial<RouterTransportRefs>);
52
52
  static generateDescription(servers: Record<string, McpServerConfig>): string;
53
53
  dispatch(server?: string, action?: string, tool?: string, params?: any): Promise<RouterDispatchResponse>;
54
54
  getToolList(server: string): Promise<RouterToolHint[]>;
@@ -5,7 +5,7 @@ const __filename = fileURLToPath(import.meta.url);
5
5
  const __dirname = dirname(__filename);
6
6
  export const PACKAGE_VERSION = (() => {
7
7
  try {
8
- return JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8")).version;
8
+ return JSON.parse(readFileSync(join(__dirname, "..", "..", "package.json"), "utf-8")).version;
9
9
  }
10
10
  catch {
11
11
  return "0.0.0";
@@ -178,7 +178,9 @@ export class SmartFilter {
178
178
  const allServerWords = [...descriptionWords, ...keywordWords];
179
179
  // Calculate overlaps
180
180
  const descMatches = this.countOverlap(queryWords, descriptionWords);
181
- const keywordOnlyMatches = this.countOverlap(queryWords, keywordWords) - descMatches;
181
+ // Count keyword matches that are NOT already counted in description
182
+ const keywordOnlyWords = keywordWords.filter(kw => !descriptionWords.includes(kw));
183
+ const keywordOnlyMatches = this.countOverlap(queryWords, keywordOnlyWords);
182
184
  // Add basic synonym matching for common terms
183
185
  let semanticMatches = 0;
184
186
  for (const queryWord of queryWords) {
@@ -194,7 +196,7 @@ export class SmartFilter {
194
196
  }
195
197
  }
196
198
  // Weighted scoring: description 1.0x, keywords 0.7x, semantic 0.5x, partial matches 0.3x
197
- const score = (descMatches * 1.0 + Math.max(0, keywordOnlyMatches) * 0.7 + semanticMatches * 0.5 + partialMatches) / queryWords.length;
199
+ const score = (descMatches * 1.0 + keywordOnlyMatches * 0.7 + semanticMatches * 0.5 + partialMatches) / queryWords.length;
198
200
  return score;
199
201
  }
200
202
  getSemanticScore(queryWord, serverWords) {
@@ -391,8 +393,8 @@ export const DEFAULTS = {
391
393
  topServers: 5,
392
394
  hardCap: 8,
393
395
  topTools: 10,
394
- serverThreshold: 0.15,
395
- toolThreshold: 0.10,
396
+ serverThreshold: 0.01,
397
+ toolThreshold: 0.05,
396
398
  fallback: "keyword",
397
399
  alwaysInclude: [],
398
400
  timeoutMs: 500,
@@ -246,9 +246,16 @@ export class StandaloneServer {
246
246
  }
247
247
  }
248
248
  /** Connect to all backend servers and discover their tools (direct mode). */
249
- async discoverDirectTools() {
250
- if (this.directTools.length > 0)
249
+ async discoverDirectTools(force = false) {
250
+ if (this.directTools.length > 0 && !force)
251
251
  return; // Already discovered
252
+ if (force) {
253
+ this.directTools = [];
254
+ for (const [, conn] of this.directConnections) {
255
+ await conn.transport.disconnect().catch(() => { });
256
+ }
257
+ this.directConnections.clear();
258
+ }
252
259
  const globalNames = new Set();
253
260
  for (const [serverName, serverConfig] of Object.entries(this.config.servers)) {
254
261
  try {
@@ -279,7 +286,8 @@ export class StandaloneServer {
279
286
  }
280
287
  createTransport(serverName, serverConfig) {
281
288
  const onReconnected = async () => {
282
- this.logger.info(`[mcp-bridge] ${serverName} reconnected`);
289
+ this.logger.info(`[mcp-bridge] ${serverName} reconnected, refreshing tools`);
290
+ await this.discoverDirectTools(true);
283
291
  };
284
292
  switch (serverConfig.transport) {
285
293
  case "sse":
@@ -1,4 +1,4 @@
1
- import { McpTransport, McpRequest, McpResponse, McpServerConfig } from "./types.js";
1
+ import { McpTransport, McpRequest, McpResponse, McpServerConfig, McpClientConfig, Logger } from "./types.js";
2
2
  export type PendingRequest = {
3
3
  resolve: Function;
4
4
  reject: Function;
@@ -14,14 +14,14 @@ export type PendingRequest = {
14
14
  */
15
15
  export declare abstract class BaseTransport implements McpTransport {
16
16
  protected config: McpServerConfig;
17
- protected clientConfig: any;
17
+ protected clientConfig: McpClientConfig;
18
18
  protected connected: boolean;
19
19
  protected pendingRequests: Map<number, PendingRequest>;
20
- protected logger: any;
20
+ protected logger: Logger;
21
21
  protected reconnectTimer: NodeJS.Timeout | null;
22
22
  protected onReconnected?: () => Promise<void>;
23
23
  protected backoffDelay: number;
24
- constructor(config: McpServerConfig, clientConfig: any, logger: any, onReconnected?: () => Promise<void>);
24
+ constructor(config: McpServerConfig, clientConfig: McpClientConfig, logger: Logger, onReconnected?: () => Promise<void>);
25
25
  abstract connect(): Promise<void>;
26
26
  abstract disconnect(): Promise<void>;
27
27
  abstract sendRequest(request: McpRequest): Promise<McpResponse>;
@@ -73,4 +73,4 @@ export declare function resolveArgs(args: string[], extraEnv?: Record<string, st
73
73
  /**
74
74
  * Warn if a URL uses non-TLS HTTP to a remote (non-localhost) host.
75
75
  */
76
- export declare function warnIfNonTlsRemoteUrl(rawUrl: string, logger: any): void;
76
+ export declare function warnIfNonTlsRemoteUrl(rawUrl: string, logger: Logger): void;
@@ -52,7 +52,7 @@ export class SseTransport extends BaseTransport {
52
52
  const reader = response.body.getReader();
53
53
  const decoder = new TextDecoder();
54
54
  let buffer = "";
55
- let currentEvent = "";
55
+ const state = { event: "", dataBuffer: [] };
56
56
  while (true) {
57
57
  const { done, value } = await reader.read();
58
58
  if (done)
@@ -61,17 +61,7 @@ export class SseTransport extends BaseTransport {
61
61
  const lines = buffer.split('\n');
62
62
  buffer = lines.pop() || "";
63
63
  for (const line of lines) {
64
- const trimmed = line.trim();
65
- if (trimmed.startsWith("event: ")) {
66
- currentEvent = trimmed.substring(7).trim();
67
- }
68
- else if (trimmed === "") {
69
- this.processEventLine(line, currentEvent);
70
- currentEvent = "";
71
- }
72
- else {
73
- this.processEventLine(line, currentEvent);
74
- }
64
+ this.processEventLine(line, state);
75
65
  }
76
66
  }
77
67
  }
@@ -82,20 +72,22 @@ export class SseTransport extends BaseTransport {
82
72
  this.scheduleReconnect();
83
73
  }
84
74
  }
85
- processEventLine(line, currentEvent = "") {
75
+ processEventLine(line, state) {
86
76
  const trimmed = line.trim();
87
- if (trimmed.startsWith("event: "))
77
+ if (trimmed.startsWith("event: ")) {
78
+ state.event = trimmed.substring(7).trim();
88
79
  return;
80
+ }
89
81
  if (trimmed.startsWith("data: ")) {
90
- this.currentDataBuffer.push(trimmed.substring(6));
82
+ state.dataBuffer.push(trimmed.substring(6));
91
83
  return;
92
84
  }
93
85
  if (trimmed === "") {
94
- if (this.currentDataBuffer.length === 0)
86
+ if (state.dataBuffer.length === 0)
95
87
  return;
96
- const data = this.currentDataBuffer.join("\n");
97
- this.currentDataBuffer = [];
98
- if (currentEvent === "endpoint") {
88
+ const data = state.dataBuffer.join("\n");
89
+ state.dataBuffer.length = 0;
90
+ if (state.event === "endpoint") {
99
91
  if (data.startsWith("/")) {
100
92
  const base = new URL(this.config.url);
101
93
  this.endpointUrl = `${base.origin}${data}`;
@@ -60,17 +60,19 @@ export class StreamableHttpTransport extends BaseTransport {
60
60
  const dataLines = text.split('\n')
61
61
  .filter((line) => line.startsWith('data:'))
62
62
  .map((line) => line.substring(5).trim());
63
- if (dataLines.length > 0) {
64
- jsonResponse = JSON.parse(dataLines[dataLines.length - 1]);
65
- }
66
- else {
63
+ if (dataLines.length === 0) {
67
64
  throw new Error("No data lines in SSE response");
68
65
  }
66
+ for (const dl of dataLines) {
67
+ try {
68
+ this.handleMessage(JSON.parse(dl));
69
+ }
70
+ catch { /* skip malformed lines */ }
71
+ }
69
72
  }
70
73
  else {
71
- jsonResponse = await response.json();
74
+ this.handleMessage(await response.json());
72
75
  }
73
- this.handleMessage(jsonResponse);
74
76
  }
75
77
  catch (error) {
76
78
  clearTimeout(timeout);
@@ -30,12 +30,17 @@ export interface McpTool {
30
30
  description: string;
31
31
  inputSchema: any;
32
32
  }
33
+ /** MCP JSON-RPC request. id is required for requests (omit only for notifications). */
33
34
  export interface McpRequest {
34
35
  jsonrpc: "2.0";
35
36
  id?: number;
36
37
  method: string;
37
38
  params?: any;
38
39
  }
40
+ /** MCP request that requires a response (id is mandatory). */
41
+ export interface McpCallRequest extends McpRequest {
42
+ id: number;
43
+ }
39
44
  export declare function nextRequestId(): number;
40
45
  export interface McpResponse {
41
46
  jsonrpc: "2.0";
@@ -1,4 +1,4 @@
1
- import { execSync, exec as execCb } from "child_process";
1
+ import { execSync, exec as execCb, execFile } from "child_process";
2
2
  import { PACKAGE_VERSION } from "./protocol.js";
3
3
  const PACKAGE_NAME = "@aiwerk/mcp-bridge";
4
4
  let cachedUpdateInfo = null;
@@ -87,7 +87,7 @@ export async function runUpdate(logger) {
87
87
  function npmViewVersion(_logger) {
88
88
  return new Promise((resolve, reject) => {
89
89
  const timeout = setTimeout(() => reject(new Error("npm view timed out")), 10_000);
90
- execCb(`npm view ${PACKAGE_NAME} version`, { encoding: "utf-8" }, (err, stdout) => {
90
+ execFile("npm", ["view", PACKAGE_NAME, "version"], { encoding: "utf-8" }, (err, stdout) => {
91
91
  clearTimeout(timeout);
92
92
  if (err)
93
93
  return reject(err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwerk/mcp-bridge",
3
- "version": "1.1.0",
3
+ "version": "1.1.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",