@aiwerk/mcp-bridge 2.6.1 → 2.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.
@@ -378,6 +378,7 @@ async function cmdAuth(args, logger) {
378
378
  deviceAuthorizationUrl: deviceAuth.deviceAuthorizationUrl,
379
379
  tokenUrl: deviceAuth.tokenUrl,
380
380
  clientId: deviceAuth.clientId,
381
+ clientSecret: deviceAuth.clientSecret,
381
382
  scopes: deviceAuth.scopes,
382
383
  }, logger);
383
384
  }
@@ -29,6 +29,7 @@ export interface DeviceCodeConfig {
29
29
  deviceAuthorizationUrl: string;
30
30
  tokenUrl: string;
31
31
  clientId: string;
32
+ clientSecret?: string;
32
33
  scopes?: string[];
33
34
  /** Skip opening browser (for tests). */
34
35
  skipBrowser?: boolean;
@@ -1,6 +1,6 @@
1
1
  import { createServer } from "http";
2
2
  import { randomBytes, createHash } from "crypto";
3
- import { exec } from "child_process";
3
+ import { execFile } from "child_process";
4
4
  import { platform } from "os";
5
5
  /** Escape HTML special characters to prevent XSS in callback responses. */
6
6
  function escapeHtml(str) {
@@ -35,23 +35,31 @@ export function computeCodeChallenge(verifier) {
35
35
  /**
36
36
  * Open a URL in the default browser using platform-specific commands.
37
37
  */
38
+ /**
39
+ * Open a URL in the default browser.
40
+ * Uses execFile (no shell) to prevent shell injection from untrusted URLs
41
+ * (e.g. verification_uri_complete from an external authorization server).
42
+ */
38
43
  function openBrowser(url, logger) {
39
44
  const os = platform();
40
- let cmd;
41
45
  if (os === "darwin") {
42
- cmd = `open "${url}"`;
46
+ execFile("open", [url], (err) => {
47
+ if (err)
48
+ logger.warn(`[mcp-bridge] Could not open browser automatically. Please visit:\n${url}`);
49
+ });
43
50
  }
44
51
  else if (os === "win32") {
45
- cmd = `start "" "${url}"`;
52
+ execFile("cmd", ["/c", "start", "", url], (err) => {
53
+ if (err)
54
+ logger.warn(`[mcp-bridge] Could not open browser automatically. Please visit:\n${url}`);
55
+ });
46
56
  }
47
57
  else {
48
- cmd = `xdg-open "${url}"`;
58
+ execFile("xdg-open", [url], (err) => {
59
+ if (err)
60
+ logger.warn(`[mcp-bridge] Could not open browser automatically. Please visit:\n${url}`);
61
+ });
49
62
  }
50
- exec(cmd, (err) => {
51
- if (err) {
52
- logger.warn(`[mcp-bridge] Could not open browser automatically. Please visit:\n${url}`);
53
- }
54
- });
55
63
  }
56
64
  const DEFAULT_EXPIRES_IN = 3600;
57
65
  const EXPIRY_BUFFER_SECONDS = 60;
@@ -170,6 +178,8 @@ export async function performDeviceCodeLogin(serverName, config, logger) {
170
178
  // Step 1: Request device code
171
179
  const formData = new URLSearchParams();
172
180
  formData.set("client_id", config.clientId);
181
+ if (config.clientSecret)
182
+ formData.set("client_secret", config.clientSecret);
173
183
  if (config.scopes?.length)
174
184
  formData.set("scope", config.scopes.join(" "));
175
185
  const deviceResponse = await fetch(config.deviceAuthorizationUrl, {
@@ -216,12 +226,27 @@ export async function performDeviceCodeLogin(serverName, config, logger) {
216
226
  tokenForm.set("grant_type", "urn:ietf:params:oauth:grant-type:device_code");
217
227
  tokenForm.set("device_code", deviceCode);
218
228
  tokenForm.set("client_id", config.clientId);
229
+ if (config.clientSecret)
230
+ tokenForm.set("client_secret", config.clientSecret);
219
231
  const tokenResponse = await fetch(config.tokenUrl, {
220
232
  method: "POST",
221
233
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
222
234
  body: tokenForm.toString(),
223
235
  });
224
- const tokenPayload = (await tokenResponse.json());
236
+ // Guard against non-JSON responses (e.g. 500 HTML error pages)
237
+ const contentType = tokenResponse.headers.get("content-type") || "";
238
+ if (!tokenResponse.ok && !contentType.includes("json")) {
239
+ logger.warn(`[mcp-bridge] Token poll returned HTTP ${tokenResponse.status} (non-JSON), retrying...`);
240
+ continue;
241
+ }
242
+ let tokenPayload;
243
+ try {
244
+ tokenPayload = (await tokenResponse.json());
245
+ }
246
+ catch {
247
+ logger.warn("[mcp-bridge] Token poll returned invalid JSON, retrying...");
248
+ continue;
249
+ }
225
250
  if (tokenPayload.error) {
226
251
  if (tokenPayload.error === "authorization_pending") {
227
252
  continue;
@@ -18,13 +18,14 @@ export interface DeviceCodeOAuth2Config {
18
18
  grantType: "device_code";
19
19
  tokenUrl: string;
20
20
  clientId: string;
21
+ clientSecret?: string;
21
22
  scopes?: string[];
22
23
  }
23
24
  export declare class OAuth2TokenManager {
24
25
  private readonly logger;
25
26
  private readonly tokenCache;
26
27
  private readonly inflight;
27
- private readonly authCodeInflight;
28
+ private readonly tokenRefreshInflight;
28
29
  private readonly tokenStore?;
29
30
  constructor(logger: Logger, tokenStore?: TokenStore);
30
31
  getToken(config: OAuth2Config): Promise<string>;
@@ -4,7 +4,7 @@ export class OAuth2TokenManager {
4
4
  logger;
5
5
  tokenCache = new Map();
6
6
  inflight = new Map();
7
- authCodeInflight = new Map();
7
+ tokenRefreshInflight = new Map();
8
8
  tokenStore;
9
9
  constructor(logger, tokenStore) {
10
10
  this.logger = logger;
@@ -62,17 +62,17 @@ export class OAuth2TokenManager {
62
62
  // Token expired — try refresh with inflight dedup to avoid
63
63
  // concurrent requests both trying to refresh the same token
64
64
  // (the second refresh would fail because the first invalidated the refresh_token)
65
- const existingInflight = this.authCodeInflight.get(serverName);
65
+ const existingInflight = this.tokenRefreshInflight.get(serverName);
66
66
  if (existingInflight) {
67
67
  return existingInflight;
68
68
  }
69
69
  const refreshPromise = this.doAuthCodeRefresh(serverName, stored, config);
70
- this.authCodeInflight.set(serverName, refreshPromise);
70
+ this.tokenRefreshInflight.set(serverName, refreshPromise);
71
71
  try {
72
72
  return await refreshPromise;
73
73
  }
74
74
  finally {
75
- this.authCodeInflight.delete(serverName);
75
+ this.tokenRefreshInflight.delete(serverName);
76
76
  }
77
77
  }
78
78
  /**
@@ -94,17 +94,17 @@ export class OAuth2TokenManager {
94
94
  return stored.accessToken;
95
95
  }
96
96
  // Token expired — try refresh with inflight dedup
97
- const existingInflight = this.authCodeInflight.get(serverName);
97
+ const existingInflight = this.tokenRefreshInflight.get(serverName);
98
98
  if (existingInflight) {
99
99
  return existingInflight;
100
100
  }
101
101
  const refreshPromise = this.doDeviceCodeRefresh(serverName, stored, config);
102
- this.authCodeInflight.set(serverName, refreshPromise);
102
+ this.tokenRefreshInflight.set(serverName, refreshPromise);
103
103
  try {
104
104
  return await refreshPromise;
105
105
  }
106
106
  finally {
107
- this.authCodeInflight.delete(serverName);
107
+ this.tokenRefreshInflight.delete(serverName);
108
108
  }
109
109
  }
110
110
  async doDeviceCodeRefresh(serverName, stored, config) {
@@ -129,6 +129,8 @@ export class OAuth2TokenManager {
129
129
  formData.set("grant_type", "refresh_token");
130
130
  formData.set("refresh_token", stored.refreshToken);
131
131
  formData.set("client_id", config.clientId);
132
+ if (config.clientSecret)
133
+ formData.set("client_secret", config.clientSecret);
132
134
  if (config.scopes?.length)
133
135
  formData.set("scope", config.scopes.join(" "));
134
136
  const response = await fetch(stored.tokenUrl, {
@@ -235,6 +235,7 @@ export function resolveDeviceCodeOAuth2Config(config, extraEnv, envFallback) {
235
235
  grantType: "device_code",
236
236
  tokenUrl: resolveEnvVars(auth.tokenUrl, "oauth2 tokenUrl", extraEnv, envFallback),
237
237
  clientId: resolveEnvVars(auth.clientId, "oauth2 clientId", extraEnv, envFallback),
238
+ ...(auth.clientSecret ? { clientSecret: resolveEnvVars(auth.clientSecret, "oauth2 clientSecret", extraEnv, envFallback) } : {}),
238
239
  ...(scopes && scopes.length > 0 ? { scopes } : {}),
239
240
  };
240
241
  }
@@ -32,6 +32,7 @@ export type HttpAuthConfig = {
32
32
  deviceAuthorizationUrl: string;
33
33
  tokenUrl: string;
34
34
  clientId: string;
35
+ clientSecret?: string;
35
36
  scopes?: string[];
36
37
  };
37
38
  export interface RetryConfig {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwerk/mcp-bridge",
3
- "version": "2.6.1",
3
+ "version": "2.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",