@aiwerk/mcp-bridge 2.6.2 → 2.6.4

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.
@@ -41,4 +41,4 @@ export interface DeviceCodeConfig {
41
41
  * 2. Display user_code and verification_uri to the user
42
42
  * 3. Poll tokenUrl until the user authorizes or the code expires
43
43
  */
44
- 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>;
@@ -174,7 +174,7 @@ const SLOW_DOWN_INCREMENT_S = 5;
174
174
  * 2. Display user_code and verification_uri to the user
175
175
  * 3. Poll tokenUrl until the user authorizes or the code expires
176
176
  */
177
- export async function performDeviceCodeLogin(serverName, config, logger) {
177
+ export async function performDeviceCodeLogin(serverName, config, logger, signal) {
178
178
  // Step 1: Request device code
179
179
  const formData = new URLSearchParams();
180
180
  formData.set("client_id", config.clientId);
@@ -221,7 +221,13 @@ export async function performDeviceCodeLogin(serverName, config, logger) {
221
221
  // Step 3: Poll for token
222
222
  const deadline = Date.now() + expiresInS * 1000;
223
223
  while (Date.now() < deadline) {
224
+ if (signal?.aborted) {
225
+ throw new Error("Device code login aborted");
226
+ }
224
227
  await sleep(intervalS * 1000);
228
+ if (signal?.aborted) {
229
+ throw new Error("Device code login aborted");
230
+ }
225
231
  const tokenForm = new URLSearchParams();
226
232
  tokenForm.set("grant_type", "urn:ietf:params:oauth:grant-type:device_code");
227
233
  tokenForm.set("device_code", deviceCode);
@@ -232,6 +238,7 @@ export async function performDeviceCodeLogin(serverName, config, logger) {
232
238
  method: "POST",
233
239
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
234
240
  body: tokenForm.toString(),
241
+ signal,
235
242
  });
236
243
  // Guard against non-JSON responses (e.g. 500 HTML error pages)
237
244
  const contentType = tokenResponse.headers.get("content-type") || "";
@@ -41,10 +41,16 @@ export declare class OAuth2TokenManager {
41
41
  * Checks TokenStore, refreshes if expired, throws if unavailable.
42
42
  */
43
43
  getTokenForDeviceCode(serverName: string, config: DeviceCodeOAuth2Config): Promise<string>;
44
- private doDeviceCodeRefresh;
45
- private refreshDeviceCodeToken;
46
- private doAuthCodeRefresh;
47
- private refreshAuthCodeToken;
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;
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;
48
54
  private makeKey;
49
55
  private fetchToken;
50
56
  private exchangeToken;
@@ -66,7 +66,7 @@ export class OAuth2TokenManager {
66
66
  if (existingInflight) {
67
67
  return existingInflight;
68
68
  }
69
- const refreshPromise = this.doAuthCodeRefresh(serverName, stored, config);
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.doDeviceCodeRefresh(serverName, stored, config);
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;
@@ -107,66 +107,19 @@ export class OAuth2TokenManager {
107
107
  this.tokenRefreshInflight.delete(serverName);
108
108
  }
109
109
  }
110
- async doDeviceCodeRefresh(serverName, stored, config) {
111
- if (stored.refreshToken) {
112
- try {
113
- const refreshed = await this.refreshDeviceCodeToken(stored, config);
114
- this.tokenStore.save(serverName, refreshed);
115
- return refreshed.accessToken;
116
- }
117
- catch (err) {
118
- this.logger.warn("[mcp-bridge] Device code token refresh failed:", err);
119
- }
120
- }
121
- // Refresh failed or no refresh token
122
- this.tokenStore.remove(serverName);
123
- const error = new Error(`Authentication expired for server "${serverName}". Run: mcp-bridge auth login ${serverName}`);
124
- error.code = -32006;
125
- throw error;
126
- }
127
- async refreshDeviceCodeToken(stored, config) {
128
- const formData = new URLSearchParams();
129
- formData.set("grant_type", "refresh_token");
130
- formData.set("refresh_token", stored.refreshToken);
131
- formData.set("client_id", config.clientId);
132
- if (config.clientSecret)
133
- formData.set("client_secret", config.clientSecret);
134
- if (config.scopes?.length)
135
- formData.set("scope", config.scopes.join(" "));
136
- const response = await fetch(stored.tokenUrl, {
137
- method: "POST",
138
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
139
- body: formData.toString(),
140
- });
141
- if (!response.ok) {
142
- throw new Error(`OAuth2 refresh token exchange failed: HTTP ${response.status}`);
143
- }
144
- const payload = (await response.json());
145
- if (!payload.access_token) {
146
- throw new Error("OAuth2 refresh response missing access_token");
147
- }
148
- const expiresIn = Number.isFinite(payload.expires_in)
149
- ? Number(payload.expires_in)
150
- : DEFAULT_EXPIRES_IN_SECONDS;
151
- const expiresAt = Date.now() + Math.max(0, expiresIn - EXPIRY_BUFFER_SECONDS) * 1000;
152
- return {
153
- accessToken: payload.access_token,
154
- refreshToken: payload.refresh_token ?? stored.refreshToken,
155
- expiresAt,
156
- tokenUrl: stored.tokenUrl,
157
- clientId: config.clientId,
158
- scopes: config.scopes,
159
- };
160
- }
161
- async doAuthCodeRefresh(serverName, stored, config) {
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) {
162
115
  if (stored.refreshToken) {
163
116
  try {
164
- const refreshed = await this.refreshAuthCodeToken(stored, config);
117
+ const refreshed = await refreshFn(stored);
165
118
  this.tokenStore.save(serverName, refreshed);
166
119
  return refreshed.accessToken;
167
120
  }
168
121
  catch (err) {
169
- this.logger.warn("[mcp-bridge] Auth code token refresh failed:", err);
122
+ this.logger.warn(`[mcp-bridge] ${flowName} token refresh failed:`, err);
170
123
  }
171
124
  }
172
125
  // Refresh failed or no refresh token
@@ -175,16 +128,20 @@ export class OAuth2TokenManager {
175
128
  error.code = -32006;
176
129
  throw error;
177
130
  }
178
- 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) {
179
136
  const formData = new URLSearchParams();
180
137
  formData.set("grant_type", "refresh_token");
181
138
  formData.set("refresh_token", stored.refreshToken);
182
- if (config.clientId)
183
- formData.set("client_id", config.clientId);
184
- if (config.clientSecret)
185
- formData.set("client_secret", config.clientSecret);
186
- if (config.scopes?.length)
187
- 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(" "));
188
145
  const response = await fetch(stored.tokenUrl, {
189
146
  method: "POST",
190
147
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
@@ -206,8 +163,8 @@ export class OAuth2TokenManager {
206
163
  refreshToken: payload.refresh_token ?? stored.refreshToken,
207
164
  expiresAt,
208
165
  tokenUrl: stored.tokenUrl,
209
- clientId: config.clientId,
210
- scopes: config.scopes,
166
+ clientId: params.clientId,
167
+ scopes: params.scopes,
211
168
  };
212
169
  }
213
170
  makeKey(tokenUrl, clientId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwerk/mcp-bridge",
3
- "version": "2.6.2",
3
+ "version": "2.6.4",
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",