@mcp-use/client 2.1.0 → 2.1.1-canary.0

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.
@@ -270,6 +270,7 @@ var PERSISTED_SERVER_CONFIG_KEYS = [
270
270
  "reconnectionOptions",
271
271
  "popupFeatures",
272
272
  "preventAutoAuth",
273
+ "detectMixedAuth",
273
274
  "useRedirectFlow",
274
275
  "protocolNegotiation",
275
276
  "timeout",
@@ -449,11 +450,136 @@ import { useCallback as useCallback2, useEffect, useMemo, useRef, useState } fro
449
450
  // src/transport/http.ts
450
451
  import {
451
452
  Client,
453
+ discoverOAuthProtectedResourceMetadata,
452
454
  SdkError,
453
455
  SdkHttpError,
454
456
  StreamableHTTPClientTransport,
457
+ UnauthorizedError as UnauthorizedError2
458
+ } from "@modelcontextprotocol/client";
459
+
460
+ // src/auth/flow.ts
461
+ import {
462
+ auth,
463
+ InsufficientScopeError,
455
464
  UnauthorizedError
456
465
  } from "@modelcontextprotocol/client";
466
+ var DEFAULT_AUTH_TIMEOUT_MS = 5 * 6e4;
467
+ function isUnauthorized(err, depth = 0) {
468
+ if (!err || depth > 5) return false;
469
+ if (err instanceof UnauthorizedError) return true;
470
+ if (err instanceof Error) {
471
+ const code = err.code;
472
+ if (code === 401) return true;
473
+ if (err.name === "UnauthorizedError") return true;
474
+ const message = err.message ?? "";
475
+ if (message.includes("401") || message.includes("Unauthorized")) {
476
+ return true;
477
+ }
478
+ if (err.cause && isUnauthorized(err.cause, depth + 1)) return true;
479
+ const data = err.data;
480
+ if (data?.cause && isUnauthorized(data.cause, depth + 1)) return true;
481
+ }
482
+ return false;
483
+ }
484
+ function isOAuthInteractionRequired(err, depth = 0) {
485
+ if (!err || depth > 5) return false;
486
+ if (err instanceof InsufficientScopeError || err instanceof UnauthorizedError) {
487
+ return true;
488
+ }
489
+ if (err instanceof Error) {
490
+ if (err.name === "InsufficientScopeError" || err.name === "UnauthorizedError") {
491
+ return true;
492
+ }
493
+ if (err.cause && isOAuthInteractionRequired(err.cause, depth + 1)) {
494
+ return true;
495
+ }
496
+ const data = err.data;
497
+ if (data?.cause && isOAuthInteractionRequired(data.cause, depth + 1)) {
498
+ return true;
499
+ }
500
+ }
501
+ return false;
502
+ }
503
+ async function completeOAuthFlow(provider, serverUrl, options = {}) {
504
+ const flowProvider = provider;
505
+ const timeoutMs = options.timeoutMs ?? DEFAULT_AUTH_TIMEOUT_MS;
506
+ const fetchFn = options.fetchFn ?? flowProvider.getProxyFetch?.() ?? void 0;
507
+ if (!flowProvider.hasPendingFlow) {
508
+ const result = await auth(provider, { serverUrl, fetchFn });
509
+ if (result === "AUTHORIZED") return;
510
+ if (result !== "REDIRECT") {
511
+ throw new Error(`Unexpected OAuth auth() result: ${result}`);
512
+ }
513
+ }
514
+ if (flowProvider.preventAutoAuth === true && typeof flowProvider.startAuthorization === "function") {
515
+ flowProvider.startAuthorization();
516
+ }
517
+ if (typeof flowProvider.getAuthorizationResponse === "function" || typeof flowProvider.getAuthorizationCode === "function") {
518
+ const response = typeof flowProvider.getAuthorizationResponse === "function" ? await flowProvider.getAuthorizationResponse() : { code: await flowProvider.getAuthorizationCode() };
519
+ if (options.finishAuthorization) {
520
+ await options.finishAuthorization(response.code, response.iss);
521
+ } else {
522
+ await auth(provider, {
523
+ serverUrl,
524
+ authorizationCode: response.code,
525
+ ...response.iss !== void 0 ? { iss: response.iss } : {},
526
+ fetchFn
527
+ });
528
+ }
529
+ return;
530
+ }
531
+ await waitForBrowserAuthComplete(flowProvider, timeoutMs);
532
+ }
533
+ async function waitForBrowserAuthComplete(provider, timeoutMs) {
534
+ if (typeof window === "undefined") {
535
+ throw new Error(
536
+ "OAuth redirect requires a browser environment or a provider with getAuthorizationCode()"
537
+ );
538
+ }
539
+ if (provider.useRedirectFlow) {
540
+ await new Promise(() => {
541
+ });
542
+ return;
543
+ }
544
+ const tokensKey = provider.getKey?.("tokens");
545
+ if (!tokensKey) {
546
+ throw new Error(
547
+ "Browser OAuth provider must expose getKey() for token storage"
548
+ );
549
+ }
550
+ let state = null;
551
+ const authUrl = provider.getLastAttemptedAuthUrl?.();
552
+ if (authUrl) {
553
+ try {
554
+ state = new URL(authUrl).searchParams.get("state");
555
+ } catch {
556
+ }
557
+ }
558
+ try {
559
+ const result = await runAuthPopup({
560
+ popup: null,
561
+ state,
562
+ tokensKey,
563
+ timeoutMs
564
+ });
565
+ switch (result.kind) {
566
+ case "success":
567
+ return;
568
+ case "cancelled":
569
+ throw new Error("OAuth authentication was cancelled.");
570
+ case "timeout":
571
+ throw new Error(
572
+ `OAuth callback not received within ${timeoutMs}ms. Ensure /oauth/callback calls onMcpAuthorization().`
573
+ );
574
+ case "error":
575
+ throw new Error(result.error);
576
+ default:
577
+ throw new Error("Unexpected OAuth popup result");
578
+ }
579
+ } finally {
580
+ provider.markFlowComplete?.();
581
+ }
582
+ }
457
583
 
458
584
  // src/utils/json-schema-validator.ts
459
585
  import {
@@ -517,6 +643,7 @@ var BaseConnector = class {
517
643
  toolsCache = null;
518
644
  capabilitiesCache = null;
519
645
  serverInfoCache = null;
646
+ authorizationCache;
520
647
  connected = false;
521
648
  opts;
522
649
  notificationHandlers = [];
@@ -780,6 +907,21 @@ var BaseConnector = class {
780
907
  "setupElicitationHandler: Elicitation handler registered successfully"
781
908
  );
782
909
  }
910
+ /**
911
+ * Run one logical MCP operation. HTTP connectors override this host seam to
912
+ * finish an SDK-started interactive OAuth flow and retry exactly once.
913
+ */
914
+ async executeRequest(operation) {
915
+ return operation();
916
+ }
917
+ /** OAuth state discovered for the active connection, when available. */
918
+ get authorization() {
919
+ return this.authorizationCache;
920
+ }
921
+ /** Start optional OAuth for a connected mixed-auth server. */
922
+ async authenticate() {
923
+ throw new Error("This connector does not support interactive OAuth");
924
+ }
783
925
  /**
784
926
  * Disconnects the SDK client and releases transport resources.
785
927
  *
@@ -827,13 +969,13 @@ var BaseConnector = class {
827
969
  icons: serverInfo.icons
828
970
  } : null;
829
971
  try {
830
- const listToolsRes = await this.client.listTools(
831
- void 0,
832
- defaultRequestOptions
972
+ const listToolsRes = await this.executeRequest(
973
+ () => this.client.listTools(void 0, defaultRequestOptions)
833
974
  );
834
975
  this.toolsCache = listToolsRes.tools ?? [];
835
976
  logger.debug(`Fetched ${this.toolsCache.length} tools from server`);
836
977
  } catch (err) {
978
+ if (isOAuthInteractionRequired(err)) throw err;
837
979
  const error = err;
838
980
  if (error.code === -32601) {
839
981
  logger.debug("Server does not implement tools/list, assuming no tools");
@@ -907,9 +1049,8 @@ var BaseConnector = class {
907
1049
  const progressHandler = enhancedOptions?.onprogress;
908
1050
  if (progressHandler) this.activeProgressHandlers.add(progressHandler);
909
1051
  try {
910
- const res = await this.client.callTool(
911
- { name, arguments: args },
912
- enhancedOptions
1052
+ const res = await this.executeRequest(
1053
+ () => this.client.callTool({ name, arguments: args }, enhancedOptions)
913
1054
  );
914
1055
  logger.debug(`Tool '${name}' returned`, res);
915
1056
  return res;
@@ -929,7 +1070,9 @@ var BaseConnector = class {
929
1070
  throw new Error("MCP client is not connected");
930
1071
  }
931
1072
  logger.debug("[listTools] Fetching fresh tools from server...");
932
- const result = await this.client.listTools(void 0, options);
1073
+ const result = await this.executeRequest(
1074
+ () => this.client.listTools(void 0, options)
1075
+ );
933
1076
  const tools = result.tools ? [...result.tools] : [];
934
1077
  logger.debug(
935
1078
  `[listTools] Returned ${tools.length} tools:`,
@@ -949,7 +1092,9 @@ var BaseConnector = class {
949
1092
  throw new Error("MCP client is not connected");
950
1093
  }
951
1094
  logger.debug("Listing resources", cursor ? `with cursor: ${cursor}` : "");
952
- return await this.client.listResources({ cursor }, options);
1095
+ return await this.executeRequest(
1096
+ () => this.client.listResources({ cursor }, options)
1097
+ );
953
1098
  }
954
1099
  /**
955
1100
  * List all resources from the server, automatically handling pagination
@@ -967,14 +1112,16 @@ var BaseConnector = class {
967
1112
  }
968
1113
  try {
969
1114
  logger.debug("Listing all resources (with auto-pagination)");
970
- const allResources = [];
971
- let cursor = void 0;
972
- do {
973
- const result = await this.client.listResources({ cursor }, options);
974
- allResources.push(...result.resources || []);
975
- cursor = result.nextCursor;
976
- } while (cursor);
977
- return { resources: allResources };
1115
+ return await this.executeRequest(async () => {
1116
+ const allResources = [];
1117
+ let cursor = void 0;
1118
+ do {
1119
+ const result = await this.client.listResources({ cursor }, options);
1120
+ allResources.push(...result.resources || []);
1121
+ cursor = result.nextCursor;
1122
+ } while (cursor);
1123
+ return { resources: allResources };
1124
+ });
978
1125
  } catch (err) {
979
1126
  const error = err;
980
1127
  if (error.code === -32601) {
@@ -995,7 +1142,9 @@ var BaseConnector = class {
995
1142
  throw new Error("MCP client is not connected");
996
1143
  }
997
1144
  logger.debug("Listing resource templates");
998
- return await this.client.listResourceTemplates(void 0, options);
1145
+ return await this.executeRequest(
1146
+ () => this.client.listResourceTemplates(void 0, options)
1147
+ );
999
1148
  }
1000
1149
  /**
1001
1150
  * Request completion suggestions for a prompt or resource template argument
@@ -1009,7 +1158,9 @@ var BaseConnector = class {
1009
1158
  throw new Error("MCP client is not connected");
1010
1159
  }
1011
1160
  logger.debug("[complete] Requesting completions for:", params.ref);
1012
- const result = await this.client.complete(params, options);
1161
+ const result = await this.executeRequest(
1162
+ () => this.client.complete(params, options)
1163
+ );
1013
1164
  logger.debug(
1014
1165
  `[complete] Received ${result.completion.values.length} suggestions`
1015
1166
  );
@@ -1027,7 +1178,9 @@ var BaseConnector = class {
1027
1178
  throw new Error("MCP client is not connected");
1028
1179
  }
1029
1180
  logger.debug(`Reading resource ${uri}`);
1030
- const res = await this.client.readResource({ uri }, options);
1181
+ const res = await this.executeRequest(
1182
+ () => this.client.readResource({ uri }, options)
1183
+ );
1031
1184
  return res;
1032
1185
  }
1033
1186
  /**
@@ -1041,7 +1194,9 @@ var BaseConnector = class {
1041
1194
  throw new Error("MCP client is not connected");
1042
1195
  }
1043
1196
  logger.debug(`Subscribing to resource: ${uri}`);
1044
- return await this.client.subscribeResource({ uri }, options);
1197
+ return await this.executeRequest(
1198
+ () => this.client.subscribeResource({ uri }, options)
1199
+ );
1045
1200
  }
1046
1201
  /**
1047
1202
  * Unsubscribe from resource updates
@@ -1054,7 +1209,9 @@ var BaseConnector = class {
1054
1209
  throw new Error("MCP client is not connected");
1055
1210
  }
1056
1211
  logger.debug(`Unsubscribing from resource: ${uri}`);
1057
- return await this.client.unsubscribeResource({ uri }, options);
1212
+ return await this.executeRequest(
1213
+ () => this.client.unsubscribeResource({ uri }, options)
1214
+ );
1058
1215
  }
1059
1216
  /**
1060
1217
  * Lists prompts exposed by the server.
@@ -1071,7 +1228,7 @@ var BaseConnector = class {
1071
1228
  }
1072
1229
  try {
1073
1230
  logger.debug("Listing prompts");
1074
- return await this.client.listPrompts();
1231
+ return await this.executeRequest(() => this.client.listPrompts());
1075
1232
  } catch (err) {
1076
1233
  const error = err;
1077
1234
  if (error.code === -32601) {
@@ -1093,7 +1250,9 @@ var BaseConnector = class {
1093
1250
  throw new Error("MCP client is not connected");
1094
1251
  }
1095
1252
  logger.debug(`Getting prompt ${name}`);
1096
- return await this.client.getPrompt({ name, arguments: args });
1253
+ return await this.executeRequest(
1254
+ () => this.client.getPrompt({ name, arguments: args })
1255
+ );
1097
1256
  }
1098
1257
  /**
1099
1258
  * Sends a raw, potentially non-standard request through the SDK client.
@@ -1108,10 +1267,12 @@ var BaseConnector = class {
1108
1267
  throw new Error("MCP client is not connected");
1109
1268
  }
1110
1269
  logger.debug(`Sending raw request '${method}' with params`, params);
1111
- return await this.client.request(
1112
- { method, params: params ?? {} },
1113
- passthroughResultSchema,
1114
- options
1270
+ return await this.executeRequest(
1271
+ () => this.client.request(
1272
+ { method, params: params ?? {} },
1273
+ passthroughResultSchema,
1274
+ options
1275
+ )
1115
1276
  );
1116
1277
  }
1117
1278
  /**
@@ -1144,6 +1305,7 @@ var BaseConnector = class {
1144
1305
  }
1145
1306
  }
1146
1307
  this.toolsCache = null;
1308
+ this.authorizationCache = void 0;
1147
1309
  if (issues.length) {
1148
1310
  logger.warn(`Resource cleanup finished with ${issues.length} issue(s)`);
1149
1311
  }
@@ -1151,9 +1313,10 @@ var BaseConnector = class {
1151
1313
  };
1152
1314
 
1153
1315
  // src/transport/http.ts
1316
+ var MIXED_AUTH_DISCOVERY_TIMEOUT_MS = 2e3;
1154
1317
  function detectUnauthorized(err, depth = 0) {
1155
1318
  if (!err || depth > 5) return false;
1156
- if (err instanceof UnauthorizedError) return true;
1319
+ if (err instanceof UnauthorizedError2) return true;
1157
1320
  if (err instanceof SdkHttpError && err.status === 401) return true;
1158
1321
  if (err instanceof Error) {
1159
1322
  if (err.cause) {
@@ -1164,6 +1327,11 @@ function detectUnauthorized(err, depth = 0) {
1164
1327
  }
1165
1328
  return false;
1166
1329
  }
1330
+ function isOAuthClientProvider(provider) {
1331
+ return Boolean(
1332
+ provider && "redirectToAuthorization" in provider && typeof provider.redirectToAuthorization === "function" && "tokens" in provider && typeof provider.tokens === "function"
1333
+ );
1334
+ }
1167
1335
  function createMcpProxyFetch(logicalServerUrl, proxyUrl, baseFetch, serverId) {
1168
1336
  const logical = new URL(logicalServerUrl);
1169
1337
  const proxy = proxyUrl.replace(/\/$/, "");
@@ -1189,6 +1357,31 @@ function createMcpProxyFetch(logicalServerUrl, proxyUrl, baseFetch, serverId) {
1189
1357
  );
1190
1358
  };
1191
1359
  }
1360
+ function createDeadlineFetch(baseFetch, deadlineSignal) {
1361
+ return async (input, init) => {
1362
+ const requestSignal = init?.signal;
1363
+ if (!requestSignal) {
1364
+ return baseFetch(input, { ...init, signal: deadlineSignal });
1365
+ }
1366
+ const controller = new AbortController();
1367
+ const abortFromRequest = () => controller.abort(requestSignal.reason);
1368
+ const abortFromDeadline = () => controller.abort(deadlineSignal.reason);
1369
+ if (requestSignal.aborted) abortFromRequest();
1370
+ else
1371
+ requestSignal.addEventListener("abort", abortFromRequest, { once: true });
1372
+ if (deadlineSignal.aborted) abortFromDeadline();
1373
+ else
1374
+ deadlineSignal.addEventListener("abort", abortFromDeadline, {
1375
+ once: true
1376
+ });
1377
+ try {
1378
+ return await baseFetch(input, { ...init, signal: controller.signal });
1379
+ } finally {
1380
+ requestSignal.removeEventListener("abort", abortFromRequest);
1381
+ deadlineSignal.removeEventListener("abort", abortFromDeadline);
1382
+ }
1383
+ };
1384
+ }
1192
1385
  var HttpConnector = class extends BaseConnector {
1193
1386
  baseUrl;
1194
1387
  headers;
@@ -1199,8 +1392,11 @@ var HttpConnector = class extends BaseConnector {
1199
1392
  gatewayUrl;
1200
1393
  serverId;
1201
1394
  reconnectionOptions;
1395
+ detectMixedAuth;
1202
1396
  transportType = null;
1203
1397
  streamableTransport = null;
1398
+ hadAccessTokenAtConnect = false;
1399
+ pendingOAuthCompletion = null;
1204
1400
  /**
1205
1401
  * Creates an HTTP connector.
1206
1402
  *
@@ -1231,6 +1427,99 @@ var HttpConnector = class extends BaseConnector {
1231
1427
  };
1232
1428
  this.protocolNegotiation = opts.protocolNegotiation ?? "auto";
1233
1429
  this.reconnectionOptions = opts.reconnectionOptions;
1430
+ this.detectMixedAuth = opts.detectMixedAuth ?? true;
1431
+ }
1432
+ get oauthProvider() {
1433
+ return isOAuthClientProvider(this.opts.authProvider) ? this.opts.authProvider : void 0;
1434
+ }
1435
+ async completeInteractiveAuthorization() {
1436
+ const provider = this.oauthProvider;
1437
+ if (!provider) {
1438
+ throw new Error("No OAuth client provider is configured");
1439
+ }
1440
+ if (!this.pendingOAuthCompletion) {
1441
+ this.pendingOAuthCompletion = completeOAuthFlow(provider, this.baseUrl, {
1442
+ fetchFn: this.customFetch,
1443
+ finishAuthorization: async (code, iss) => {
1444
+ const transport = this.streamableTransport;
1445
+ if (!transport) {
1446
+ throw new Error("OAuth transport is no longer connected");
1447
+ }
1448
+ await transport.finishAuth(code, iss);
1449
+ }
1450
+ }).then(() => {
1451
+ if (this.authorizationCache) {
1452
+ this.authorizationCache = {
1453
+ ...this.authorizationCache,
1454
+ authenticated: true
1455
+ };
1456
+ }
1457
+ }).finally(() => {
1458
+ this.pendingOAuthCompletion = null;
1459
+ });
1460
+ }
1461
+ await this.pendingOAuthCompletion;
1462
+ }
1463
+ async executeRequest(operation) {
1464
+ try {
1465
+ return await operation();
1466
+ } catch (error) {
1467
+ const provider = this.oauthProvider;
1468
+ if (!provider || provider.preventAutoAuth === true || !isOAuthInteractionRequired(error)) {
1469
+ throw error;
1470
+ }
1471
+ await this.completeInteractiveAuthorization();
1472
+ return operation();
1473
+ }
1474
+ }
1475
+ /** Authenticate an already-connected server without requiring a 401 first. */
1476
+ async authenticate() {
1477
+ if (!this.connected || !this.streamableTransport) {
1478
+ throw new Error("MCP client is not connected");
1479
+ }
1480
+ await this.completeInteractiveAuthorization();
1481
+ }
1482
+ async initialize(defaultRequestOptions = this.opts.defaultRequestOptions ?? {}) {
1483
+ const capabilities = await super.initialize(defaultRequestOptions);
1484
+ if (!this.detectMixedAuth || !this.oauthProvider || this.hadAccessTokenAtConnect) {
1485
+ return capabilities;
1486
+ }
1487
+ const controller = new AbortController();
1488
+ let timeout;
1489
+ const discoveryTimeout = new Promise((_, reject) => {
1490
+ timeout = setTimeout(() => {
1491
+ const error = new Error(
1492
+ `Mixed-auth metadata discovery timed out after ${MIXED_AUTH_DISCOVERY_TIMEOUT_MS}ms`
1493
+ );
1494
+ controller.abort(error);
1495
+ reject(error);
1496
+ }, MIXED_AUTH_DISCOVERY_TIMEOUT_MS);
1497
+ });
1498
+ const baseFetch = this.customFetch ?? globalThis.fetch.bind(globalThis);
1499
+ try {
1500
+ const metadata = await Promise.race([
1501
+ discoverOAuthProtectedResourceMetadata(
1502
+ this.baseUrl,
1503
+ { protocolVersion: this.negotiatedProtocolVersion },
1504
+ createDeadlineFetch(baseFetch, controller.signal)
1505
+ ),
1506
+ discoveryTimeout
1507
+ ]);
1508
+ this.authorizationCache = {
1509
+ mode: "mixed",
1510
+ authenticated: false,
1511
+ ...metadata.resource ? { resource: metadata.resource } : {},
1512
+ ...metadata.scopes_supported ? { scopesSupported: [...metadata.scopes_supported] } : {}
1513
+ };
1514
+ logger.info(
1515
+ "OAuth protected-resource metadata found after anonymous connection; server uses mixed auth"
1516
+ );
1517
+ } catch (error) {
1518
+ logger.debug("Mixed-auth metadata was not discovered:", error);
1519
+ } finally {
1520
+ if (timeout) clearTimeout(timeout);
1521
+ }
1522
+ return capabilities;
1234
1523
  }
1235
1524
  buildClientOptions() {
1236
1525
  return {
@@ -1337,6 +1626,16 @@ var HttpConnector = class extends BaseConnector {
1337
1626
  }
1338
1627
  const baseUrl = this.baseUrl;
1339
1628
  logger.debug(`Connecting to MCP implementation via HTTP: ${baseUrl}`);
1629
+ const oauthProvider = this.oauthProvider;
1630
+ if (oauthProvider) {
1631
+ try {
1632
+ this.hadAccessTokenAtConnect = Boolean(
1633
+ (await oauthProvider.tokens())?.access_token
1634
+ );
1635
+ } catch {
1636
+ this.hadAccessTokenAtConnect = false;
1637
+ }
1638
+ }
1340
1639
  try {
1341
1640
  await this.connectWithStreamableHttp(baseUrl);
1342
1641
  logger.debug("\u2705 Successfully connected via streamable HTTP");
@@ -1611,7 +1910,7 @@ var HttpConnector = class extends BaseConnector {
1611
1910
  };
1612
1911
 
1613
1912
  // src/utils/version.ts
1614
- var VERSION = "2.1.0";
1913
+ var VERSION = "2.1.1-canary.0";
1615
1914
  function getPackageVersion() {
1616
1915
  return VERSION;
1617
1916
  }
@@ -2577,29 +2876,43 @@ var BrowserOAuthClientProvider = class {
2577
2876
  * @param authorizationUrl - The fully constructed authorization URL from the SDK.
2578
2877
  */
2579
2878
  async redirectToAuthorization(authorizationUrl) {
2580
- const sanitizedAuthUrl = await this.prepareAuthorizationUrl(authorizationUrl);
2879
+ await this.prepareAuthorizationUrl(authorizationUrl);
2581
2880
  if (this.preventAutoAuth) {
2582
2881
  console.info(
2583
2882
  `[${this.storageKeyPrefix}] Auto-auth prevented. Authorization URL stored for manual trigger.`
2584
2883
  );
2585
2884
  return;
2586
2885
  }
2886
+ this.startAuthorization();
2887
+ }
2888
+ /**
2889
+ * Open the authorization URL prepared by the official SDK.
2890
+ *
2891
+ * This is the explicit-user-action counterpart to `preventAutoAuth`: the
2892
+ * provider still lets the SDK own discovery and PKCE state, while a host can
2893
+ * launch the stored authorization request later from an Authenticate button.
2894
+ */
2895
+ startAuthorization() {
2896
+ const authorizationUrl = this.lastAttemptedAuthUrl;
2897
+ if (!authorizationUrl) {
2898
+ throw new Error("No prepared OAuth authorization is available");
2899
+ }
2587
2900
  if (this.useRedirectFlow) {
2588
2901
  console.info(
2589
2902
  `[${this.storageKeyPrefix}] Redirecting to authorization URL (full-page redirect).`
2590
2903
  );
2591
- window.location.href = sanitizedAuthUrl;
2904
+ window.location.href = authorizationUrl;
2592
2905
  return;
2593
2906
  }
2594
2907
  const popupFeatures = "width=600,height=700,resizable=yes,scrollbars=yes,status=yes";
2595
2908
  try {
2596
2909
  const popup = window.open(
2597
- sanitizedAuthUrl,
2910
+ authorizationUrl,
2598
2911
  `mcp_auth_${this.serverUrlHash}`,
2599
2912
  popupFeatures
2600
2913
  );
2601
2914
  if (this.onPopupWindow) {
2602
- this.onPopupWindow(sanitizedAuthUrl, popupFeatures, popup);
2915
+ this.onPopupWindow(authorizationUrl, popupFeatures, popup);
2603
2916
  }
2604
2917
  if (!popup || popup.closed || typeof popup.closed === "undefined") {
2605
2918
  console.warn(
@@ -3148,103 +3461,6 @@ function setTelemetrySource(source) {
3148
3461
  Tel.getInstance().setSource(source);
3149
3462
  }
3150
3463
 
3151
- // src/auth/flow.ts
3152
- import {
3153
- auth,
3154
- UnauthorizedError as UnauthorizedError2
3155
- } from "@modelcontextprotocol/client";
3156
- var DEFAULT_AUTH_TIMEOUT_MS = 5 * 6e4;
3157
- function isUnauthorized(err, depth = 0) {
3158
- if (!err || depth > 5) return false;
3159
- if (err instanceof UnauthorizedError2) return true;
3160
- if (err instanceof Error) {
3161
- const code = err.code;
3162
- if (code === 401) return true;
3163
- if (err.name === "UnauthorizedError") return true;
3164
- const message = err.message ?? "";
3165
- if (message.includes("401") || message.includes("Unauthorized")) {
3166
- return true;
3167
- }
3168
- if (err.cause && isUnauthorized(err.cause, depth + 1)) return true;
3169
- const data = err.data;
3170
- if (data?.cause && isUnauthorized(data.cause, depth + 1)) return true;
3171
- }
3172
- return false;
3173
- }
3174
- async function completeOAuthFlow(provider, serverUrl, options = {}) {
3175
- const flowProvider = provider;
3176
- const timeoutMs = options.timeoutMs ?? DEFAULT_AUTH_TIMEOUT_MS;
3177
- const fetchFn = options.fetchFn ?? flowProvider.getProxyFetch?.() ?? void 0;
3178
- if (!flowProvider.hasPendingFlow) {
3179
- const result = await auth(provider, { serverUrl, fetchFn });
3180
- if (result === "AUTHORIZED") return;
3181
- if (result !== "REDIRECT") {
3182
- throw new Error(`Unexpected OAuth auth() result: ${result}`);
3183
- }
3184
- }
3185
- if (typeof flowProvider.getAuthorizationResponse === "function" || typeof flowProvider.getAuthorizationCode === "function") {
3186
- const response = typeof flowProvider.getAuthorizationResponse === "function" ? await flowProvider.getAuthorizationResponse() : { code: await flowProvider.getAuthorizationCode() };
3187
- await auth(provider, {
3188
- serverUrl,
3189
- authorizationCode: response.code,
3190
- ...response.iss !== void 0 ? { iss: response.iss } : {},
3191
- fetchFn
3192
- });
3193
- return;
3194
- }
3195
- await waitForBrowserAuthComplete(flowProvider, timeoutMs);
3196
- }
3197
- async function waitForBrowserAuthComplete(provider, timeoutMs) {
3198
- if (typeof window === "undefined") {
3199
- throw new Error(
3200
- "OAuth redirect requires a browser environment or a provider with getAuthorizationCode()"
3201
- );
3202
- }
3203
- if (provider.useRedirectFlow) {
3204
- await new Promise(() => {
3205
- });
3206
- return;
3207
- }
3208
- const tokensKey = provider.getKey?.("tokens");
3209
- if (!tokensKey) {
3210
- throw new Error(
3211
- "Browser OAuth provider must expose getKey() for token storage"
3212
- );
3213
- }
3214
- let state = null;
3215
- const authUrl = provider.getLastAttemptedAuthUrl?.();
3216
- if (authUrl) {
3217
- try {
3218
- state = new URL(authUrl).searchParams.get("state");
3219
- } catch {
3220
- }
3221
- }
3222
- try {
3223
- const result = await runAuthPopup({
3224
- popup: null,
3225
- state,
3226
- tokensKey,
3227
- timeoutMs
3228
- });
3229
- switch (result.kind) {
3230
- case "success":
3231
- return;
3232
- case "cancelled":
3233
- throw new Error("OAuth authentication was cancelled.");
3234
- case "timeout":
3235
- throw new Error(
3236
- `OAuth callback not received within ${timeoutMs}ms. Ensure /oauth/callback calls onMcpAuthorization().`
3237
- );
3238
- case "error":
3239
- throw new Error(result.error);
3240
- default:
3241
- throw new Error("Unexpected OAuth popup result");
3242
- }
3243
- } finally {
3244
- provider.markFlowComplete?.();
3245
- }
3246
- }
3247
-
3248
3464
  // src/core/base.ts
3249
3465
  init_logging();
3250
3466
 
@@ -3475,6 +3691,14 @@ var MCPConnection = class {
3475
3691
  get serverInfo() {
3476
3692
  return this.connector.serverInfo;
3477
3693
  }
3694
+ /** OAuth state discovered for this connection, when available. */
3695
+ get authorization() {
3696
+ return this.connector.authorization;
3697
+ }
3698
+ /** Authenticate an already-connected mixed-auth server. */
3699
+ async authenticate() {
3700
+ await this.connector.authenticate();
3701
+ }
3478
3702
  /**
3479
3703
  * The negotiated protocol era for this session's connection:
3480
3704
  * `"legacy"` (2025-era) or `"modern"` (2026-07-28-era).
@@ -3507,7 +3731,8 @@ var MCPConnection = class {
3507
3731
  ...server ? { server } : {},
3508
3732
  capabilities,
3509
3733
  instructions: this.connector.instructions,
3510
- extensions
3734
+ extensions,
3735
+ ...this.authorization ? { authorization: this.authorization } : {}
3511
3736
  };
3512
3737
  }
3513
3738
  /**
@@ -3747,7 +3972,7 @@ function trackClientRemoveServer(name) {
3747
3972
  }
3748
3973
 
3749
3974
  // src/core/base.ts
3750
- function isOAuthClientProvider(provider) {
3975
+ function isOAuthClientProvider2(provider) {
3751
3976
  return !!provider && typeof provider === "object" && "redirectUrl" in provider && "clientMetadata" in provider;
3752
3977
  }
3753
3978
  var BaseMCPClient = class {
@@ -3980,7 +4205,7 @@ var BaseMCPClient = class {
3980
4205
  ...serverConfig,
3981
4206
  authProvider: oauthProvider
3982
4207
  };
3983
- } else if ("authProvider" in serverConfig && serverConfig.authProvider && isOAuthClientProvider(serverConfig.authProvider)) {
4208
+ } else if ("authProvider" in serverConfig && serverConfig.authProvider && isOAuthClientProvider2(serverConfig.authProvider)) {
3984
4209
  oauthProvider = serverConfig.authProvider;
3985
4210
  }
3986
4211
  const openSession = async () => {
@@ -4320,6 +4545,7 @@ var BrowserMCPClient = class _BrowserMCPClient extends BaseMCPClient {
4320
4545
  fetch: configuredFetch,
4321
4546
  authToken,
4322
4547
  authProvider,
4548
+ detectMixedAuth,
4323
4549
  wrapTransport,
4324
4550
  clientOptions,
4325
4551
  protocolNegotiation,
@@ -4344,6 +4570,7 @@ var BrowserMCPClient = class _BrowserMCPClient extends BaseMCPClient {
4344
4570
  fetch: configuredFetch ?? globalThis.fetch.bind(globalThis),
4345
4571
  authToken,
4346
4572
  authProvider,
4573
+ detectMixedAuth,
4347
4574
  wrapTransport,
4348
4575
  clientOptions,
4349
4576
  onSampling: resolved.onSampling,
@@ -4655,6 +4882,9 @@ function useMcpOperations(params) {
4655
4882
  });
4656
4883
  return result;
4657
4884
  } catch (error) {
4885
+ if (isOAuthInteractionRequired(error)) {
4886
+ params.onAuthorizationRequired(error);
4887
+ }
4658
4888
  params.addLog("error", `Tool "${name}" call failed:`, error);
4659
4889
  Tel.getInstance().trackUseMcpToolCall({
4660
4890
  toolName: name,
@@ -4666,7 +4896,7 @@ function useMcpOperations(params) {
4666
4896
  throw error;
4667
4897
  }
4668
4898
  },
4669
- [params.addLog]
4899
+ [params.addLog, params.onAuthorizationRequired]
4670
4900
  );
4671
4901
  const listResources = useCallback(async () => {
4672
4902
  const connection = requireConnection(params, "list resources");
@@ -4842,6 +5072,7 @@ function useMcp(options) {
4842
5072
  reconnectionOptions,
4843
5073
  preventAutoAuth = true,
4844
5074
  // Default to true - require explicit user action for OAuth
5075
+ detectMixedAuth = true,
4845
5076
  useRedirectFlow = false,
4846
5077
  // Default to false for backward compatibility (use popup)
4847
5078
  onPopupWindow,
@@ -5019,6 +5250,7 @@ function useMcp(options) {
5019
5250
  const [log, setLog] = useState([]);
5020
5251
  const [authUrl, setAuthUrl] = useState(void 0);
5021
5252
  const [authTokens, setAuthTokens] = useState(void 0);
5253
+ const [authorization, setAuthorization] = useState(void 0);
5022
5254
  const clientRef = useRef(null);
5023
5255
  const connectionRef = useRef(null);
5024
5256
  const authProviderRef = useRef(
@@ -5033,6 +5265,9 @@ function useMcp(options) {
5033
5265
  const retryScheduledRef = useRef(false);
5034
5266
  const popupFlowActiveRef = useRef(false);
5035
5267
  const stateRef = useRef(state);
5268
+ const authorizationRef = useRef(authorization);
5269
+ const authorizationServerUrlRef = useRef(url);
5270
+ authorizationRef.current = authorization;
5036
5271
  const autoReconnectRef = useRef(autoReconnect);
5037
5272
  const successfulTransportRef = useRef(null);
5038
5273
  const connectRef = useRef(null);
@@ -5099,6 +5334,24 @@ function useMcp(options) {
5099
5334
  },
5100
5335
  [instanceLogger]
5101
5336
  );
5337
+ const onAuthorizationRequired = useCallback2(
5338
+ (authError) => {
5339
+ const preparedAuthUrl = authProviderRef.current?.getLastAttemptedAuthUrl?.() ?? void 0;
5340
+ addLog(
5341
+ "info",
5342
+ "This server requires OAuth for the requested operation; waiting for authentication.",
5343
+ authError
5344
+ );
5345
+ const authorizationRequired = {
5346
+ ...authorizationRef.current ?? { mode: "mixed" },
5347
+ authenticated: false
5348
+ };
5349
+ authorizationRef.current = authorizationRequired;
5350
+ setAuthorization(authorizationRequired);
5351
+ if (preparedAuthUrl) setAuthUrl(preparedAuthUrl);
5352
+ },
5353
+ [addLog]
5354
+ );
5102
5355
  const connectionOperations = useMcpOperations({
5103
5356
  stateRef,
5104
5357
  connectionRef,
@@ -5109,7 +5362,8 @@ function useMcp(options) {
5109
5362
  setResourceTemplates,
5110
5363
  setPrompts,
5111
5364
  setSkills,
5112
- addLog
5365
+ addLog,
5366
+ onAuthorizationRequired
5113
5367
  });
5114
5368
  const disconnect = useCallback2(
5115
5369
  async (quiet = false) => {
@@ -5250,6 +5504,11 @@ function useMcp(options) {
5250
5504
  connectingRef.current = true;
5251
5505
  connectEpochRef.current += 1;
5252
5506
  connectAttemptRef.current += 1;
5507
+ if (authorizationServerUrlRef.current !== url) {
5508
+ authorizationServerUrlRef.current = url;
5509
+ authorizationRef.current = void 0;
5510
+ setAuthorization(void 0);
5511
+ }
5253
5512
  setError(void 0);
5254
5513
  setAuthUrl(void 0);
5255
5514
  successfulTransportRef.current = null;
@@ -5336,6 +5595,7 @@ function useMcp(options) {
5336
5595
  // Protocol era negotiation mode ("legacy" | "auto" | { pin }); the
5337
5596
  // connector defaults to automatic v1/v2 negotiation.
5338
5597
  ...protocolNegotiation !== void 0 && { protocolNegotiation },
5598
+ detectMixedAuth,
5339
5599
  // Pass user-configurable reconnection options, or when autoReconnect
5340
5600
  // is disabled, disable SDK transport reconnection to prevent
5341
5601
  // unwanted GET polling requests
@@ -5501,8 +5761,13 @@ function useMcp(options) {
5501
5761
  protocolEra: protocolEra2,
5502
5762
  protocolVersion: protocolVersion2,
5503
5763
  instructions: instructions2,
5504
- extensions: extensions2
5764
+ extensions: extensions2,
5765
+ authorization: connectionAuthorization
5505
5766
  } = connection.info;
5767
+ if (connectionAuthorization) {
5768
+ setAuthorization(connectionAuthorization);
5769
+ authorizationRef.current = connectionAuthorization;
5770
+ }
5506
5771
  if (isMountedRef.current) {
5507
5772
  setProtocolEra(protocolEra2);
5508
5773
  setProtocolVersion(protocolVersion2);
@@ -5553,6 +5818,14 @@ function useMcp(options) {
5553
5818
  return "failed";
5554
5819
  }
5555
5820
  if (tokens?.access_token) {
5821
+ if (authorizationRef.current?.mode === "mixed") {
5822
+ const authenticatedAuthorization = {
5823
+ ...authorizationRef.current,
5824
+ authenticated: true
5825
+ };
5826
+ setAuthorization(authenticatedAuthorization);
5827
+ authorizationRef.current = authenticatedAuthorization;
5828
+ }
5556
5829
  const expiresAt = getOAuthTokenExpiry(tokens);
5557
5830
  let tokenEndpoint = null;
5558
5831
  let resource = null;
@@ -5732,6 +6005,7 @@ function useMcp(options) {
5732
6005
  headers,
5733
6006
  transportType,
5734
6007
  preventAutoAuth,
6008
+ detectMixedAuth,
5735
6009
  useRedirectFlow,
5736
6010
  onPopupWindow,
5737
6011
  enabled,
@@ -5768,11 +6042,12 @@ function useMcp(options) {
5768
6042
  const authenticate = useCallback2(async () => {
5769
6043
  addLog("info", "Manual authentication requested...");
5770
6044
  const currentState = stateRef.current;
6045
+ const isOptionalMixedAuthentication = currentState === "ready" && authorizationRef.current?.mode === "mixed";
5771
6046
  if (currentState === "failed") {
5772
6047
  addLog("info", "Attempting to reconnect and authenticate via retry...");
5773
6048
  retry();
5774
- } else if (currentState === "pending_auth") {
5775
- addLog("info", "Proceeding with authentication from pending state...");
6049
+ } else if (currentState === "pending_auth" || currentState === "ready" && authorizationRef.current?.mode === "mixed" && !authorizationRef.current.authenticated) {
6050
+ addLog("info", "Proceeding with authentication...");
5776
6051
  try {
5777
6052
  assert(
5778
6053
  authProviderRef.current,
@@ -5891,16 +6166,16 @@ function useMcp(options) {
5891
6166
  case "cancelled":
5892
6167
  addLog(
5893
6168
  "warn",
5894
- "Authentication popup was closed before completing. Returning to pending_auth."
6169
+ isOptionalMixedAuthentication ? "Authentication popup was closed before completing. Public tools remain available." : "Authentication popup was closed before completing. Returning to pending_auth."
5895
6170
  );
5896
- setState("pending_auth");
6171
+ setState(isOptionalMixedAuthentication ? "ready" : "pending_auth");
5897
6172
  break;
5898
6173
  case "timeout":
5899
6174
  addLog(
5900
6175
  "warn",
5901
- "Authentication timed out waiting for the popup. Returning to pending_auth."
6176
+ isOptionalMixedAuthentication ? "Authentication timed out waiting for the popup. Public tools remain available." : "Authentication timed out waiting for the popup. Returning to pending_auth."
5902
6177
  );
5903
- setState("pending_auth");
6178
+ setState(isOptionalMixedAuthentication ? "ready" : "pending_auth");
5904
6179
  break;
5905
6180
  case "error":
5906
6181
  failConnection(`Authentication failed: ${result.error}`);
@@ -6189,6 +6464,7 @@ function useMcp(options) {
6189
6464
  log,
6190
6465
  authUrl,
6191
6466
  authTokens,
6467
+ authorization,
6192
6468
  client: clientRef.current,
6193
6469
  ...connectionOperations,
6194
6470
  retry,
@@ -6598,7 +6874,7 @@ function isSameMcpServer(left, right) {
6598
6874
  return left.id === right.id && sameSerializedValue(
6599
6875
  pickLiveServerConfig(left),
6600
6876
  pickLiveServerConfig(right)
6601
- ) && left.name === right.name && left.state === right.state && left.error === right.error && left.authUrl === right.authUrl && sameSerializedValue(left.authTokens, right.authTokens) && left.protocolEra === right.protocolEra && left.protocolVersion === right.protocolVersion && sameSerializedValue(left.serverInfo, right.serverInfo) && sameSerializedValue(left.capabilities, right.capabilities) && left.instructions === right.instructions && sameSerializedValue(left.extensions, right.extensions) && sameSerializedValue(left.tools, right.tools) && sameSerializedValue(left.resources, right.resources) && sameSerializedValue(left.resourceTemplates, right.resourceTemplates) && sameSerializedValue(left.prompts, right.prompts) && sameSerializedValue(left.notifications, right.notifications) && left.unreadNotificationCount === right.unreadNotificationCount && sameSerializedValue(
6877
+ ) && left.name === right.name && left.state === right.state && left.error === right.error && left.authUrl === right.authUrl && sameSerializedValue(left.authTokens, right.authTokens) && sameSerializedValue(left.authorization, right.authorization) && left.protocolEra === right.protocolEra && left.protocolVersion === right.protocolVersion && sameSerializedValue(left.serverInfo, right.serverInfo) && sameSerializedValue(left.capabilities, right.capabilities) && left.instructions === right.instructions && sameSerializedValue(left.extensions, right.extensions) && sameSerializedValue(left.tools, right.tools) && sameSerializedValue(left.resources, right.resources) && sameSerializedValue(left.resourceTemplates, right.resourceTemplates) && sameSerializedValue(left.prompts, right.prompts) && sameSerializedValue(left.notifications, right.notifications) && left.unreadNotificationCount === right.unreadNotificationCount && sameSerializedValue(
6602
6878
  left.pendingSamplingRequests,
6603
6879
  right.pendingSamplingRequests
6604
6880
  ) && sameSerializedValue(
@@ -6780,6 +7056,7 @@ function McpServerWrapper({
6780
7056
  mcp.instructions,
6781
7057
  mcp.extensions,
6782
7058
  mcp.authTokens,
7059
+ mcp.authorization,
6783
7060
  // Functions excluded - they're stable via useCallback in useMcp
6784
7061
  // mcp.log excluded - log changes shouldn't trigger provider updates
6785
7062
  // mcp.client excluded - client reference stability handled by manual check
@@ -8991,6 +9268,7 @@ export {
8991
9268
  getAllRpcLogs,
8992
9269
  getRpcLogs,
8993
9270
  getViewResourceUri,
9271
+ isOAuthInteractionRequired,
8994
9272
  isToolVisibleToModel,
8995
9273
  isViewResource,
8996
9274
  isViewTool,