@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.
- package/dist/bin/mcp-bridge.js +1 -0
- package/dist/src/cli-auth.d.ts +1 -0
- package/dist/src/cli-auth.js +36 -11
- package/dist/src/oauth2-token-manager.d.ts +2 -1
- package/dist/src/oauth2-token-manager.js +9 -7
- package/dist/src/transport-base.js +1 -0
- package/dist/src/types.d.ts +1 -0
- package/package.json +1 -1
package/dist/bin/mcp-bridge.js
CHANGED
|
@@ -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
|
}
|
package/dist/src/cli-auth.d.ts
CHANGED
package/dist/src/cli-auth.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createServer } from "http";
|
|
2
2
|
import { randomBytes, createHash } from "crypto";
|
|
3
|
-
import {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
70
|
+
this.tokenRefreshInflight.set(serverName, refreshPromise);
|
|
71
71
|
try {
|
|
72
72
|
return await refreshPromise;
|
|
73
73
|
}
|
|
74
74
|
finally {
|
|
75
|
-
this.
|
|
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.
|
|
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.
|
|
102
|
+
this.tokenRefreshInflight.set(serverName, refreshPromise);
|
|
103
103
|
try {
|
|
104
104
|
return await refreshPromise;
|
|
105
105
|
}
|
|
106
106
|
finally {
|
|
107
|
-
this.
|
|
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
|
}
|
package/dist/src/types.d.ts
CHANGED