@aiwerk/mcp-bridge 1.1.0 → 1.1.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.
@@ -100,7 +100,13 @@ export function loadConfig(options = {}) {
100
100
  }
101
101
  /** Get the default config directory path. */
102
102
  export function getConfigDir(configPath) {
103
- return configPath ? join(configPath, "..") : DEFAULT_CONFIG_DIR;
103
+ if (!configPath)
104
+ return DEFAULT_CONFIG_DIR;
105
+ // If path ends with separator or has no extension, treat as directory
106
+ if (configPath.endsWith("/") || configPath.endsWith("\\") || !configPath.includes(".")) {
107
+ return configPath;
108
+ }
109
+ return join(configPath, "..");
104
110
  }
105
111
  /** Initialize the config directory with template files. */
106
112
  export function initConfigDir(logger) {
@@ -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,11 @@ 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 = [];
252
254
  const globalNames = new Set();
253
255
  for (const [serverName, serverConfig] of Object.entries(this.config.servers)) {
254
256
  try {
@@ -279,7 +281,8 @@ export class StandaloneServer {
279
281
  }
280
282
  createTransport(serverName, serverConfig) {
281
283
  const onReconnected = async () => {
282
- this.logger.info(`[mcp-bridge] ${serverName} reconnected`);
284
+ this.logger.info(`[mcp-bridge] ${serverName} reconnected, refreshing tools`);
285
+ await this.discoverDirectTools(true);
283
286
  };
284
287
  switch (serverConfig.transport) {
285
288
  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: this.currentDataBuffer };
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}`;
@@ -61,7 +61,21 @@ export class StreamableHttpTransport extends BaseTransport {
61
61
  .filter((line) => line.startsWith('data:'))
62
62
  .map((line) => line.substring(5).trim());
63
63
  if (dataLines.length > 0) {
64
- jsonResponse = JSON.parse(dataLines[dataLines.length - 1]);
64
+ for (const dl of dataLines) {
65
+ try {
66
+ const parsed = JSON.parse(dl);
67
+ if (parsed.id !== undefined) {
68
+ jsonResponse = parsed;
69
+ }
70
+ else {
71
+ this.handleMessage(parsed);
72
+ }
73
+ }
74
+ catch { /* skip malformed lines */ }
75
+ }
76
+ if (!jsonResponse) {
77
+ jsonResponse = JSON.parse(dataLines[dataLines.length - 1]);
78
+ }
65
79
  }
66
80
  else {
67
81
  throw new Error("No data lines in SSE response");
@@ -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.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",