@aiwerk/mcp-bridge 2.6.3 → 2.6.5

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.
@@ -48,16 +48,30 @@ export function parseEnvFile(content) {
48
48
  if (eqIdx === -1)
49
49
  continue;
50
50
  const key = trimmed.substring(0, eqIdx).trim();
51
- let value = trimmed.substring(eqIdx + 1).trim();
51
+ const rawValue = trimmed.substring(eqIdx + 1).trim();
52
+ let value;
53
+ let wasQuoted = false;
52
54
  // Strip surrounding quotes and handle escaped quotes within
53
- if ((value.startsWith('"') && value.endsWith('"')) ||
54
- (value.startsWith("'") && value.endsWith("'"))) {
55
- const quote = value[0];
56
- value = value.slice(1, -1);
55
+ if ((rawValue.startsWith('"') && rawValue.endsWith('"')) ||
56
+ (rawValue.startsWith("'") && rawValue.endsWith("'"))) {
57
+ const quote = rawValue[0];
58
+ value = rawValue.slice(1, -1);
57
59
  // Unescape escaped quotes: \" → " or \' → '
58
60
  value = value.replace(new RegExp(`\\\\${quote}`, "g"), quote);
59
61
  // Unescape escaped backslashes: \\ → \
60
62
  value = value.replace(/\\\\/g, "\\");
63
+ wasQuoted = true;
64
+ }
65
+ else {
66
+ value = rawValue;
67
+ }
68
+ // Strip inline comments (KEY=value # comment) for unquoted values only.
69
+ // Quoted values preserve # characters literally: KEY="val#ue" → val#ue
70
+ if (!wasQuoted) {
71
+ const hashIdx = value.indexOf(" #");
72
+ if (hashIdx !== -1) {
73
+ value = value.substring(0, hashIdx).trimEnd();
74
+ }
61
75
  }
62
76
  if (key)
63
77
  env[key] = value;
@@ -200,7 +200,7 @@ export class McpRouter {
200
200
  }
201
201
  }
202
202
  if (normalizedAction !== "call") {
203
- return this.error("invalid_params", `action must be one of: list, call, batch, refresh, schema, intent`);
203
+ return this.error("invalid_params", `action must be one of: list, call, batch, refresh, schema, intent, status, promotions`);
204
204
  }
205
205
  if (!tool) {
206
206
  return this.error("invalid_params", "tool is required for action=call");
@@ -46,8 +46,11 @@ export declare class OAuth2TokenManager {
46
46
  * Takes a refreshFn that performs the actual token exchange.
47
47
  */
48
48
  private doTokenRefresh;
49
- private refreshDeviceCodeToken;
50
- private refreshAuthCodeToken;
49
+ /**
50
+ * Shared refresh token exchange for both auth_code and device_code flows.
51
+ * The only differences are which fields are optional (clientId/clientSecret).
52
+ */
53
+ private refreshStoredToken;
51
54
  private makeKey;
52
55
  private fetchToken;
53
56
  private exchangeToken;
@@ -66,7 +66,7 @@ export class OAuth2TokenManager {
66
66
  if (existingInflight) {
67
67
  return existingInflight;
68
68
  }
69
- const refreshPromise = this.doTokenRefresh(serverName, stored, (s) => this.refreshAuthCodeToken(s, config), "Auth code");
69
+ const refreshPromise = this.doTokenRefresh(serverName, stored, (s) => this.refreshStoredToken(s, config), "Auth code");
70
70
  this.tokenRefreshInflight.set(serverName, refreshPromise);
71
71
  try {
72
72
  return await refreshPromise;
@@ -98,7 +98,7 @@ export class OAuth2TokenManager {
98
98
  if (existingInflight) {
99
99
  return existingInflight;
100
100
  }
101
- const refreshPromise = this.doTokenRefresh(serverName, stored, (s) => this.refreshDeviceCodeToken(s, config), "Device code");
101
+ const refreshPromise = this.doTokenRefresh(serverName, stored, (s) => this.refreshStoredToken(s, config), "Device code");
102
102
  this.tokenRefreshInflight.set(serverName, refreshPromise);
103
103
  try {
104
104
  return await refreshPromise;
@@ -128,50 +128,20 @@ export class OAuth2TokenManager {
128
128
  error.code = -32006;
129
129
  throw error;
130
130
  }
131
- async refreshDeviceCodeToken(stored, config) {
132
- const formData = new URLSearchParams();
133
- formData.set("grant_type", "refresh_token");
134
- formData.set("refresh_token", stored.refreshToken);
135
- formData.set("client_id", config.clientId);
136
- if (config.clientSecret)
137
- formData.set("client_secret", config.clientSecret);
138
- if (config.scopes?.length)
139
- formData.set("scope", config.scopes.join(" "));
140
- const response = await fetch(stored.tokenUrl, {
141
- method: "POST",
142
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
143
- body: formData.toString(),
144
- });
145
- if (!response.ok) {
146
- throw new Error(`OAuth2 refresh token exchange failed: HTTP ${response.status}`);
147
- }
148
- const payload = (await response.json());
149
- if (!payload.access_token) {
150
- throw new Error("OAuth2 refresh response missing access_token");
151
- }
152
- const expiresIn = Number.isFinite(payload.expires_in)
153
- ? Number(payload.expires_in)
154
- : DEFAULT_EXPIRES_IN_SECONDS;
155
- const expiresAt = Date.now() + Math.max(0, expiresIn - EXPIRY_BUFFER_SECONDS) * 1000;
156
- return {
157
- accessToken: payload.access_token,
158
- refreshToken: payload.refresh_token ?? stored.refreshToken,
159
- expiresAt,
160
- tokenUrl: stored.tokenUrl,
161
- clientId: config.clientId,
162
- scopes: config.scopes,
163
- };
164
- }
165
- async refreshAuthCodeToken(stored, config) {
131
+ /**
132
+ * Shared refresh token exchange for both auth_code and device_code flows.
133
+ * The only differences are which fields are optional (clientId/clientSecret).
134
+ */
135
+ async refreshStoredToken(stored, params) {
166
136
  const formData = new URLSearchParams();
167
137
  formData.set("grant_type", "refresh_token");
168
138
  formData.set("refresh_token", stored.refreshToken);
169
- if (config.clientId)
170
- formData.set("client_id", config.clientId);
171
- if (config.clientSecret)
172
- formData.set("client_secret", config.clientSecret);
173
- if (config.scopes?.length)
174
- formData.set("scope", config.scopes.join(" "));
139
+ if (params.clientId)
140
+ formData.set("client_id", params.clientId);
141
+ if (params.clientSecret)
142
+ formData.set("client_secret", params.clientSecret);
143
+ if (params.scopes?.length)
144
+ formData.set("scope", params.scopes.join(" "));
175
145
  const response = await fetch(stored.tokenUrl, {
176
146
  method: "POST",
177
147
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
@@ -193,8 +163,8 @@ export class OAuth2TokenManager {
193
163
  refreshToken: payload.refresh_token ?? stored.refreshToken,
194
164
  expiresAt,
195
165
  tokenUrl: stored.tokenUrl,
196
- clientId: config.clientId,
197
- scopes: config.scopes,
166
+ clientId: params.clientId,
167
+ scopes: params.scopes,
198
168
  };
199
169
  }
200
170
  makeKey(tokenUrl, clientId) {
@@ -213,16 +213,20 @@ export function processResult(result, serverName, serverConfig, clientConfig) {
213
213
  const wasTruncated = processed !== null && typeof processed === "object" && processed._truncated === true;
214
214
  // Sanitize step (only for trust=sanitize, handled inside applyTrustLevel)
215
215
  processed = applyTrustLevel(processed, serverName, serverConfig);
216
- // If both truncated and untrusted, flatten the metadata to top level
216
+ // If both truncated and untrusted/sanitize, flatten the metadata to top level
217
+ // to avoid double-wrapping ({ _trust, result: { _truncated, result: actual } })
217
218
  const trust = serverConfig.trust ?? "trusted";
218
- if (wasTruncated && trust === "untrusted") {
219
- return {
220
- _trust: "untrusted",
221
- _server: serverName,
219
+ if (wasTruncated && (trust === "untrusted" || trust === "sanitize")) {
220
+ const flat = {
222
221
  _truncated: true,
223
222
  _originalLength: processed.result?._originalLength,
224
223
  result: processed.result?.result,
225
224
  };
225
+ if (trust === "untrusted") {
226
+ flat._trust = "untrusted";
227
+ flat._server = serverName;
228
+ }
229
+ return flat;
226
230
  }
227
231
  return processed;
228
232
  }
@@ -28,7 +28,7 @@ export class StandaloneServer {
28
28
  this.logger = logger;
29
29
  this.tokenManager = new OAuth2TokenManager(logger, new FileTokenStore());
30
30
  if (this.isRouterMode()) {
31
- this.router = new McpRouter(config.servers || {}, config, logger);
31
+ this.router = new McpRouter(config.servers ?? {}, config, logger);
32
32
  }
33
33
  }
34
34
  isRouterMode() {
@@ -1,5 +1,5 @@
1
1
  import { OAuth2TokenManager } from "./oauth2-token-manager.js";
2
- import { BaseTransport, isAuthCodeOAuth2, resolveOAuth2Config, resolveServerHeaders, resolveServerHeadersAsync, warnIfNonTlsRemoteUrl, } from "./transport-base.js";
2
+ import { BaseTransport, isAuthCodeOAuth2, isDeviceCodeOAuth2, resolveOAuth2Config, resolveServerHeaders, resolveServerHeadersAsync, warnIfNonTlsRemoteUrl, } from "./transport-base.js";
3
3
  export class SseTransport extends BaseTransport {
4
4
  endpointUrl = null;
5
5
  sseAbortController = null;
@@ -65,7 +65,8 @@ export class SseTransport extends BaseTransport {
65
65
  if (this.config.auth?.type !== "oauth2") {
66
66
  return;
67
67
  }
68
- if (isAuthCodeOAuth2(this.config.auth)) {
68
+ // Auth code and device code flows use the token store, not the in-memory cache
69
+ if (isAuthCodeOAuth2(this.config.auth) || isDeviceCodeOAuth2(this.config.auth)) {
69
70
  return;
70
71
  }
71
72
  const oauth2Config = resolveOAuth2Config(this.config, undefined, this.clientConfig.envFallback);
@@ -111,7 +112,7 @@ export class SseTransport extends BaseTransport {
111
112
  if (done)
112
113
  break;
113
114
  buffer += decoder.decode(value, { stream: true });
114
- const lines = buffer.split("\n");
115
+ const lines = buffer.split(/\r?\n/);
115
116
  buffer = lines.pop() || "";
116
117
  for (const line of lines) {
117
118
  this.processEventLine(line, state);
@@ -122,7 +122,22 @@ export class StdioTransport extends BaseTransport {
122
122
  cleanup();
123
123
  reject(error);
124
124
  };
125
- const onFirstData = () => settleResolve();
125
+ const onFirstData = (chunk) => {
126
+ // Validate that first data looks like JSON-RPC (starts with { or Content-Length).
127
+ // Some servers write banner text to stdout instead of stderr, which would
128
+ // cause a false-positive connect (we'd think the transport is ready).
129
+ const text = chunk.toString().trim();
130
+ // Accept empty/whitespace readiness signals (common in lightweight stdio MCP servers),
131
+ // JSON messages, or LSP framing headers.
132
+ if (text === "" || text.startsWith("{") || text.startsWith("Content-Length")) {
133
+ settleResolve();
134
+ }
135
+ else {
136
+ this.logger.warn(`[mcp-bridge] Stdio process sent non-JSON data on stdout: ${text.substring(0, 80)}`);
137
+ // Still listen for valid data — don't reject yet, the next chunk might be valid
138
+ this.process?.stdout?.once("data", onFirstData);
139
+ }
140
+ };
126
141
  const onProcessError = (error) => settleReject(error);
127
142
  const onProcessExit = () => settleReject(new Error("MCP server exited before stdout became ready"));
128
143
  this.process.stdout.once("data", onFirstData);
@@ -1,5 +1,5 @@
1
1
  import { OAuth2TokenManager } from "./oauth2-token-manager.js";
2
- import { BaseTransport, isAuthCodeOAuth2, resolveOAuth2Config, resolveServerHeaders, resolveServerHeadersAsync, warnIfNonTlsRemoteUrl, } from "./transport-base.js";
2
+ import { BaseTransport, isAuthCodeOAuth2, isDeviceCodeOAuth2, resolveOAuth2Config, resolveServerHeaders, resolveServerHeadersAsync, warnIfNonTlsRemoteUrl, } from "./transport-base.js";
3
3
  export class StreamableHttpTransport extends BaseTransport {
4
4
  sessionId;
5
5
  resolvedHeaders = null;
@@ -48,8 +48,8 @@ export class StreamableHttpTransport extends BaseTransport {
48
48
  if (this.config.auth?.type !== "oauth2") {
49
49
  return;
50
50
  }
51
- // authorization_code tokens are managed via TokenStore, not the in-memory cache
52
- if (isAuthCodeOAuth2(this.config.auth)) {
51
+ // Auth code and device code tokens are managed via TokenStore, not the in-memory cache
52
+ if (isAuthCodeOAuth2(this.config.auth) || isDeviceCodeOAuth2(this.config.auth)) {
53
53
  return;
54
54
  }
55
55
  const oauth2Config = resolveOAuth2Config(this.config, undefined, this.clientConfig.envFallback);
@@ -146,7 +146,7 @@ export class StreamableHttpTransport extends BaseTransport {
146
146
  if (done)
147
147
  break;
148
148
  partial += decoder.decode(value, { stream: true });
149
- const lines = partial.split("\n");
149
+ const lines = partial.split(/\r?\n/);
150
150
  // Keep the last (potentially incomplete) line in partial
151
151
  partial = lines.pop() || "";
152
152
  for (const line of lines) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwerk/mcp-bridge",
3
- "version": "2.6.3",
3
+ "version": "2.6.5",
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",