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