@aiwerk/mcp-bridge 2.5.3 → 2.6.1

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/README.md CHANGED
@@ -400,7 +400,7 @@ OAuth2 features: automatic token acquisition, caching with expiry-aware refresh,
400
400
 
401
401
  **OAuth2 Authorization Code + PKCE** (interactive browser login):
402
402
 
403
- For MCP servers behind enterprise SSO or user-level OAuth2 that require browser-based login:
403
+ For MCP servers behind enterprise SSO or user-level OAuth2 that require browser-based login (desktop/laptop):
404
404
 
405
405
  ```json
406
406
  {
@@ -429,6 +429,40 @@ Features:
429
429
  - **Automatic refresh** — tokens refreshed transparently via `refresh_token` grant
430
430
  - **Actionable errors** — expired tokens return error with exact CLI command to re-authenticate
431
431
 
432
+ **OAuth2 Device Code** (headless environments — VPS, Docker, SSH, CI):
433
+
434
+ For environments without a browser. You authenticate on a separate device using a short code:
435
+
436
+ ```json
437
+ {
438
+ "auth": {
439
+ "type": "oauth2",
440
+ "grantType": "device_code",
441
+ "deviceAuthorizationUrl": "https://github.com/login/device/code",
442
+ "tokenUrl": "https://github.com/login/oauth/access_token",
443
+ "clientId": "your-app-id",
444
+ "scopes": ["repo", "read:org"]
445
+ }
446
+ }
447
+ ```
448
+
449
+ ```bash
450
+ mcp-bridge auth login my-server
451
+ # ──────────────────────────────────────────
452
+ # Device authentication for "my-server"
453
+ #
454
+ # 1. Open: https://github.com/login/device
455
+ # 2. Enter code: ABCD-1234
456
+ # ──────────────────────────────────────────
457
+ # Waiting for authorization...
458
+ ```
459
+
460
+ Features:
461
+ - **RFC 8628 compliant** — works with GitHub, Google, Microsoft, Auth0, Okta
462
+ - **No local browser needed** — authenticate from phone/laptop, token received on server
463
+ - **Automatic polling** — respects `interval` and `slow_down` responses
464
+ - **Same token persistence** — stored in `~/.mcp-bridge/tokens/` with auto-refresh
465
+
432
466
  ### Environment variables
433
467
 
434
468
  Secrets go in `~/.mcp-bridge/.env` (chmod 600 on init):
@@ -556,7 +590,7 @@ For production deployments with high security requirements, consider adding an e
556
590
  | ✅ | Configurable retries + graceful shutdown | 2.0.0 |
557
591
  | ✅ | OAuth2 Client Credentials | 2.1.0 |
558
592
  | ✅ | OAuth2 Authorization Code + PKCE | 2.5.0 |
559
- | 🔜 | OAuth2 Device Code flow (headless) | planned |
593
+ | | OAuth2 Device Code flow (headless) | 2.6.0 |
560
594
  | 🔜 | Hosted bridge (bridge.aiwerk.ch) | planned |
561
595
  | 🔜 | Remote catalog integration | planned |
562
596
  | 🔜 | OpenTelemetry / Prometheus metrics | planned |
@@ -9,7 +9,7 @@ import { StandaloneServer } from "../src/standalone-server.js";
9
9
  import { PACKAGE_VERSION } from "../src/protocol.js";
10
10
  import { checkForUpdate, runUpdate } from "../src/update-checker.js";
11
11
  import { FileTokenStore } from "../src/token-store.js";
12
- import { performAuthCodeLogin } from "../src/cli-auth.js";
12
+ import { performAuthCodeLogin, performDeviceCodeLogin } from "../src/cli-auth.js";
13
13
  const __filename = fileURLToPath(import.meta.url);
14
14
  const __dirname = dirname(__filename);
15
15
  // After tsc, this file lives at dist/bin/mcp-bridge.js.
@@ -317,7 +317,7 @@ async function cmdAuth(args, logger) {
317
317
  status = token.refreshToken ? "expired (refresh available)" : "expired";
318
318
  }
319
319
  }
320
- else if (grantType === "authorization_code") {
320
+ else if (grantType === "authorization_code" || grantType === "device_code") {
321
321
  status = "not authenticated";
322
322
  }
323
323
  else {
@@ -366,19 +366,36 @@ async function cmdAuth(args, logger) {
366
366
  process.exit(1);
367
367
  }
368
368
  const auth = serverConfig.auth;
369
- if (!auth || auth.type !== "oauth2" || !("grantType" in auth) || auth.grantType !== "authorization_code") {
370
- logger.error(`Server "${serverName}" is not configured for OAuth2 authorization_code flow`);
369
+ if (!auth || auth.type !== "oauth2" || !("grantType" in auth)) {
370
+ logger.error(`Server "${serverName}" is not configured for an interactive OAuth2 flow (authorization_code or device_code)`);
371
+ process.exit(1);
372
+ }
373
+ const grantType = auth.grantType;
374
+ let token;
375
+ if (grantType === "device_code") {
376
+ const deviceAuth = auth;
377
+ token = await performDeviceCodeLogin(serverName, {
378
+ deviceAuthorizationUrl: deviceAuth.deviceAuthorizationUrl,
379
+ tokenUrl: deviceAuth.tokenUrl,
380
+ clientId: deviceAuth.clientId,
381
+ scopes: deviceAuth.scopes,
382
+ }, logger);
383
+ }
384
+ else if (grantType === "authorization_code") {
385
+ const authCodeAuth = auth;
386
+ token = await performAuthCodeLogin(serverName, {
387
+ authorizationUrl: authCodeAuth.authorizationUrl,
388
+ tokenUrl: authCodeAuth.tokenUrl,
389
+ clientId: authCodeAuth.clientId,
390
+ clientSecret: authCodeAuth.clientSecret,
391
+ scopes: authCodeAuth.scopes,
392
+ callbackPort: authCodeAuth.callbackPort,
393
+ }, logger);
394
+ }
395
+ else {
396
+ logger.error(`Server "${serverName}" uses grant type "${grantType}" which does not support interactive login`);
371
397
  process.exit(1);
372
398
  }
373
- const authCodeAuth = auth;
374
- const token = await performAuthCodeLogin(serverName, {
375
- authorizationUrl: authCodeAuth.authorizationUrl,
376
- tokenUrl: authCodeAuth.tokenUrl,
377
- clientId: authCodeAuth.clientId,
378
- clientSecret: authCodeAuth.clientSecret,
379
- scopes: authCodeAuth.scopes,
380
- callbackPort: authCodeAuth.callbackPort,
381
- }, logger);
382
399
  tokenStore.save(serverName, token);
383
400
  process.stdout.write(`Authentication successful for ${serverName}. Token stored.\n`);
384
401
  }
@@ -25,3 +25,19 @@ export declare function computeCodeChallenge(verifier: string): string;
25
25
  * 4. Exchange code for tokens
26
26
  */
27
27
  export declare function performAuthCodeLogin(serverName: string, authConfig: AuthCodeConfig, logger: Logger): Promise<StoredToken>;
28
+ export interface DeviceCodeConfig {
29
+ deviceAuthorizationUrl: string;
30
+ tokenUrl: string;
31
+ clientId: string;
32
+ scopes?: string[];
33
+ /** Skip opening browser (for tests). */
34
+ skipBrowser?: boolean;
35
+ }
36
+ /**
37
+ * Perform the OAuth2 Device Authorization Grant (RFC 8628).
38
+ *
39
+ * 1. POST to deviceAuthorizationUrl to obtain device_code + user_code
40
+ * 2. Display user_code and verification_uri to the user
41
+ * 3. Poll tokenUrl until the user authorizes or the code expires
42
+ */
43
+ export declare function performDeviceCodeLogin(serverName: string, config: DeviceCodeConfig, logger: Logger): Promise<StoredToken>;
@@ -157,6 +157,109 @@ export async function performAuthCodeLogin(serverName, authConfig, logger) {
157
157
  });
158
158
  });
159
159
  }
160
+ const DEFAULT_POLL_INTERVAL_S = 5;
161
+ const SLOW_DOWN_INCREMENT_S = 5;
162
+ /**
163
+ * Perform the OAuth2 Device Authorization Grant (RFC 8628).
164
+ *
165
+ * 1. POST to deviceAuthorizationUrl to obtain device_code + user_code
166
+ * 2. Display user_code and verification_uri to the user
167
+ * 3. Poll tokenUrl until the user authorizes or the code expires
168
+ */
169
+ export async function performDeviceCodeLogin(serverName, config, logger) {
170
+ // Step 1: Request device code
171
+ const formData = new URLSearchParams();
172
+ formData.set("client_id", config.clientId);
173
+ if (config.scopes?.length)
174
+ formData.set("scope", config.scopes.join(" "));
175
+ const deviceResponse = await fetch(config.deviceAuthorizationUrl, {
176
+ method: "POST",
177
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
178
+ body: formData.toString(),
179
+ });
180
+ if (!deviceResponse.ok) {
181
+ const text = await deviceResponse.text().catch(() => "");
182
+ throw new Error(`Device authorization request failed: HTTP ${deviceResponse.status} ${text}`);
183
+ }
184
+ const devicePayload = (await deviceResponse.json());
185
+ if (devicePayload.error) {
186
+ throw new Error(`Device authorization error: ${devicePayload.error} — ${devicePayload.error_description || ""}`);
187
+ }
188
+ if (!devicePayload.device_code || !devicePayload.user_code || !devicePayload.verification_uri) {
189
+ throw new Error("Device authorization response missing required fields (device_code, user_code, verification_uri)");
190
+ }
191
+ const deviceCode = devicePayload.device_code;
192
+ const userCode = devicePayload.user_code;
193
+ const verificationUri = devicePayload.verification_uri;
194
+ const verificationUriComplete = devicePayload.verification_uri_complete;
195
+ const expiresInS = devicePayload.expires_in ?? 900;
196
+ let intervalS = devicePayload.interval ?? DEFAULT_POLL_INTERVAL_S;
197
+ // Step 2: Display instructions to the user
198
+ logger.info(`[mcp-bridge] ──────────────────────────────────────────`);
199
+ logger.info(`[mcp-bridge] Device authentication for "${serverName}"`);
200
+ logger.info(`[mcp-bridge]`);
201
+ logger.info(`[mcp-bridge] 1. Open: ${verificationUri}`);
202
+ logger.info(`[mcp-bridge] 2. Enter code: ${userCode}`);
203
+ logger.info(`[mcp-bridge] ──────────────────────────────────────────`);
204
+ if (verificationUriComplete) {
205
+ logger.info(`[mcp-bridge] Or open this URL directly: ${verificationUriComplete}`);
206
+ if (!config.skipBrowser) {
207
+ openBrowser(verificationUriComplete, logger);
208
+ }
209
+ }
210
+ logger.info(`[mcp-bridge] Waiting for authorization (expires in ${expiresInS}s)...`);
211
+ // Step 3: Poll for token
212
+ const deadline = Date.now() + expiresInS * 1000;
213
+ while (Date.now() < deadline) {
214
+ await sleep(intervalS * 1000);
215
+ const tokenForm = new URLSearchParams();
216
+ tokenForm.set("grant_type", "urn:ietf:params:oauth:grant-type:device_code");
217
+ tokenForm.set("device_code", deviceCode);
218
+ tokenForm.set("client_id", config.clientId);
219
+ const tokenResponse = await fetch(config.tokenUrl, {
220
+ method: "POST",
221
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
222
+ body: tokenForm.toString(),
223
+ });
224
+ const tokenPayload = (await tokenResponse.json());
225
+ if (tokenPayload.error) {
226
+ if (tokenPayload.error === "authorization_pending") {
227
+ continue;
228
+ }
229
+ if (tokenPayload.error === "slow_down") {
230
+ intervalS += SLOW_DOWN_INCREMENT_S;
231
+ continue;
232
+ }
233
+ if (tokenPayload.error === "expired_token") {
234
+ throw new Error("Device code expired. Please try again.");
235
+ }
236
+ if (tokenPayload.error === "access_denied") {
237
+ throw new Error("Authorization denied by user.");
238
+ }
239
+ throw new Error(`Device code token error: ${tokenPayload.error} — ${tokenPayload.error_description || ""}`);
240
+ }
241
+ if (!tokenPayload.access_token) {
242
+ throw new Error("Device code token response missing access_token");
243
+ }
244
+ const expiresIn = Number.isFinite(tokenPayload.expires_in)
245
+ ? Number(tokenPayload.expires_in)
246
+ : DEFAULT_EXPIRES_IN;
247
+ const expiresAt = Date.now() + Math.max(0, expiresIn - EXPIRY_BUFFER_SECONDS) * 1000;
248
+ logger.info(`[mcp-bridge] Authentication successful. Token expires in ${expiresIn}s.`);
249
+ return {
250
+ accessToken: tokenPayload.access_token,
251
+ refreshToken: tokenPayload.refresh_token,
252
+ expiresAt,
253
+ tokenUrl: config.tokenUrl,
254
+ clientId: config.clientId,
255
+ scopes: config.scopes,
256
+ };
257
+ }
258
+ throw new Error("Device code expired (timeout). Please try again.");
259
+ }
260
+ function sleep(ms) {
261
+ return new Promise((resolve) => setTimeout(resolve, ms));
262
+ }
160
263
  async function exchangeCodeForToken(config, code, redirectUri, codeVerifier, logger) {
161
264
  const formData = new URLSearchParams();
162
265
  formData.set("grant_type", "authorization_code");
@@ -14,6 +14,12 @@ export interface AuthCodeOAuth2Config {
14
14
  clientSecret?: string;
15
15
  scopes?: string[];
16
16
  }
17
+ export interface DeviceCodeOAuth2Config {
18
+ grantType: "device_code";
19
+ tokenUrl: string;
20
+ clientId: string;
21
+ scopes?: string[];
22
+ }
17
23
  export declare class OAuth2TokenManager {
18
24
  private readonly logger;
19
25
  private readonly tokenCache;
@@ -29,6 +35,13 @@ export declare class OAuth2TokenManager {
29
35
  * Checks TokenStore, refreshes if expired, throws if unavailable.
30
36
  */
31
37
  getTokenForAuthCode(serverName: string, config: AuthCodeOAuth2Config): Promise<string>;
38
+ /**
39
+ * Get a token for a device_code flow server.
40
+ * Checks TokenStore, refreshes if expired, throws if unavailable.
41
+ */
42
+ getTokenForDeviceCode(serverName: string, config: DeviceCodeOAuth2Config): Promise<string>;
43
+ private doDeviceCodeRefresh;
44
+ private refreshDeviceCodeToken;
32
45
  private doAuthCodeRefresh;
33
46
  private refreshAuthCodeToken;
34
47
  private makeKey;
@@ -75,6 +75,87 @@ export class OAuth2TokenManager {
75
75
  this.authCodeInflight.delete(serverName);
76
76
  }
77
77
  }
78
+ /**
79
+ * Get a token for a device_code flow server.
80
+ * Checks TokenStore, refreshes if expired, throws if unavailable.
81
+ */
82
+ async getTokenForDeviceCode(serverName, config) {
83
+ if (!this.tokenStore) {
84
+ throw new Error(`Authentication required for server "${serverName}". Run: mcp-bridge auth login ${serverName}`);
85
+ }
86
+ const stored = this.tokenStore.load(serverName);
87
+ if (!stored) {
88
+ const err = new Error(`Authentication required for server "${serverName}". Run: mcp-bridge auth login ${serverName}`);
89
+ err.code = -32007;
90
+ throw err;
91
+ }
92
+ const now = Date.now();
93
+ if (stored.expiresAt > now) {
94
+ return stored.accessToken;
95
+ }
96
+ // Token expired — try refresh with inflight dedup
97
+ const existingInflight = this.authCodeInflight.get(serverName);
98
+ if (existingInflight) {
99
+ return existingInflight;
100
+ }
101
+ const refreshPromise = this.doDeviceCodeRefresh(serverName, stored, config);
102
+ this.authCodeInflight.set(serverName, refreshPromise);
103
+ try {
104
+ return await refreshPromise;
105
+ }
106
+ finally {
107
+ this.authCodeInflight.delete(serverName);
108
+ }
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.scopes?.length)
133
+ formData.set("scope", config.scopes.join(" "));
134
+ const response = await fetch(stored.tokenUrl, {
135
+ method: "POST",
136
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
137
+ body: formData.toString(),
138
+ });
139
+ if (!response.ok) {
140
+ throw new Error(`OAuth2 refresh token exchange failed: HTTP ${response.status}`);
141
+ }
142
+ const payload = (await response.json());
143
+ if (!payload.access_token) {
144
+ throw new Error("OAuth2 refresh response missing access_token");
145
+ }
146
+ const expiresIn = Number.isFinite(payload.expires_in)
147
+ ? Number(payload.expires_in)
148
+ : DEFAULT_EXPIRES_IN_SECONDS;
149
+ const expiresAt = Date.now() + Math.max(0, expiresIn - EXPIRY_BUFFER_SECONDS) * 1000;
150
+ return {
151
+ accessToken: payload.access_token,
152
+ refreshToken: payload.refresh_token ?? stored.refreshToken,
153
+ expiresAt,
154
+ tokenUrl: stored.tokenUrl,
155
+ clientId: config.clientId,
156
+ scopes: config.scopes,
157
+ };
158
+ }
78
159
  async doAuthCodeRefresh(serverName, stored, config) {
79
160
  if (stored.refreshToken) {
80
161
  try {
@@ -1,5 +1,5 @@
1
1
  import { McpTransport, McpRequest, McpResponse, McpServerConfig, McpClientConfig, Logger, JsonRpcMessage, RequestIdGenerator } from "./types.js";
2
- import type { OAuth2Config, AuthCodeOAuth2Config, OAuth2TokenManager } from "./oauth2-token-manager.js";
2
+ import type { OAuth2Config, AuthCodeOAuth2Config, DeviceCodeOAuth2Config, OAuth2TokenManager } from "./oauth2-token-manager.js";
3
3
  export type PendingRequest = {
4
4
  resolve: (value: McpResponse) => void;
5
5
  reject: (reason: Error) => void;
@@ -83,8 +83,14 @@ export declare function isAuthCodeOAuth2(auth: {
83
83
  type: "oauth2";
84
84
  grantType?: string;
85
85
  }): boolean;
86
+ /** Check whether an oauth2 auth config uses the device_code grant type. */
87
+ export declare function isDeviceCodeOAuth2(auth: {
88
+ type: "oauth2";
89
+ grantType?: string;
90
+ }): boolean;
86
91
  export declare function resolveOAuth2Config(config: McpServerConfig, extraEnv?: Record<string, string | undefined>, envFallback?: () => Record<string, string>): OAuth2Config;
87
92
  export declare function resolveAuthCodeOAuth2Config(config: McpServerConfig, extraEnv?: Record<string, string | undefined>, envFallback?: () => Record<string, string>): AuthCodeOAuth2Config;
93
+ export declare function resolveDeviceCodeOAuth2Config(config: McpServerConfig, extraEnv?: Record<string, string | undefined>, envFallback?: () => Record<string, string>): DeviceCodeOAuth2Config;
88
94
  export declare function resolveAuthHeadersAsync(config: McpServerConfig, tokenManager: OAuth2TokenManager, extraEnv?: Record<string, string | undefined>, envFallback?: () => Record<string, string>, serverName?: string): Promise<Record<string, string>>;
89
95
  /**
90
96
  * Resolve server headers and merge auth headers (auth takes precedence).
@@ -185,6 +185,10 @@ export function resolveAuthHeaders(config, extraEnv, envFallback) {
185
185
  export function isAuthCodeOAuth2(auth) {
186
186
  return auth.grantType === "authorization_code";
187
187
  }
188
+ /** Check whether an oauth2 auth config uses the device_code grant type. */
189
+ export function isDeviceCodeOAuth2(auth) {
190
+ return auth.grantType === "device_code";
191
+ }
188
192
  export function resolveOAuth2Config(config, extraEnv, envFallback) {
189
193
  if (!config.auth || config.auth.type !== "oauth2") {
190
194
  throw new Error("[mcp-bridge] resolveOAuth2Config called for non-oauth2 auth config");
@@ -192,14 +196,18 @@ export function resolveOAuth2Config(config, extraEnv, envFallback) {
192
196
  if (isAuthCodeOAuth2(config.auth)) {
193
197
  throw new Error("[mcp-bridge] resolveOAuth2Config called for authorization_code config — use resolveAuthCodeOAuth2Config instead");
194
198
  }
195
- const scopes = config.auth.scopes?.map((scope, index) => resolveEnvVars(scope, `oauth2 scope[${index}]`, extraEnv, envFallback));
199
+ if (isDeviceCodeOAuth2(config.auth)) {
200
+ throw new Error("[mcp-bridge] resolveOAuth2Config called for device_code config — use resolveDeviceCodeOAuth2Config instead");
201
+ }
202
+ const auth = config.auth;
203
+ const scopes = auth.scopes?.map((scope, index) => resolveEnvVars(scope, `oauth2 scope[${index}]`, extraEnv, envFallback));
196
204
  return {
197
- clientId: resolveEnvVars(config.auth.clientId, "oauth2 clientId", extraEnv, envFallback),
198
- clientSecret: resolveEnvVars(config.auth.clientSecret, "oauth2 clientSecret", extraEnv, envFallback),
199
- tokenUrl: resolveEnvVars(config.auth.tokenUrl, "oauth2 tokenUrl", extraEnv, envFallback),
205
+ clientId: resolveEnvVars(auth.clientId, "oauth2 clientId", extraEnv, envFallback),
206
+ clientSecret: resolveEnvVars(auth.clientSecret, "oauth2 clientSecret", extraEnv, envFallback),
207
+ tokenUrl: resolveEnvVars(auth.tokenUrl, "oauth2 tokenUrl", extraEnv, envFallback),
200
208
  ...(scopes && scopes.length > 0 ? { scopes } : {}),
201
- ...("audience" in config.auth && config.auth.audience
202
- ? { audience: resolveEnvVars(config.auth.audience, "oauth2 audience", extraEnv, envFallback) }
209
+ ...(auth.audience
210
+ ? { audience: resolveEnvVars(auth.audience, "oauth2 audience", extraEnv, envFallback) }
203
211
  : {}),
204
212
  };
205
213
  }
@@ -217,6 +225,19 @@ export function resolveAuthCodeOAuth2Config(config, extraEnv, envFallback) {
217
225
  ...(scopes && scopes.length > 0 ? { scopes } : {}),
218
226
  };
219
227
  }
228
+ export function resolveDeviceCodeOAuth2Config(config, extraEnv, envFallback) {
229
+ if (!config.auth || config.auth.type !== "oauth2" || !isDeviceCodeOAuth2(config.auth)) {
230
+ throw new Error("[mcp-bridge] resolveDeviceCodeOAuth2Config called for non-device_code auth config");
231
+ }
232
+ const auth = config.auth;
233
+ const scopes = auth.scopes?.map((scope, index) => resolveEnvVars(scope, `oauth2 scope[${index}]`, extraEnv, envFallback));
234
+ return {
235
+ grantType: "device_code",
236
+ tokenUrl: resolveEnvVars(auth.tokenUrl, "oauth2 tokenUrl", extraEnv, envFallback),
237
+ clientId: resolveEnvVars(auth.clientId, "oauth2 clientId", extraEnv, envFallback),
238
+ ...(scopes && scopes.length > 0 ? { scopes } : {}),
239
+ };
240
+ }
220
241
  export async function resolveAuthHeadersAsync(config, tokenManager, extraEnv, envFallback, serverName) {
221
242
  if (!config.auth)
222
243
  return {};
@@ -229,6 +250,14 @@ export async function resolveAuthHeadersAsync(config, tokenManager, extraEnv, en
229
250
  const token = await tokenManager.getTokenForAuthCode(serverName, authCodeConfig);
230
251
  return { Authorization: `Bearer ${token}` };
231
252
  }
253
+ if (isDeviceCodeOAuth2(config.auth)) {
254
+ if (!serverName) {
255
+ throw new Error("[mcp-bridge] serverName is required for device_code OAuth2 flow");
256
+ }
257
+ const deviceCodeConfig = resolveDeviceCodeOAuth2Config(config, extraEnv, envFallback);
258
+ const token = await tokenManager.getTokenForDeviceCode(serverName, deviceCodeConfig);
259
+ return { Authorization: `Bearer ${token}` };
260
+ }
232
261
  const oauth2Config = resolveOAuth2Config(config, extraEnv, envFallback);
233
262
  const token = await tokenManager.getToken(oauth2Config);
234
263
  return { Authorization: `Bearer ${token}` };
@@ -26,6 +26,13 @@ export type HttpAuthConfig = {
26
26
  clientSecret?: string;
27
27
  scopes?: string[];
28
28
  callbackPort?: number;
29
+ } | {
30
+ type: "oauth2";
31
+ grantType: "device_code";
32
+ deviceAuthorizationUrl: string;
33
+ tokenUrl: string;
34
+ clientId: string;
35
+ scopes?: string[];
29
36
  };
30
37
  export interface RetryConfig {
31
38
  maxAttempts?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwerk/mcp-bridge",
3
- "version": "2.5.3",
3
+ "version": "2.6.1",
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",