@aiwerk/mcp-bridge 1.1.6 → 1.1.8

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.
@@ -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, McpCallRequest, McpResponse, McpTransport, McpServerConnection, BridgeConfig, } from "./types.js";
10
+ export type { Logger, McpServerConfig, McpClientConfig, McpTool, McpRequest, McpCallRequest, McpResponse, JsonRpcMessage, 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";
@@ -22,8 +22,10 @@ export declare class StandaloneServer {
22
22
  private handleInitialize;
23
23
  private handleToolsList;
24
24
  private handleToolsCall;
25
+ private discoveryPromise?;
25
26
  /** Connect to all backend servers and discover their tools (direct mode). */
26
27
  private discoverDirectTools;
28
+ private _doDiscovery;
27
29
  private createTransport;
28
30
  /** Graceful shutdown: disconnect all backend servers. */
29
31
  shutdown(): Promise<void>;
@@ -89,9 +89,17 @@ export class StandaloneServer {
89
89
  switch (request.method) {
90
90
  case "initialize":
91
91
  return this.handleInitialize(id);
92
+ case "notifications/initialized":
93
+ return { jsonrpc: "2.0", id, result: {} };
92
94
  case "tools/list":
95
+ if (!this.initialized) {
96
+ return { jsonrpc: "2.0", id, error: { code: -32002, message: "Server not initialized. Call 'initialize' first." } };
97
+ }
93
98
  return this.handleToolsList(id);
94
99
  case "tools/call":
100
+ if (!this.initialized) {
101
+ return { jsonrpc: "2.0", id, error: { code: -32002, message: "Server not initialized. Call 'initialize' first." } };
102
+ }
95
103
  return this.handleToolsCall(id, request.params);
96
104
  case "ping":
97
105
  return { jsonrpc: "2.0", id, result: {} };
@@ -245,10 +253,28 @@ export class StandaloneServer {
245
253
  };
246
254
  }
247
255
  }
256
+ discoveryPromise;
248
257
  /** Connect to all backend servers and discover their tools (direct mode). */
249
258
  async discoverDirectTools(force = false) {
250
259
  if (this.directTools.length > 0 && !force)
251
260
  return; // Already discovered
261
+ if (this.discoveryPromise && !force) {
262
+ await this.discoveryPromise;
263
+ return;
264
+ }
265
+ const promise = this._doDiscovery(force);
266
+ this.discoveryPromise = promise;
267
+ try {
268
+ await promise;
269
+ }
270
+ finally {
271
+ // Only clear if we're still the active promise (a force call may have replaced us)
272
+ if (this.discoveryPromise === promise) {
273
+ this.discoveryPromise = undefined;
274
+ }
275
+ }
276
+ }
277
+ async _doDiscovery(force) {
252
278
  if (force) {
253
279
  this.directTools = [];
254
280
  for (const [, conn] of this.directConnections) {
@@ -1,4 +1,4 @@
1
- import { McpTransport, McpRequest, McpResponse, McpServerConfig, McpClientConfig, Logger } from "./types.js";
1
+ import { McpTransport, McpRequest, McpResponse, McpServerConfig, McpClientConfig, Logger, JsonRpcMessage } from "./types.js";
2
2
  export type PendingRequest = {
3
3
  resolve: (value: McpResponse) => void;
4
4
  reject: (reason: Error) => void;
@@ -35,7 +35,7 @@ export declare abstract class BaseTransport implements McpTransport {
35
35
  * - Other notifications -> debug log
36
36
  * - Responses with id -> resolve/reject matching pending request
37
37
  */
38
- protected handleMessage(message: any): void;
38
+ protected handleMessage(message: JsonRpcMessage): void;
39
39
  /** Reject and clear all pending requests with the given reason. */
40
40
  protected rejectAllPending(reason: string): void;
41
41
  /**
@@ -44,15 +44,18 @@ export class BaseTransport {
44
44
  this.logger.debug(`[mcp-bridge] Unhandled ${this.transportName} notification: ${message.method}`);
45
45
  return;
46
46
  }
47
- if (hasId && this.pendingRequests.has(message.id)) {
48
- const pending = this.pendingRequests.get(message.id);
49
- clearTimeout(pending.timeout);
50
- this.pendingRequests.delete(message.id);
51
- if (message.error) {
52
- pending.reject(new Error(message.error.message || "MCP error"));
53
- }
54
- else {
55
- pending.resolve(message);
47
+ if (hasId) {
48
+ const id = message.id;
49
+ if (this.pendingRequests.has(id)) {
50
+ const pending = this.pendingRequests.get(id);
51
+ clearTimeout(pending.timeout);
52
+ this.pendingRequests.delete(id);
53
+ if (message.error) {
54
+ pending.reject(new Error(message.error.message || "MCP error"));
55
+ }
56
+ else {
57
+ pending.resolve({ jsonrpc: "2.0", id, result: message.result });
58
+ }
56
59
  }
57
60
  }
58
61
  }
@@ -57,17 +57,34 @@ export class StreamableHttpTransport extends BaseTransport {
57
57
  let jsonResponse;
58
58
  if (contentType.includes("text/event-stream")) {
59
59
  const text = await response.text();
60
- const dataLines = text.split('\n')
61
- .filter((line) => line.startsWith('data:'))
62
- .map((line) => line.substring(5).trim());
63
- if (dataLines.length === 0) {
64
- throw new Error("No data lines in SSE response");
65
- }
66
- for (const dl of dataLines) {
60
+ const lines = text.split('\n');
61
+ // SSE event boundary parsing: collect data lines, dispatch on empty line
62
+ let dataBuffer = [];
63
+ const dispatch = () => {
64
+ if (dataBuffer.length === 0)
65
+ return;
66
+ const data = dataBuffer.join("\n");
67
+ dataBuffer = [];
67
68
  try {
68
- this.handleMessage(JSON.parse(dl));
69
+ this.handleMessage(JSON.parse(data));
70
+ }
71
+ catch { /* skip malformed events */ }
72
+ };
73
+ let hasData = false;
74
+ for (const line of lines) {
75
+ const trimmed = line.trim();
76
+ if (trimmed.startsWith("data:")) {
77
+ dataBuffer.push(trimmed.substring(5).trimStart());
78
+ hasData = true;
69
79
  }
70
- catch { /* skip malformed lines */ }
80
+ else if (trimmed === "" && dataBuffer.length > 0) {
81
+ dispatch();
82
+ }
83
+ }
84
+ // Dispatch any trailing data (server may omit final empty line)
85
+ dispatch();
86
+ if (!hasData) {
87
+ throw new Error("No data lines in SSE response");
71
88
  }
72
89
  }
73
90
  else {
@@ -30,6 +30,18 @@ export interface McpTool {
30
30
  description: string;
31
31
  inputSchema: any;
32
32
  }
33
+ /** Incoming JSON-RPC message (response or notification). */
34
+ export interface JsonRpcMessage {
35
+ jsonrpc: "2.0";
36
+ id?: number | null;
37
+ method?: string;
38
+ result?: any;
39
+ error?: {
40
+ code: number;
41
+ message: string;
42
+ data?: unknown;
43
+ };
44
+ }
33
45
  /** MCP JSON-RPC request. id is required for requests (omit only for notifications). */
34
46
  export interface McpRequest {
35
47
  jsonrpc: "2.0";
@@ -1,4 +1,4 @@
1
- import { execSync, exec as execCb, execFile } from "child_process";
1
+ import { execFileSync, 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;
@@ -66,7 +66,8 @@ export async function runUpdate(logger) {
66
66
  }
67
67
  logger.info(`[mcp-bridge] Running update: ${info.updateCommand}`);
68
68
  try {
69
- const output = await execAsync(info.updateCommand, 60_000);
69
+ const parts = info.updateCommand.split(/\s+/);
70
+ const output = await execFileAsync(parts[0], parts.slice(1), 60_000);
70
71
  // Invalidate cache so next check re-fetches
71
72
  cachedUpdateInfo = null;
72
73
  noticeDelivered = false;
@@ -100,16 +101,16 @@ function npmViewVersion(_logger) {
100
101
  }
101
102
  function npmViewVersionSync(_logger) {
102
103
  try {
103
- return execSync(`npm view ${PACKAGE_NAME} version`, { encoding: "utf-8", timeout: 10_000 }).trim();
104
+ return execFileSync("npm", ["view", PACKAGE_NAME, "version"], { encoding: "utf-8", timeout: 10_000 }).trim();
104
105
  }
105
106
  catch {
106
107
  return "unknown";
107
108
  }
108
109
  }
109
- function execAsync(cmd, timeoutMs) {
110
+ function execFileAsync(file, args, timeoutMs) {
110
111
  return new Promise((resolve, reject) => {
111
112
  const timeout = setTimeout(() => reject(new Error(`Command timed out after ${timeoutMs}ms`)), timeoutMs);
112
- execCb(cmd, { encoding: "utf-8", timeout: timeoutMs }, (err, stdout, stderr) => {
113
+ execFile(file, args, { encoding: "utf-8", timeout: timeoutMs }, (err, stdout, stderr) => {
113
114
  clearTimeout(timeout);
114
115
  if (err)
115
116
  return reject(new Error(`${err.message}\n${stderr ?? ""}`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwerk/mcp-bridge",
3
- "version": "1.1.6",
3
+ "version": "1.1.8",
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",