@aiwerk/mcp-bridge 2.6.1 → 2.6.3
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 +2 -1
- package/dist/src/cli-auth.js +44 -12
- package/dist/src/oauth2-token-manager.d.ts +7 -3
- package/dist/src/oauth2-token-manager.js +18 -29
- 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
|
@@ -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;
|
|
@@ -40,4 +41,4 @@ export interface DeviceCodeConfig {
|
|
|
40
41
|
* 2. Display user_code and verification_uri to the user
|
|
41
42
|
* 3. Poll tokenUrl until the user authorizes or the code expires
|
|
42
43
|
*/
|
|
43
|
-
export declare function performDeviceCodeLogin(serverName: string, config: DeviceCodeConfig, logger: Logger): Promise<StoredToken>;
|
|
44
|
+
export declare function performDeviceCodeLogin(serverName: string, config: DeviceCodeConfig, logger: Logger, signal?: AbortSignal): Promise<StoredToken>;
|
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;
|
|
@@ -166,10 +174,12 @@ const SLOW_DOWN_INCREMENT_S = 5;
|
|
|
166
174
|
* 2. Display user_code and verification_uri to the user
|
|
167
175
|
* 3. Poll tokenUrl until the user authorizes or the code expires
|
|
168
176
|
*/
|
|
169
|
-
export async function performDeviceCodeLogin(serverName, config, logger) {
|
|
177
|
+
export async function performDeviceCodeLogin(serverName, config, logger, signal) {
|
|
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, {
|
|
@@ -211,17 +221,39 @@ export async function performDeviceCodeLogin(serverName, config, logger) {
|
|
|
211
221
|
// Step 3: Poll for token
|
|
212
222
|
const deadline = Date.now() + expiresInS * 1000;
|
|
213
223
|
while (Date.now() < deadline) {
|
|
224
|
+
if (signal?.aborted) {
|
|
225
|
+
throw new Error("Device code login aborted");
|
|
226
|
+
}
|
|
214
227
|
await sleep(intervalS * 1000);
|
|
228
|
+
if (signal?.aborted) {
|
|
229
|
+
throw new Error("Device code login aborted");
|
|
230
|
+
}
|
|
215
231
|
const tokenForm = new URLSearchParams();
|
|
216
232
|
tokenForm.set("grant_type", "urn:ietf:params:oauth:grant-type:device_code");
|
|
217
233
|
tokenForm.set("device_code", deviceCode);
|
|
218
234
|
tokenForm.set("client_id", config.clientId);
|
|
235
|
+
if (config.clientSecret)
|
|
236
|
+
tokenForm.set("client_secret", config.clientSecret);
|
|
219
237
|
const tokenResponse = await fetch(config.tokenUrl, {
|
|
220
238
|
method: "POST",
|
|
221
239
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
222
240
|
body: tokenForm.toString(),
|
|
241
|
+
signal,
|
|
223
242
|
});
|
|
224
|
-
|
|
243
|
+
// Guard against non-JSON responses (e.g. 500 HTML error pages)
|
|
244
|
+
const contentType = tokenResponse.headers.get("content-type") || "";
|
|
245
|
+
if (!tokenResponse.ok && !contentType.includes("json")) {
|
|
246
|
+
logger.warn(`[mcp-bridge] Token poll returned HTTP ${tokenResponse.status} (non-JSON), retrying...`);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
let tokenPayload;
|
|
250
|
+
try {
|
|
251
|
+
tokenPayload = (await tokenResponse.json());
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
logger.warn("[mcp-bridge] Token poll returned invalid JSON, retrying...");
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
225
257
|
if (tokenPayload.error) {
|
|
226
258
|
if (tokenPayload.error === "authorization_pending") {
|
|
227
259
|
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>;
|
|
@@ -40,9 +41,12 @@ export declare class OAuth2TokenManager {
|
|
|
40
41
|
* Checks TokenStore, refreshes if expired, throws if unavailable.
|
|
41
42
|
*/
|
|
42
43
|
getTokenForDeviceCode(serverName: string, config: DeviceCodeOAuth2Config): Promise<string>;
|
|
43
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Shared refresh logic for both auth_code and device_code flows.
|
|
46
|
+
* Takes a refreshFn that performs the actual token exchange.
|
|
47
|
+
*/
|
|
48
|
+
private doTokenRefresh;
|
|
44
49
|
private refreshDeviceCodeToken;
|
|
45
|
-
private doAuthCodeRefresh;
|
|
46
50
|
private refreshAuthCodeToken;
|
|
47
51
|
private makeKey;
|
|
48
52
|
private fetchToken;
|
|
@@ -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
|
-
const refreshPromise = this.
|
|
70
|
-
this.
|
|
69
|
+
const refreshPromise = this.doTokenRefresh(serverName, stored, (s) => this.refreshAuthCodeToken(s, config), "Auth code");
|
|
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,28 +94,32 @@ 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
|
-
const refreshPromise = this.
|
|
102
|
-
this.
|
|
101
|
+
const refreshPromise = this.doTokenRefresh(serverName, stored, (s) => this.refreshDeviceCodeToken(s, config), "Device code");
|
|
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
|
+
/**
|
|
111
|
+
* Shared refresh logic for both auth_code and device_code flows.
|
|
112
|
+
* Takes a refreshFn that performs the actual token exchange.
|
|
113
|
+
*/
|
|
114
|
+
async doTokenRefresh(serverName, stored, refreshFn, flowName) {
|
|
111
115
|
if (stored.refreshToken) {
|
|
112
116
|
try {
|
|
113
|
-
const refreshed = await
|
|
117
|
+
const refreshed = await refreshFn(stored);
|
|
114
118
|
this.tokenStore.save(serverName, refreshed);
|
|
115
119
|
return refreshed.accessToken;
|
|
116
120
|
}
|
|
117
121
|
catch (err) {
|
|
118
|
-
this.logger.warn(
|
|
122
|
+
this.logger.warn(`[mcp-bridge] ${flowName} token refresh failed:`, err);
|
|
119
123
|
}
|
|
120
124
|
}
|
|
121
125
|
// Refresh failed or no refresh token
|
|
@@ -129,6 +133,8 @@ export class OAuth2TokenManager {
|
|
|
129
133
|
formData.set("grant_type", "refresh_token");
|
|
130
134
|
formData.set("refresh_token", stored.refreshToken);
|
|
131
135
|
formData.set("client_id", config.clientId);
|
|
136
|
+
if (config.clientSecret)
|
|
137
|
+
formData.set("client_secret", config.clientSecret);
|
|
132
138
|
if (config.scopes?.length)
|
|
133
139
|
formData.set("scope", config.scopes.join(" "));
|
|
134
140
|
const response = await fetch(stored.tokenUrl, {
|
|
@@ -156,23 +162,6 @@ export class OAuth2TokenManager {
|
|
|
156
162
|
scopes: config.scopes,
|
|
157
163
|
};
|
|
158
164
|
}
|
|
159
|
-
async doAuthCodeRefresh(serverName, stored, config) {
|
|
160
|
-
if (stored.refreshToken) {
|
|
161
|
-
try {
|
|
162
|
-
const refreshed = await this.refreshAuthCodeToken(stored, config);
|
|
163
|
-
this.tokenStore.save(serverName, refreshed);
|
|
164
|
-
return refreshed.accessToken;
|
|
165
|
-
}
|
|
166
|
-
catch (err) {
|
|
167
|
-
this.logger.warn("[mcp-bridge] Auth code token refresh failed:", err);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
// Refresh failed or no refresh token
|
|
171
|
-
this.tokenStore.remove(serverName);
|
|
172
|
-
const error = new Error(`Authentication expired for server "${serverName}". Run: mcp-bridge auth login ${serverName}`);
|
|
173
|
-
error.code = -32006;
|
|
174
|
-
throw error;
|
|
175
|
-
}
|
|
176
165
|
async refreshAuthCodeToken(stored, config) {
|
|
177
166
|
const formData = new URLSearchParams();
|
|
178
167
|
formData.set("grant_type", "refresh_token");
|
|
@@ -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