@mastra/mcp 1.14.0 → 1.15.0-alpha.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.
package/dist/index.cjs CHANGED
@@ -12,8 +12,10 @@ var streamableHttp_js = require('@modelcontextprotocol/sdk/client/streamableHttp
12
12
  var protocol_js = require('@modelcontextprotocol/sdk/shared/protocol.js');
13
13
  var types_js = require('@modelcontextprotocol/sdk/types.js');
14
14
  var exitHook = require('exit-hook');
15
+ var auth_js = require('@modelcontextprotocol/sdk/client/auth.js');
15
16
  var crypto$1 = require('crypto');
16
17
  var equal = require('fast-deep-equal');
18
+ var http = require('http');
17
19
  var mcp = require('@mastra/core/mcp');
18
20
  var schema = require('@mastra/core/schema');
19
21
  var fs = require('fs');
@@ -24,7 +26,6 @@ var index_js$1 = require('@modelcontextprotocol/sdk/server/index.js');
24
26
  var sse_js$1 = require('@modelcontextprotocol/sdk/server/sse.js');
25
27
  var stdio_js$1 = require('@modelcontextprotocol/sdk/server/stdio.js');
26
28
  var streamableHttp_js$1 = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
27
- var auth_js = require('@modelcontextprotocol/sdk/client/auth.js');
28
29
 
29
30
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
30
31
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
@@ -31083,6 +31084,44 @@ function getMastraToolStrictMeta(meta3) {
31083
31084
  const strict = mastraMeta[STRICT_META_KEY];
31084
31085
  return typeof strict === "boolean" ? strict : void 0;
31085
31086
  }
31087
+ function escapeHeaderValue(value) {
31088
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
31089
+ }
31090
+ function generateWWWAuthenticateHeader(options = {}) {
31091
+ const params = [];
31092
+ if (options.resourceMetadataUrl) {
31093
+ params.push(`resource_metadata="${escapeHeaderValue(options.resourceMetadataUrl)}"`);
31094
+ }
31095
+ if (options.additionalParams) {
31096
+ for (const [key, value] of Object.entries(options.additionalParams)) {
31097
+ params.push(`${key}="${escapeHeaderValue(value)}"`);
31098
+ }
31099
+ }
31100
+ if (params.length === 0) {
31101
+ return "Bearer";
31102
+ }
31103
+ return `Bearer ${params.join(", ")}`;
31104
+ }
31105
+ function generateProtectedResourceMetadata(config2) {
31106
+ return {
31107
+ resource: config2.resource,
31108
+ authorization_servers: config2.authorizationServers,
31109
+ scopes_supported: config2.scopesSupported ?? ["mcp:read", "mcp:write"],
31110
+ bearer_methods_supported: ["header"],
31111
+ ...config2.resourceName && { resource_name: config2.resourceName },
31112
+ ...config2.resourceDocumentation && {
31113
+ resource_documentation: config2.resourceDocumentation
31114
+ }
31115
+ };
31116
+ }
31117
+ function extractBearerToken(authHeader) {
31118
+ if (!authHeader) return void 0;
31119
+ const prefix = "bearer ";
31120
+ if (authHeader.length <= prefix.length) return void 0;
31121
+ if (authHeader.slice(0, prefix.length).toLowerCase() !== prefix) return void 0;
31122
+ const token = authHeader.slice(prefix.length).trim();
31123
+ return token || void 0;
31124
+ }
31086
31125
 
31087
31126
  // src/client/actions/elicitation.ts
31088
31127
  var ElicitationClientActions = class {
@@ -31539,6 +31578,8 @@ var InternalMastraMCPClient = class extends base.MastraBase {
31539
31578
  enableProgressTracking;
31540
31579
  serverConfig;
31541
31580
  transport;
31581
+ pendingAuthTransport;
31582
+ _authState;
31542
31583
  operationContextStore = new async_hooks.AsyncLocalStorage();
31543
31584
  exitHookUnsubscribe;
31544
31585
  sigTermHandler;
@@ -31723,14 +31764,14 @@ var InternalMastraMCPClient = class extends base.MastraBase {
31723
31764
  this.log("debug", `Attempting to connect to URL: ${url2}`);
31724
31765
  let shouldTrySSE = url2.pathname.endsWith(`/sse`);
31725
31766
  if (!shouldTrySSE) {
31767
+ const streamableTransport = new streamableHttp_js.StreamableHTTPClientTransport(url2, {
31768
+ requestInit,
31769
+ reconnectionOptions: this.serverConfig.reconnectionOptions,
31770
+ authProvider,
31771
+ fetch: fetch2
31772
+ });
31726
31773
  try {
31727
31774
  this.log("debug", "Trying Streamable HTTP transport...");
31728
- const streamableTransport = new streamableHttp_js.StreamableHTTPClientTransport(url2, {
31729
- requestInit,
31730
- reconnectionOptions: this.serverConfig.reconnectionOptions,
31731
- authProvider,
31732
- fetch: fetch2
31733
- });
31734
31775
  await this.client.connect(streamableTransport, {
31735
31776
  timeout: connectTimeout ?? DEFAULT_SERVER_CONNECT_TIMEOUT_MSEC
31736
31777
  });
@@ -31738,6 +31779,10 @@ var InternalMastraMCPClient = class extends base.MastraBase {
31738
31779
  this.log("debug", "Successfully connected using Streamable HTTP transport.");
31739
31780
  } catch (error51) {
31740
31781
  this.log("debug", `Streamable HTTP transport failed: ${error51}`);
31782
+ if (authProvider && error51 instanceof auth_js.UnauthorizedError) {
31783
+ this.markNeedsAuth(streamableTransport);
31784
+ throw error51;
31785
+ }
31741
31786
  const status = error51?.code;
31742
31787
  if (status !== void 0 && !SSE_FALLBACK_STATUS_CODES.includes(status)) {
31743
31788
  throw error51;
@@ -31747,18 +31792,22 @@ var InternalMastraMCPClient = class extends base.MastraBase {
31747
31792
  }
31748
31793
  if (shouldTrySSE) {
31749
31794
  this.log("debug", "Falling back to deprecated HTTP+SSE transport...");
31795
+ const sseEventSourceInit = { ...eventSourceInit, fetch: eventSourceInit?.fetch ?? fetch2 };
31796
+ const sseTransport = new sse_js.SSEClientTransport(url2, {
31797
+ requestInit,
31798
+ eventSourceInit: sseEventSourceInit,
31799
+ authProvider,
31800
+ fetch: fetch2
31801
+ });
31750
31802
  try {
31751
- const sseEventSourceInit = { ...eventSourceInit, fetch: fetch2 };
31752
- const sseTransport = new sse_js.SSEClientTransport(url2, {
31753
- requestInit,
31754
- eventSourceInit: sseEventSourceInit,
31755
- authProvider,
31756
- fetch: fetch2
31757
- });
31758
31803
  await this.client.connect(sseTransport, { timeout: this.serverConfig.timeout ?? this.timeout });
31759
31804
  this.transport = sseTransport;
31760
31805
  this.log("debug", "Successfully connected using deprecated HTTP+SSE transport.");
31761
31806
  } catch (sseError) {
31807
+ if (authProvider && sseError instanceof auth_js.UnauthorizedError) {
31808
+ this.markNeedsAuth(sseTransport);
31809
+ throw sseError;
31810
+ }
31762
31811
  this.log(
31763
31812
  "error",
31764
31813
  `Failed to connect with SSE transport after failing to connect to Streamable HTTP transport first. SSE error: ${sseError}`
@@ -31766,6 +31815,64 @@ var InternalMastraMCPClient = class extends base.MastraBase {
31766
31815
  throw new Error("Could not connect to server with any available HTTP transport");
31767
31816
  }
31768
31817
  }
31818
+ this.closePendingAuthTransport();
31819
+ if (authProvider) {
31820
+ this._authState = "authorized";
31821
+ }
31822
+ }
31823
+ /**
31824
+ * Closes and clears any transport retained from an unfinished authorization
31825
+ * flow. Safe to call when nothing is pending. Centralizes the cleanup so
31826
+ * success, disconnect, and forceReconnect all release the same resource.
31827
+ */
31828
+ closePendingAuthTransport(replacement) {
31829
+ const pending = this.pendingAuthTransport;
31830
+ this.pendingAuthTransport = replacement;
31831
+ if (pending && pending !== replacement) {
31832
+ void pending.close().catch(() => {
31833
+ });
31834
+ }
31835
+ }
31836
+ /**
31837
+ * Records that the server rejected the connection with a 401 and keeps the
31838
+ * transport that started the authorization flow so finishAuth can complete it.
31839
+ */
31840
+ markNeedsAuth(transport) {
31841
+ this.closePendingAuthTransport(transport);
31842
+ this._authState = "needs-auth";
31843
+ this.log("debug", "Server requires OAuth authorization before connecting.");
31844
+ }
31845
+ /**
31846
+ * OAuth authorization state of this server connection, when it has an authProvider.
31847
+ *
31848
+ * @internal
31849
+ */
31850
+ get authState() {
31851
+ return this._authState;
31852
+ }
31853
+ /**
31854
+ * Completes a pending OAuth authorization-code flow.
31855
+ *
31856
+ * Exchanges the authorization code captured at the redirect URI on the same
31857
+ * transport that started the flow, then leaves the client ready to connect().
31858
+ *
31859
+ * @param authorizationCode - The authorization code captured at the redirect URI
31860
+ * @throws {Error} If no authorization flow is pending for this server
31861
+ *
31862
+ * @internal
31863
+ */
31864
+ async finishAuth(authorizationCode) {
31865
+ const pending = this.pendingAuthTransport;
31866
+ if (!pending) {
31867
+ throw new Error("No OAuth authorization is pending for this server. Call connect() first.");
31868
+ }
31869
+ this.pendingAuthTransport = void 0;
31870
+ try {
31871
+ await pending.finishAuth(authorizationCode);
31872
+ } finally {
31873
+ void pending.close().catch(() => {
31874
+ });
31875
+ }
31769
31876
  }
31770
31877
  isConnected = null;
31771
31878
  /**
@@ -31876,6 +31983,7 @@ var InternalMastraMCPClient = class extends base.MastraBase {
31876
31983
  this.serverInstructions = this.client.getInstructions();
31877
31984
  }
31878
31985
  async disconnect() {
31986
+ this.closePendingAuthTransport();
31879
31987
  if (!this.transport) {
31880
31988
  this.log("debug", "Disconnect called but no transport was connected.");
31881
31989
  return;
@@ -31920,6 +32028,7 @@ var InternalMastraMCPClient = class extends base.MastraBase {
31920
32028
  */
31921
32029
  async forceReconnect() {
31922
32030
  this.log("debug", "Forcing reconnection to MCP server...");
32031
+ this.closePendingAuthTransport();
31923
32032
  try {
31924
32033
  if (this.transport) {
31925
32034
  await this.transport.close();
@@ -32198,6 +32307,399 @@ var InternalMastraMCPClient = class extends base.MastraBase {
32198
32307
  };
32199
32308
  }
32200
32309
  };
32310
+ var CALLBACK_PORT_FALLBACK_RANGE = 10;
32311
+ var DEFAULT_CALLBACK_TIMEOUT_MS = 5 * 60 * 1e3;
32312
+ var SUCCESS_HTML = `<!DOCTYPE html>
32313
+ <html>
32314
+ <head><meta charset="utf-8"><title>Authentication complete</title></head>
32315
+ <body style="font-family: system-ui, sans-serif; text-align: center; padding-top: 4rem;">
32316
+ <h1>Authentication complete</h1>
32317
+ <p>You can close this tab and return to your application.</p>
32318
+ </body>
32319
+ </html>`;
32320
+ var ERROR_HTML = `<!DOCTYPE html>
32321
+ <html>
32322
+ <head><meta charset="utf-8"><title>Authentication failed</title></head>
32323
+ <body style="font-family: system-ui, sans-serif; text-align: center; padding-top: 4rem;">
32324
+ <h1>Authentication failed</h1>
32325
+ <p>You can close this tab and retry from your application.</p>
32326
+ </body>
32327
+ </html>`;
32328
+ function getCallbackUrlCandidates(redirectUrl) {
32329
+ const base = new URL(redirectUrl.toString());
32330
+ const preferredPort = base.port ? Number(base.port) : base.protocol === "https:" ? 443 : 80;
32331
+ const candidates = [];
32332
+ for (let offset = 0; offset <= CALLBACK_PORT_FALLBACK_RANGE; offset++) {
32333
+ const port = preferredPort + offset;
32334
+ if (port > 65535) break;
32335
+ const candidate = new URL(base.toString());
32336
+ candidate.port = String(port);
32337
+ candidates.push(candidate);
32338
+ }
32339
+ return candidates;
32340
+ }
32341
+ function stateMatches(receivedState, expectedState) {
32342
+ const received = Buffer.from(receivedState);
32343
+ const expected = Buffer.from(expectedState);
32344
+ return received.length === expected.length && crypto$1.timingSafeEqual(received, expected);
32345
+ }
32346
+ function listen(server, port, hostname3) {
32347
+ return new Promise((resolve, reject) => {
32348
+ const onError = (error51) => {
32349
+ server.off("listening", onListening);
32350
+ if (error51.code === "EADDRINUSE") {
32351
+ resolve(error51);
32352
+ } else {
32353
+ reject(error51);
32354
+ }
32355
+ };
32356
+ const onListening = () => {
32357
+ server.off("error", onError);
32358
+ resolve(null);
32359
+ };
32360
+ server.once("error", onError);
32361
+ server.once("listening", onListening);
32362
+ server.listen(port, hostname3);
32363
+ });
32364
+ }
32365
+ async function createOAuthCallbackServer(options) {
32366
+ const candidates = getCallbackUrlCandidates(options.redirectUrl);
32367
+ const callbackPath = candidates[0].pathname;
32368
+ let settled = false;
32369
+ let resolveCode;
32370
+ let rejectCode;
32371
+ const codePromise = new Promise((resolve, reject) => {
32372
+ resolveCode = resolve;
32373
+ rejectCode = reject;
32374
+ });
32375
+ codePromise.catch(() => {
32376
+ });
32377
+ const settle = (outcome) => {
32378
+ if (settled) return;
32379
+ settled = true;
32380
+ if ("result" in outcome) {
32381
+ resolveCode(outcome.result);
32382
+ } else {
32383
+ rejectCode(outcome.error);
32384
+ }
32385
+ };
32386
+ const server = http.createServer((req, res) => {
32387
+ const url2 = new URL(req.url ?? "", "http://localhost");
32388
+ if (url2.pathname !== callbackPath) {
32389
+ res.statusCode = 404;
32390
+ res.end("Not found");
32391
+ return;
32392
+ }
32393
+ if (settled) {
32394
+ res.statusCode = 410;
32395
+ res.end("Callback already handled");
32396
+ return;
32397
+ }
32398
+ const respond = (statusCode, html) => {
32399
+ res.statusCode = statusCode;
32400
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
32401
+ res.end(html);
32402
+ };
32403
+ const state = url2.searchParams.get("state");
32404
+ if (state === null || !stateMatches(state, options.state)) {
32405
+ respond(400, ERROR_HTML);
32406
+ return;
32407
+ }
32408
+ const error51 = url2.searchParams.get("error");
32409
+ if (error51) {
32410
+ const description = url2.searchParams.get("error_description");
32411
+ respond(400, ERROR_HTML);
32412
+ settle({ error: new Error(`Authorization failed: ${error51}${description ? ` (${description})` : ""}`) });
32413
+ return;
32414
+ }
32415
+ const code = url2.searchParams.get("code");
32416
+ if (!code) {
32417
+ respond(400, ERROR_HTML);
32418
+ settle({ error: new Error("Authorization failed: missing authorization code") });
32419
+ return;
32420
+ }
32421
+ respond(200, SUCCESS_HTML);
32422
+ settle({ result: { code, state } });
32423
+ });
32424
+ const hostname3 = candidates[0].hostname.replace(/^\[|\]$/g, "");
32425
+ let boundUrl;
32426
+ let boundPort;
32427
+ for (const candidate of candidates) {
32428
+ const candidatePort = Number(candidate.port);
32429
+ const error51 = await listen(server, candidatePort, hostname3);
32430
+ if (!error51) {
32431
+ boundUrl = candidate;
32432
+ boundPort = candidatePort;
32433
+ break;
32434
+ }
32435
+ }
32436
+ if (!boundUrl || boundPort === void 0) {
32437
+ const firstPort = Number(candidates[0].port);
32438
+ const lastPort = Number(candidates[candidates.length - 1].port);
32439
+ throw new Error(`Failed to start OAuth callback server: ports ${firstPort}-${lastPort} are all in use`);
32440
+ }
32441
+ server.on("error", (error51) => {
32442
+ settle({ error: error51 instanceof Error ? error51 : new Error(String(error51)) });
32443
+ });
32444
+ return {
32445
+ url: boundUrl,
32446
+ port: boundPort,
32447
+ waitForCode({ timeoutMs = DEFAULT_CALLBACK_TIMEOUT_MS } = {}) {
32448
+ let timer;
32449
+ const timeout = new Promise((_, reject) => {
32450
+ timer = setTimeout(
32451
+ () => reject(new Error(`Timed out waiting for OAuth callback after ${timeoutMs}ms`)),
32452
+ timeoutMs
32453
+ );
32454
+ timer.unref?.();
32455
+ });
32456
+ return Promise.race([codePromise, timeout]).finally(() => clearTimeout(timer));
32457
+ },
32458
+ close() {
32459
+ settle({ error: new Error("OAuth callback server closed before receiving an authorization code") });
32460
+ server.closeIdleConnections();
32461
+ return new Promise((resolve, reject) => {
32462
+ server.close((error51) => {
32463
+ if (error51 && error51.code !== "ERR_SERVER_NOT_RUNNING") {
32464
+ reject(error51);
32465
+ } else {
32466
+ resolve();
32467
+ }
32468
+ });
32469
+ });
32470
+ }
32471
+ };
32472
+ }
32473
+
32474
+ // src/client/oauth-provider.ts
32475
+ var InMemoryOAuthStorage = class {
32476
+ data = /* @__PURE__ */ new Map();
32477
+ set(key, value) {
32478
+ this.data.set(key, value);
32479
+ }
32480
+ get(key) {
32481
+ return this.data.get(key);
32482
+ }
32483
+ delete(key) {
32484
+ this.data.delete(key);
32485
+ }
32486
+ clear() {
32487
+ this.data.clear();
32488
+ }
32489
+ };
32490
+ var MCPOAuthClientProvider = class {
32491
+ _redirectUrl;
32492
+ _clientMetadata;
32493
+ storage;
32494
+ onRedirect;
32495
+ generateState;
32496
+ _clientInfo;
32497
+ _sessionState;
32498
+ _sessionRedirectUrl;
32499
+ constructor(options) {
32500
+ this._redirectUrl = options.redirectUrl;
32501
+ this._clientMetadata = options.clientMetadata;
32502
+ this._clientInfo = options.clientInformation;
32503
+ this.storage = options.storage ?? new InMemoryOAuthStorage();
32504
+ this.onRedirect = options.onRedirectToAuthorization;
32505
+ this.generateState = options.stateGenerator ?? (() => crypto.randomUUID());
32506
+ }
32507
+ /**
32508
+ * The URL to redirect the user agent to after authorization.
32509
+ */
32510
+ get redirectUrl() {
32511
+ return this._redirectUrl;
32512
+ }
32513
+ /**
32514
+ * Metadata about this OAuth client.
32515
+ */
32516
+ get clientMetadata() {
32517
+ return this._clientMetadata;
32518
+ }
32519
+ /**
32520
+ * Returns a OAuth2 state parameter.
32521
+ *
32522
+ * While an authorization session is active (see beginAuthorizationSession),
32523
+ * the pinned session state is returned so a callback server can validate
32524
+ * the redirect against a known value.
32525
+ */
32526
+ async state() {
32527
+ return this._sessionState ?? this.generateState();
32528
+ }
32529
+ /**
32530
+ * Pins the OAuth state parameter for the next authorization request.
32531
+ *
32532
+ * Hosts driving an interactive authorization flow (e.g. MCPClient.authenticate)
32533
+ * call this before triggering the flow so the loopback callback server knows
32534
+ * which state value to expect. Call endAuthorizationSession once the flow settles.
32535
+ *
32536
+ * @returns The pinned state value, generated with the configured stateGenerator
32537
+ */
32538
+ async beginAuthorizationSession() {
32539
+ this._sessionState = await this.generateState();
32540
+ this._sessionRedirectUrl = this._redirectUrl;
32541
+ return this._sessionState;
32542
+ }
32543
+ /**
32544
+ * Clears the pinned authorization state (see beginAuthorizationSession) and
32545
+ * restores the configured redirect URL if applyResolvedRedirectUrl rebased
32546
+ * it to a fallback port during the session, so the next flow starts from
32547
+ * the preferred port again.
32548
+ */
32549
+ endAuthorizationSession() {
32550
+ this._sessionState = void 0;
32551
+ if (this._sessionRedirectUrl !== void 0) {
32552
+ this._redirectUrl = this._sessionRedirectUrl;
32553
+ this._sessionRedirectUrl = void 0;
32554
+ }
32555
+ }
32556
+ /**
32557
+ * Points the provider at the callback URL that is actually bound.
32558
+ *
32559
+ * Loopback callback servers may bind a fallback port when the preferred one
32560
+ * is in use. Call this before triggering authorization so the authorization
32561
+ * request's redirect_uri matches the listening server, and so dynamic client
32562
+ * registration registers every candidate callback URL (see
32563
+ * getCallbackUrlCandidates) rather than only the preferred one.
32564
+ *
32565
+ * @param redirectUrl - The callback URL that is actually bound
32566
+ * @param registeredRedirectUris - The redirect URIs to register during dynamic client registration
32567
+ */
32568
+ applyResolvedRedirectUrl(redirectUrl, registeredRedirectUris) {
32569
+ this._redirectUrl = redirectUrl;
32570
+ this._clientMetadata = {
32571
+ ...this._clientMetadata,
32572
+ redirect_uris: registeredRedirectUris.map((uri) => uri.toString())
32573
+ };
32574
+ }
32575
+ /**
32576
+ * Loads information about this OAuth client.
32577
+ */
32578
+ async clientInformation() {
32579
+ if (this._clientInfo) {
32580
+ return this._clientInfo;
32581
+ }
32582
+ const stored = await this.storage.get("client_info");
32583
+ if (stored) {
32584
+ try {
32585
+ return JSON.parse(stored);
32586
+ } catch {
32587
+ }
32588
+ }
32589
+ return void 0;
32590
+ }
32591
+ /**
32592
+ * Saves dynamically registered client information.
32593
+ */
32594
+ async saveClientInformation(clientInformation) {
32595
+ this._clientInfo = clientInformation;
32596
+ await this.storage.set("client_info", JSON.stringify(clientInformation));
32597
+ }
32598
+ /**
32599
+ * Loads existing OAuth tokens.
32600
+ */
32601
+ async tokens() {
32602
+ const stored = await this.storage.get("tokens");
32603
+ if (stored) {
32604
+ try {
32605
+ return JSON.parse(stored);
32606
+ } catch {
32607
+ }
32608
+ }
32609
+ return void 0;
32610
+ }
32611
+ /**
32612
+ * Stores new OAuth tokens after successful authorization.
32613
+ */
32614
+ async saveTokens(tokens) {
32615
+ await this.storage.set("tokens", JSON.stringify(tokens));
32616
+ }
32617
+ /**
32618
+ * Invoked to redirect the user agent to the authorization URL.
32619
+ */
32620
+ async redirectToAuthorization(authorizationUrl) {
32621
+ if (this.onRedirect) {
32622
+ await this.onRedirect(authorizationUrl);
32623
+ } else {
32624
+ console.info(`Authorization required. Please visit: ${authorizationUrl.toString()}`);
32625
+ }
32626
+ }
32627
+ /**
32628
+ * Saves a PKCE code verifier before redirecting to authorization.
32629
+ */
32630
+ async saveCodeVerifier(codeVerifier) {
32631
+ await this.storage.set("code_verifier", codeVerifier);
32632
+ }
32633
+ /**
32634
+ * Loads the PKCE code verifier for validating authorization result.
32635
+ */
32636
+ async codeVerifier() {
32637
+ const verifier = await this.storage.get("code_verifier");
32638
+ if (!verifier) {
32639
+ throw new Error("No code verifier found. Authorization flow may not have started properly.");
32640
+ }
32641
+ return verifier;
32642
+ }
32643
+ /**
32644
+ * Invalidate credentials when server indicates they're no longer valid.
32645
+ */
32646
+ async invalidateCredentials(scope) {
32647
+ switch (scope) {
32648
+ case "all":
32649
+ await this.storage.delete("tokens");
32650
+ await this.storage.delete("client_info");
32651
+ await this.storage.delete("code_verifier");
32652
+ this._clientInfo = void 0;
32653
+ break;
32654
+ case "client":
32655
+ await this.storage.delete("client_info");
32656
+ this._clientInfo = void 0;
32657
+ break;
32658
+ case "tokens":
32659
+ await this.storage.delete("tokens");
32660
+ break;
32661
+ case "verifier":
32662
+ await this.storage.delete("code_verifier");
32663
+ break;
32664
+ }
32665
+ }
32666
+ /**
32667
+ * Clear all stored OAuth data.
32668
+ * Useful for logging out or resetting state.
32669
+ */
32670
+ async clear() {
32671
+ await this.invalidateCredentials("all");
32672
+ }
32673
+ /**
32674
+ * Check if the provider has valid (non-expired) tokens.
32675
+ */
32676
+ async hasValidTokens() {
32677
+ const currentTokens = await this.tokens();
32678
+ if (!currentTokens) return false;
32679
+ if (!currentTokens.access_token) return false;
32680
+ return true;
32681
+ }
32682
+ };
32683
+ function createSimpleTokenProvider(accessToken, options) {
32684
+ const tokens = {
32685
+ access_token: accessToken,
32686
+ token_type: options.tokenType ?? "Bearer",
32687
+ refresh_token: options.refreshToken,
32688
+ expires_in: options.expiresIn,
32689
+ scope: options.scope
32690
+ };
32691
+ const storage = new InMemoryOAuthStorage();
32692
+ storage.set("tokens", JSON.stringify(tokens));
32693
+ if (options.clientInformation) {
32694
+ storage.set("client_info", JSON.stringify(options.clientInformation));
32695
+ }
32696
+ return new MCPOAuthClientProvider({
32697
+ redirectUrl: options.redirectUrl,
32698
+ clientMetadata: options.clientMetadata,
32699
+ clientInformation: options.clientInformation,
32700
+ storage
32701
+ });
32702
+ }
32201
32703
  var MCPClientServerProxy = class extends mcp.MCPServerBase {
32202
32704
  clientGetter;
32203
32705
  cachedClient = null;
@@ -32337,12 +32839,25 @@ var MCPClientServerProxy = class extends mcp.MCPServerBase {
32337
32839
  // src/client/configuration.ts
32338
32840
  var mcpClientInstances = /* @__PURE__ */ new Map();
32339
32841
  var TOOL_DISCOVERY_MAX_ATTEMPTS = 2;
32842
+ var LOOPBACK_IPV4 = /^127\.(?:\d{1,3})\.(?:\d{1,3})\.(?:\d{1,3})$/;
32843
+ function isLoopbackHostname(hostname3) {
32844
+ return hostname3 === "localhost" || hostname3 === "[::1]" || hostname3 === "::1" || LOOPBACK_IPV4.test(hostname3);
32845
+ }
32340
32846
  var MCPClient = class extends base.MastraBase {
32341
32847
  serverConfigs = {};
32342
32848
  id;
32343
32849
  defaultTimeout;
32344
32850
  mcpClientsById = /* @__PURE__ */ new Map();
32345
32851
  disconnectPromise = null;
32852
+ authFlowsByServer = /* @__PURE__ */ new Map();
32853
+ authCallbackServersByServer = /* @__PURE__ */ new Map();
32854
+ /**
32855
+ * Per-server abort controllers for in-flight authorization flows. Created
32856
+ * synchronously at the start of {@link runAuthorizationFlow} — before the
32857
+ * callback server exists — so cancel/disconnect can interrupt the setup phase
32858
+ * (discovery, registration, port binding) and not just the waitForCode wait.
32859
+ */
32860
+ authAbortControllersByServer = /* @__PURE__ */ new Map();
32346
32861
  /**
32347
32862
  * Creates a new MCPClient instance for managing MCP server connections.
32348
32863
  *
@@ -33002,6 +33517,17 @@ To fix this you have three different options:
33002
33517
  this.disconnectPromise = (async () => {
33003
33518
  try {
33004
33519
  mcpClientInstances.delete(this.id);
33520
+ const pendingFlows = Array.from(this.authFlowsByServer.values());
33521
+ for (const controller of this.authAbortControllersByServer.values()) {
33522
+ controller.abort();
33523
+ }
33524
+ await Promise.allSettled(
33525
+ Array.from(this.authCallbackServersByServer.values()).map((server) => server.close())
33526
+ );
33527
+ await Promise.allSettled(pendingFlows);
33528
+ this.authAbortControllersByServer.clear();
33529
+ this.authCallbackServersByServer.clear();
33530
+ this.authFlowsByServer.clear();
33005
33531
  await Promise.allSettled(Array.from(this.mcpClientsById.values()).map((client) => client.disconnect()));
33006
33532
  this.mcpClientsById.clear();
33007
33533
  } finally {
@@ -33033,6 +33559,151 @@ To fix this you have three different options:
33033
33559
  await this.getConnectedClientForServer(serverName);
33034
33560
  }
33035
33561
  }
33562
+ /**
33563
+ * Runs the interactive OAuth authorization-code flow for a server.
33564
+ *
33565
+ * Requires the server to be configured with an MCPOAuthClientProvider whose
33566
+ * redirect URL points at a loopback address. The flow:
33567
+ *
33568
+ * 1. Starts a loopback callback server on the redirect URL's port (falling
33569
+ * back to the next sequential ports when it is in use)
33570
+ * 2. Attempts a connection so the SDK runs discovery and dynamic client
33571
+ * registration, delivering the authorization URL through the provider's
33572
+ * `onRedirectToAuthorization` callback — the host directs the user there
33573
+ * 3. Waits for the browser to deliver the authorization code, validates the
33574
+ * OAuth state, exchanges the code for tokens, and reconnects
33575
+ *
33576
+ * Concurrent calls for the same server join the pending flow (the joiner's
33577
+ * options are ignored — the pending flow keeps its own timeout); different
33578
+ * servers authenticate independently. Each server needs its own provider
33579
+ * instance: the flow pins session state on the provider, so sharing one
33580
+ * MCPOAuthClientProvider across servers is not supported. Hosts with custom
33581
+ * redirect handling (e.g. a web app with an HTTPS redirect URL) should
33582
+ * drive MCPOAuthClientProvider directly instead.
33583
+ *
33584
+ * @param serverName - The name of the server to authenticate (must match a key in `servers`)
33585
+ * @param options.timeoutMs - How long to wait for the browser callback (default 5 minutes)
33586
+ * @throws {Error} If the server has no MCPOAuthClientProvider or its redirect URL is not loopback
33587
+ *
33588
+ * @example
33589
+ * ```typescript
33590
+ * if (mcp.getServerAuthState('weatherServer') === 'needs-auth') {
33591
+ * await mcp.authenticate('weatherServer');
33592
+ * }
33593
+ * ```
33594
+ */
33595
+ async authenticate(serverName, options) {
33596
+ if (this.disconnectPromise) {
33597
+ await this.disconnectPromise.catch(() => {
33598
+ });
33599
+ }
33600
+ const pendingFlow = this.authFlowsByServer.get(serverName);
33601
+ if (pendingFlow) {
33602
+ return pendingFlow;
33603
+ }
33604
+ const flow = this.runAuthorizationFlow(serverName, options).finally(() => {
33605
+ this.authFlowsByServer.delete(serverName);
33606
+ });
33607
+ this.authFlowsByServer.set(serverName, flow);
33608
+ return flow;
33609
+ }
33610
+ /**
33611
+ * OAuth authorization state of a configured server.
33612
+ *
33613
+ * Returns `undefined` for servers without an authProvider and for servers
33614
+ * that have not attempted a connection yet.
33615
+ */
33616
+ getServerAuthState(serverName) {
33617
+ return this.mcpClientsById.get(serverName)?.authState;
33618
+ }
33619
+ async runAuthorizationFlow(serverName, options) {
33620
+ const abortController = new AbortController();
33621
+ this.authAbortControllersByServer.set(serverName, abortController);
33622
+ const throwIfAborted = () => {
33623
+ if (abortController.signal.aborted) {
33624
+ throw new Error(`Authentication for MCP server ${serverName} was cancelled.`);
33625
+ }
33626
+ };
33627
+ let provider;
33628
+ let sessionStarted = false;
33629
+ let callbackServer;
33630
+ try {
33631
+ const config2 = this.getServerConfig(serverName);
33632
+ const candidateProvider = config2.authProvider;
33633
+ if (!(candidateProvider instanceof MCPOAuthClientProvider)) {
33634
+ throw new Error(
33635
+ `Cannot authenticate MCP server ${serverName}: it is not configured with an MCPOAuthClientProvider.`
33636
+ );
33637
+ }
33638
+ provider = candidateProvider;
33639
+ const redirectUrl = new URL(provider.redirectUrl.toString());
33640
+ if (redirectUrl.protocol !== "http:" || !isLoopbackHostname(redirectUrl.hostname)) {
33641
+ throw new Error(
33642
+ `Cannot authenticate MCP server ${serverName}: the provider's redirect URL must be a loopback address, got ${redirectUrl.origin}.`
33643
+ );
33644
+ }
33645
+ const state = await provider.beginAuthorizationSession();
33646
+ sessionStarted = true;
33647
+ throwIfAborted();
33648
+ callbackServer = await createOAuthCallbackServer({ redirectUrl, state });
33649
+ throwIfAborted();
33650
+ this.authCallbackServersByServer.set(serverName, callbackServer);
33651
+ provider.applyResolvedRedirectUrl(callbackServer.url, getCallbackUrlCandidates(redirectUrl));
33652
+ const clientInfo = await provider.clientInformation();
33653
+ if (clientInfo?.redirect_uris && !clientInfo.redirect_uris.includes(callbackServer.url.toString())) {
33654
+ await provider.invalidateCredentials("client");
33655
+ }
33656
+ const client = await this.getClientForServer(serverName);
33657
+ try {
33658
+ await client.connect();
33659
+ return;
33660
+ } catch (error51) {
33661
+ if (!(error51 instanceof auth_js.UnauthorizedError)) {
33662
+ throw this.handleConnectError(serverName, error51);
33663
+ }
33664
+ }
33665
+ const { code } = await callbackServer.waitForCode(options);
33666
+ await client.finishAuth(code);
33667
+ try {
33668
+ await client.connect();
33669
+ } catch (error51) {
33670
+ throw error51 instanceof auth_js.UnauthorizedError ? error51 : this.handleConnectError(serverName, error51);
33671
+ }
33672
+ } finally {
33673
+ this.authCallbackServersByServer.delete(serverName);
33674
+ this.authAbortControllersByServer.delete(serverName);
33675
+ if (sessionStarted) {
33676
+ provider?.endAuthorizationSession();
33677
+ }
33678
+ await callbackServer?.close();
33679
+ }
33680
+ }
33681
+ /**
33682
+ * Cancels a pending {@link authenticate} flow for a server.
33683
+ *
33684
+ * Tears down the loopback callback server immediately — the pending
33685
+ * authenticate() call rejects. Useful when the user closed the browser
33686
+ * without completing consent, which the host cannot observe.
33687
+ *
33688
+ * The resulting auth state depends on how far the flow had progressed: a flow
33689
+ * cancelled after the server rejected the connection with a 401 stays in the
33690
+ * `needs-auth` state so it can be retried right away, while a flow cancelled
33691
+ * during the setup phase (before any connection was attempted) leaves the
33692
+ * state unchanged — typically `undefined`.
33693
+ *
33694
+ * @param serverName - The name of the server whose flow to cancel
33695
+ * @returns `true` if a pending flow was cancelled, `false` when no flow was pending
33696
+ */
33697
+ async cancelAuthentication(serverName) {
33698
+ const pendingFlow = this.authFlowsByServer.get(serverName);
33699
+ if (!pendingFlow) {
33700
+ return false;
33701
+ }
33702
+ this.authAbortControllersByServer.get(serverName)?.abort();
33703
+ await this.authCallbackServersByServer.get(serverName)?.close();
33704
+ await pendingFlow.catch(() => void 0);
33705
+ return true;
33706
+ }
33036
33707
  /**
33037
33708
  * Returns instructions advertised by connected MCP servers during initialize.
33038
33709
  *
@@ -33318,7 +33989,9 @@ To fix this you have three different options:
33318
33989
  );
33319
33990
  this.logger.trackException(mastraError);
33320
33991
  this.logger.error("MCPClient errored connecting to MCP server:", { error: mastraError.toString() });
33321
- this.mcpClientsById.delete(name);
33992
+ if (!(error51 instanceof auth_js.UnauthorizedError)) {
33993
+ this.mcpClientsById.delete(name);
33994
+ }
33322
33995
  return mastraError;
33323
33996
  }
33324
33997
  async getConnectedClientForServer(serverName) {
@@ -33349,184 +34022,6 @@ To fix this you have three different options:
33349
34022
  }
33350
34023
  };
33351
34024
 
33352
- // src/client/oauth-provider.ts
33353
- var InMemoryOAuthStorage = class {
33354
- data = /* @__PURE__ */ new Map();
33355
- set(key, value) {
33356
- this.data.set(key, value);
33357
- }
33358
- get(key) {
33359
- return this.data.get(key);
33360
- }
33361
- delete(key) {
33362
- this.data.delete(key);
33363
- }
33364
- clear() {
33365
- this.data.clear();
33366
- }
33367
- };
33368
- var MCPOAuthClientProvider = class {
33369
- _redirectUrl;
33370
- _clientMetadata;
33371
- storage;
33372
- onRedirect;
33373
- generateState;
33374
- _clientInfo;
33375
- constructor(options) {
33376
- this._redirectUrl = options.redirectUrl;
33377
- this._clientMetadata = options.clientMetadata;
33378
- this._clientInfo = options.clientInformation;
33379
- this.storage = options.storage ?? new InMemoryOAuthStorage();
33380
- this.onRedirect = options.onRedirectToAuthorization;
33381
- this.generateState = options.stateGenerator ?? (() => crypto.randomUUID());
33382
- }
33383
- /**
33384
- * The URL to redirect the user agent to after authorization.
33385
- */
33386
- get redirectUrl() {
33387
- return this._redirectUrl;
33388
- }
33389
- /**
33390
- * Metadata about this OAuth client.
33391
- */
33392
- get clientMetadata() {
33393
- return this._clientMetadata;
33394
- }
33395
- /**
33396
- * Returns a OAuth2 state parameter.
33397
- */
33398
- async state() {
33399
- return this.generateState();
33400
- }
33401
- /**
33402
- * Loads information about this OAuth client.
33403
- */
33404
- async clientInformation() {
33405
- if (this._clientInfo) {
33406
- return this._clientInfo;
33407
- }
33408
- const stored = await this.storage.get("client_info");
33409
- if (stored) {
33410
- try {
33411
- return JSON.parse(stored);
33412
- } catch {
33413
- }
33414
- }
33415
- return void 0;
33416
- }
33417
- /**
33418
- * Saves dynamically registered client information.
33419
- */
33420
- async saveClientInformation(clientInformation) {
33421
- this._clientInfo = clientInformation;
33422
- await this.storage.set("client_info", JSON.stringify(clientInformation));
33423
- }
33424
- /**
33425
- * Loads existing OAuth tokens.
33426
- */
33427
- async tokens() {
33428
- const stored = await this.storage.get("tokens");
33429
- if (stored) {
33430
- try {
33431
- return JSON.parse(stored);
33432
- } catch {
33433
- }
33434
- }
33435
- return void 0;
33436
- }
33437
- /**
33438
- * Stores new OAuth tokens after successful authorization.
33439
- */
33440
- async saveTokens(tokens) {
33441
- await this.storage.set("tokens", JSON.stringify(tokens));
33442
- }
33443
- /**
33444
- * Invoked to redirect the user agent to the authorization URL.
33445
- */
33446
- async redirectToAuthorization(authorizationUrl) {
33447
- if (this.onRedirect) {
33448
- await this.onRedirect(authorizationUrl);
33449
- } else {
33450
- console.info(`Authorization required. Please visit: ${authorizationUrl.toString()}`);
33451
- }
33452
- }
33453
- /**
33454
- * Saves a PKCE code verifier before redirecting to authorization.
33455
- */
33456
- async saveCodeVerifier(codeVerifier) {
33457
- await this.storage.set("code_verifier", codeVerifier);
33458
- }
33459
- /**
33460
- * Loads the PKCE code verifier for validating authorization result.
33461
- */
33462
- async codeVerifier() {
33463
- const verifier = await this.storage.get("code_verifier");
33464
- if (!verifier) {
33465
- throw new Error("No code verifier found. Authorization flow may not have started properly.");
33466
- }
33467
- return verifier;
33468
- }
33469
- /**
33470
- * Invalidate credentials when server indicates they're no longer valid.
33471
- */
33472
- async invalidateCredentials(scope) {
33473
- switch (scope) {
33474
- case "all":
33475
- await this.storage.delete("tokens");
33476
- await this.storage.delete("client_info");
33477
- await this.storage.delete("code_verifier");
33478
- this._clientInfo = void 0;
33479
- break;
33480
- case "client":
33481
- await this.storage.delete("client_info");
33482
- this._clientInfo = void 0;
33483
- break;
33484
- case "tokens":
33485
- await this.storage.delete("tokens");
33486
- break;
33487
- case "verifier":
33488
- await this.storage.delete("code_verifier");
33489
- break;
33490
- }
33491
- }
33492
- /**
33493
- * Clear all stored OAuth data.
33494
- * Useful for logging out or resetting state.
33495
- */
33496
- async clear() {
33497
- await this.invalidateCredentials("all");
33498
- }
33499
- /**
33500
- * Check if the provider has valid (non-expired) tokens.
33501
- */
33502
- async hasValidTokens() {
33503
- const currentTokens = await this.tokens();
33504
- if (!currentTokens) return false;
33505
- if (!currentTokens.access_token) return false;
33506
- return true;
33507
- }
33508
- };
33509
- function createSimpleTokenProvider(accessToken, options) {
33510
- const tokens = {
33511
- access_token: accessToken,
33512
- token_type: options.tokenType ?? "Bearer",
33513
- refresh_token: options.refreshToken,
33514
- expires_in: options.expiresIn,
33515
- scope: options.scope
33516
- };
33517
- const storage = new InMemoryOAuthStorage();
33518
- storage.set("tokens", JSON.stringify(tokens));
33519
- if (options.clientInformation) {
33520
- storage.set("client_info", JSON.stringify(options.clientInformation));
33521
- }
33522
- return new MCPOAuthClientProvider({
33523
- redirectUrl: options.redirectUrl,
33524
- clientMetadata: options.clientMetadata,
33525
- clientInformation: options.clientInformation,
33526
- storage
33527
- });
33528
- }
33529
-
33530
34025
  // ../../../../../setup-pnpm/node_modules/.bin/store/v11/links/@/hono/4.12.30/1b2e4fcdfb5fc9f7f51bffc1540f859608c39e01fb7d73d8ab30d60a680eaffc/node_modules/hono/dist/utils/stream.js
33531
34026
  var StreamingApi = class {
33532
34027
  writer;
@@ -36215,44 +36710,6 @@ Provided arguments: ${JSON.stringify(args, null, 2)}`,
36215
36710
  return { resources };
36216
36711
  }
36217
36712
  };
36218
- function escapeHeaderValue(value) {
36219
- return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
36220
- }
36221
- function generateWWWAuthenticateHeader(options = {}) {
36222
- const params = [];
36223
- if (options.resourceMetadataUrl) {
36224
- params.push(`resource_metadata="${escapeHeaderValue(options.resourceMetadataUrl)}"`);
36225
- }
36226
- if (options.additionalParams) {
36227
- for (const [key, value] of Object.entries(options.additionalParams)) {
36228
- params.push(`${key}="${escapeHeaderValue(value)}"`);
36229
- }
36230
- }
36231
- if (params.length === 0) {
36232
- return "Bearer";
36233
- }
36234
- return `Bearer ${params.join(", ")}`;
36235
- }
36236
- function generateProtectedResourceMetadata(config2) {
36237
- return {
36238
- resource: config2.resource,
36239
- authorization_servers: config2.authorizationServers,
36240
- scopes_supported: config2.scopesSupported ?? ["mcp:read", "mcp:write"],
36241
- bearer_methods_supported: ["header"],
36242
- ...config2.resourceName && { resource_name: config2.resourceName },
36243
- ...config2.resourceDocumentation && {
36244
- resource_documentation: config2.resourceDocumentation
36245
- }
36246
- };
36247
- }
36248
- function extractBearerToken(authHeader) {
36249
- if (!authHeader) return void 0;
36250
- const prefix = "bearer ";
36251
- if (authHeader.length <= prefix.length) return void 0;
36252
- if (authHeader.slice(0, prefix.length).toLowerCase() !== prefix) return void 0;
36253
- const token = authHeader.slice(prefix.length).trim();
36254
- return token || void 0;
36255
- }
36256
36713
 
36257
36714
  // src/server/oauth-middleware.ts
36258
36715
  function createOAuthMiddleware(options) {
@@ -36478,11 +36935,13 @@ exports.MCPClientServerProxy = MCPClientServerProxy;
36478
36935
  exports.MCPOAuthClientProvider = MCPOAuthClientProvider;
36479
36936
  exports.MCPServer = MCPServer;
36480
36937
  exports.createIntrospectionValidator = createIntrospectionValidator;
36938
+ exports.createOAuthCallbackServer = createOAuthCallbackServer;
36481
36939
  exports.createOAuthMiddleware = createOAuthMiddleware;
36482
36940
  exports.createSimpleTokenProvider = createSimpleTokenProvider;
36483
36941
  exports.createStaticTokenValidator = createStaticTokenValidator;
36484
36942
  exports.extractBearerToken = extractBearerToken;
36485
36943
  exports.generateProtectedResourceMetadata = generateProtectedResourceMetadata;
36486
36944
  exports.generateWWWAuthenticateHeader = generateWWWAuthenticateHeader;
36945
+ exports.getCallbackUrlCandidates = getCallbackUrlCandidates;
36487
36946
  //# sourceMappingURL=index.cjs.map
36488
36947
  //# sourceMappingURL=index.cjs.map