@mastra/mcp 2.7.0 → 2.8.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
@@ -16099,7 +16099,7 @@ var InternalMastraMCPClient = class extends MastraBase {
16099
16099
  ...configured
16100
16100
  };
16101
16101
  }
16102
- async connectStdio(command) {
16102
+ async connectStdio(command, generation) {
16103
16103
  this.log("debug", `Using Stdio transport for command: ${command}`);
16104
16104
  const transport = new StdioClientTransport({
16105
16105
  command,
@@ -16109,7 +16109,9 @@ var InternalMastraMCPClient = class extends MastraBase {
16109
16109
  cwd: this.serverConfig.cwd
16110
16110
  });
16111
16111
  observeEventStreamRequests(transport, this.noteEventStreamSent);
16112
+ this.connectingTransport = transport;
16112
16113
  try {
16114
+ this.assertSameLifecycle(generation);
16113
16115
  await this.client.connect(transport, {
16114
16116
  timeout: this.serverConfig.timeout ?? this.timeout,
16115
16117
  prior: this.priorDiscovery
@@ -16121,9 +16123,11 @@ var InternalMastraMCPClient = class extends MastraBase {
16121
16123
  this.log("error", e instanceof Error ? e.stack || e.message : JSON.stringify(e));
16122
16124
  await transport.close().catch(() => {});
16123
16125
  throw e;
16126
+ } finally {
16127
+ if (this.connectingTransport === transport) this.connectingTransport = void 0;
16124
16128
  }
16125
16129
  }
16126
- async connectHttp(url) {
16130
+ async connectHttp(url, generation) {
16127
16131
  const { requestInit, authProvider, connectTimeout, fetch: userFetch, allowedHosts } = this.serverConfig;
16128
16132
  if (allowedHosts !== void 0) assertHostAllowed(url, allowedHosts);
16129
16133
  const policyUserFetch = userFetch && allowedHosts !== void 0 ? wrapFetchWithHostPolicy(userFetch, allowedHosts) : void 0;
@@ -16143,7 +16147,9 @@ var InternalMastraMCPClient = class extends MastraBase {
16143
16147
  fetch
16144
16148
  });
16145
16149
  observeEventStreamRequests(transport, this.noteEventStreamSent);
16150
+ this.connectingTransport = transport;
16146
16151
  try {
16152
+ this.assertSameLifecycle(generation);
16147
16153
  await this.client.connect(transport, {
16148
16154
  timeout: connectTimeout ?? DEFAULT_SERVER_CONNECT_TIMEOUT_MSEC,
16149
16155
  prior: this.priorDiscovery
@@ -16157,6 +16163,8 @@ var InternalMastraMCPClient = class extends MastraBase {
16157
16163
  this.log("debug", `Streamable HTTP transport failed: ${error}`);
16158
16164
  if (authProvider && error instanceof UnauthorizedError) this.markNeedsAuth(transport);
16159
16165
  throw error;
16166
+ } finally {
16167
+ if (this.connectingTransport === transport) this.connectingTransport = void 0;
16160
16168
  }
16161
16169
  }
16162
16170
  /**
@@ -16246,6 +16254,34 @@ var InternalMastraMCPClient = class extends MastraBase {
16246
16254
  isConnected = null;
16247
16255
  reconnectPromise = null;
16248
16256
  /**
16257
+ * Transport of the connect attempt still in flight. The SDK only attaches it to
16258
+ * the client once version negotiation finishes, so a server that never answers
16259
+ * would otherwise leave nothing that disconnect() or a newer attempt can close.
16260
+ */
16261
+ connectingTransport;
16262
+ /**
16263
+ * Closes the transport of an unfinished connect attempt. Closing makes the SDK
16264
+ * reject that attempt and reap the child (and any negotiation probe) it spawned.
16265
+ */
16266
+ async abandonPendingConnect() {
16267
+ const pending = this.connectingTransport;
16268
+ if (!pending) return;
16269
+ this.connectingTransport = void 0;
16270
+ await pending.close().catch(() => {});
16271
+ }
16272
+ /**
16273
+ * Whether a connect or reconnect is still running on this client. A failed attempt
16274
+ * that a newer one replaced reports true here, since the newer attempt owns the client.
16275
+ *
16276
+ * @internal
16277
+ */
16278
+ hasPendingConnect() {
16279
+ return this.isConnected !== null || this.reconnectPromise !== null;
16280
+ }
16281
+ assertSameLifecycle(generation) {
16282
+ if (this.lifecycleGeneration !== generation) throw new Error("MCP client was disconnected while connecting");
16283
+ }
16284
+ /**
16249
16285
  * Change notifications the caller has asked for. The client keeps exactly one
16250
16286
  * `subscriptions/listen` stream open for this interest set, replaces it when the
16251
16287
  * set changes and reopens it after a reconnect.
@@ -16277,14 +16313,15 @@ var InternalMastraMCPClient = class extends MastraBase {
16277
16313
  *
16278
16314
  * @internal
16279
16315
  */
16280
- async connect() {
16316
+ async connect(generation = this.lifecycleGeneration) {
16281
16317
  if (this.isConnected) return this.isConnected;
16282
- this.isConnected = new Promise(async (resolve, reject) => {
16318
+ const attempt = new Promise(async (resolve, reject) => {
16283
16319
  try {
16320
+ await this.abandonPendingConnect();
16284
16321
  await this.detachStaleClientTransport();
16285
16322
  const { command, url } = this.serverConfig;
16286
- if (command) await this.connectStdio(command);
16287
- else if (url) await this.connectHttp(url);
16323
+ if (command) await this.connectStdio(command, generation);
16324
+ else if (url) await this.connectHttp(url, generation);
16288
16325
  else throw new Error("Server configuration must include either a command or a url.");
16289
16326
  this.serverInstructions = this.client.getInstructions();
16290
16327
  this.serverImplementation = this.client.getServerVersion();
@@ -16319,12 +16356,15 @@ var InternalMastraMCPClient = class extends MastraBase {
16319
16356
  this.clientConnectionOnClose = connectionOnClose;
16320
16357
  this.client.onclose = connectionOnClose;
16321
16358
  } catch (e) {
16322
- if (!this.transport) this.unregisterProcessHooks();
16323
- this.isConnected = null;
16359
+ if (this.isConnected === attempt) {
16360
+ if (!this.transport) this.unregisterProcessHooks();
16361
+ this.isConnected = null;
16362
+ }
16324
16363
  this.priorDiscovery = void 0;
16325
16364
  reject(e);
16326
16365
  }
16327
16366
  });
16367
+ this.isConnected = attempt;
16328
16368
  if (this.processHooks) this.registerProcessHooks();
16329
16369
  return this.isConnected;
16330
16370
  }
@@ -16407,6 +16447,7 @@ var InternalMastraMCPClient = class extends MastraBase {
16407
16447
  }
16408
16448
  async disconnect() {
16409
16449
  this.lifecycleGeneration++;
16450
+ await this.abandonPendingConnect();
16410
16451
  const reconnectPromise = this.reconnectPromise;
16411
16452
  if (reconnectPromise) await reconnectPromise.catch(() => {});
16412
16453
  this.closePendingAuthTransport();
@@ -16445,6 +16486,7 @@ var InternalMastraMCPClient = class extends MastraBase {
16445
16486
  this.log("debug", "Reconnection already in progress; waiting for it to complete");
16446
16487
  return await this.reconnectPromise;
16447
16488
  }
16489
+ const generation = this.lifecycleGeneration;
16448
16490
  const reconnectPromise = (async () => {
16449
16491
  this.log("debug", "Forcing reconnection to MCP server...");
16450
16492
  this.closePendingAuthTransport();
@@ -16462,7 +16504,8 @@ var InternalMastraMCPClient = class extends MastraBase {
16462
16504
  this.serverInstructions = void 0;
16463
16505
  this.serverImplementation = void 0;
16464
16506
  }
16465
- await this.connect();
16507
+ this.assertSameLifecycle(generation);
16508
+ await this.connect(generation);
16466
16509
  this.log("debug", "Successfully reconnected to MCP server");
16467
16510
  })();
16468
16511
  this.reconnectPromise = reconnectPromise;
@@ -17932,6 +17975,11 @@ function cachedClient(id) {
17932
17975
  return mcpClientInstances.get(id);
17933
17976
  }
17934
17977
  const TOOL_DISCOVERY_MAX_ATTEMPTS = 2;
17978
+ /**
17979
+ * `code` of the discovery error reported when a server exceeds `perServerTimeoutMs`.
17980
+ * The server's connect keeps running, so a later discovery can still succeed.
17981
+ */
17982
+ const MCP_DISCOVERY_TIMEOUT_CODE = "MCP_DISCOVERY_TIMEOUT";
17935
17983
  const LOOPBACK_IPV4 = /^127\.(?:\d{1,3})\.(?:\d{1,3})\.(?:\d{1,3})$/;
17936
17984
  function isLoopbackHostname(hostname) {
17937
17985
  return hostname === "localhost" || hostname === "[::1]" || hostname === "::1" || LOOPBACK_IPV4.test(hostname);
@@ -18711,14 +18759,14 @@ onListChanged: async (serverName, handler) => {
18711
18759
  await client.connect();
18712
18760
  return;
18713
18761
  } catch (error) {
18714
- if (!(error instanceof UnauthorizedError)) throw this.handleConnectError(serverName, error);
18762
+ if (!(error instanceof UnauthorizedError)) throw this.handleConnectError(serverName, error, client);
18715
18763
  }
18716
18764
  const { code, iss } = await callbackServer.waitForCode(options);
18717
18765
  await client.finishAuth(code, iss);
18718
18766
  try {
18719
18767
  await client.connect();
18720
18768
  } catch (error) {
18721
- throw error instanceof UnauthorizedError ? error : this.handleConnectError(serverName, error);
18769
+ throw error instanceof UnauthorizedError ? error : this.handleConnectError(serverName, error, client);
18722
18770
  }
18723
18771
  } finally {
18724
18772
  this.authCallbackServersByServer.delete(serverName);
@@ -19081,53 +19129,73 @@ onListChanged: async (serverName, handler) => {
19081
19129
  * `prompts.list`.
19082
19130
  */
19083
19131
  async discoverAcrossServers(operation, onError, options) {
19084
- const serverNames = Object.keys(this.serverConfigs);
19085
- return Promise.all(serverNames.map(async (serverName) => {
19086
- const startedAt = performance.now();
19087
- let timer;
19132
+ const only = options?.serverNames;
19133
+ const serverNames = Object.keys(this.serverConfigs).filter((name) => !only || only.includes(name));
19134
+ const onServerSettled = options?.onServerSettled;
19135
+ const notifySettled = (result) => {
19136
+ if (!onServerSettled) return result;
19137
+ const logCallbackError = (error) => {
19138
+ this.logger.error("MCP onServerSettled callback threw:", {
19139
+ serverName: result.serverName,
19140
+ error: error instanceof Error ? error.message : String(error)
19141
+ });
19142
+ };
19088
19143
  try {
19089
- const operationPromise = operation(serverName);
19090
- return {
19091
- serverName,
19092
- value: options?.perServerTimeoutMs === void 0 ? await operationPromise : await Promise.race([operationPromise, new Promise((_, reject) => {
19093
- timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`Discovery timed out after ${options.perServerTimeoutMs}ms`)), options.perServerTimeoutMs);
19094
- timer.unref?.();
19095
- })]),
19096
- error: void 0,
19097
- duration: performance.now() - startedAt
19098
- };
19144
+ const { serverName, ...settled } = result;
19145
+ const outcome = onServerSettled(serverName, settled);
19146
+ if (outcome && typeof outcome.then === "function") Promise.resolve(outcome).catch(logCallbackError);
19099
19147
  } catch (error) {
19100
- const discoveryError = getMCPDiscoveryErrorDetails(error);
19101
- try {
19102
- const mastraError = new MastraError({
19103
- id: onError.errorId,
19104
- domain: ErrorDomain.MCP,
19105
- category: ErrorCategory.THIRD_PARTY,
19106
- details: { serverName }
19107
- }, error);
19108
- this.logger.trackException(mastraError);
19109
- this.logger.error(onError.logMessage, { error: mastraError.toString() });
19110
- } catch {
19111
- const fallbackError = new MastraError({
19112
- id: onError.errorId,
19113
- domain: ErrorDomain.MCP,
19114
- category: ErrorCategory.THIRD_PARTY,
19115
- text: discoveryError.message,
19116
- details: { serverName }
19117
- });
19118
- this.logger.trackException(fallbackError);
19119
- this.logger.error(onError.logMessage, { error: fallbackError.toString() });
19120
- }
19121
- return {
19122
- serverName,
19123
- value: void 0,
19124
- error: discoveryError,
19125
- duration: performance.now() - startedAt
19126
- };
19127
- } finally {
19128
- clearTimeout(timer);
19148
+ logCallbackError(error);
19129
19149
  }
19130
- }));
19150
+ return result;
19151
+ };
19152
+ return Promise.all(serverNames.map(async (serverName) => notifySettled(await this.discoverServer(serverName, operation, onError, options?.perServerTimeoutMs))));
19153
+ }
19154
+ async discoverServer(serverName, operation, onError, perServerTimeoutMs) {
19155
+ const startedAt = performance.now();
19156
+ let timer;
19157
+ try {
19158
+ const operationPromise = operation(serverName);
19159
+ return {
19160
+ serverName,
19161
+ value: perServerTimeoutMs === void 0 ? await operationPromise : await Promise.race([operationPromise, new Promise((_, reject) => {
19162
+ timer = setTimeout(() => reject(Object.assign(/* @__PURE__ */ new Error(`Discovery timed out after ${perServerTimeoutMs}ms`), { code: MCP_DISCOVERY_TIMEOUT_CODE })), perServerTimeoutMs);
19163
+ timer.unref?.();
19164
+ })]),
19165
+ error: void 0,
19166
+ duration: performance.now() - startedAt
19167
+ };
19168
+ } catch (error) {
19169
+ const discoveryError = getMCPDiscoveryErrorDetails(error);
19170
+ try {
19171
+ const mastraError = new MastraError({
19172
+ id: onError.errorId,
19173
+ domain: ErrorDomain.MCP,
19174
+ category: ErrorCategory.THIRD_PARTY,
19175
+ details: { serverName }
19176
+ }, error);
19177
+ this.logger.trackException(mastraError);
19178
+ this.logger.error(onError.logMessage, { error: mastraError.toString() });
19179
+ } catch {
19180
+ const fallbackError = new MastraError({
19181
+ id: onError.errorId,
19182
+ domain: ErrorDomain.MCP,
19183
+ category: ErrorCategory.THIRD_PARTY,
19184
+ text: discoveryError.message,
19185
+ details: { serverName }
19186
+ });
19187
+ this.logger.trackException(fallbackError);
19188
+ this.logger.error(onError.logMessage, { error: fallbackError.toString() });
19189
+ }
19190
+ return {
19191
+ serverName,
19192
+ value: void 0,
19193
+ error: discoveryError,
19194
+ duration: performance.now() - startedAt
19195
+ };
19196
+ } finally {
19197
+ clearTimeout(timer);
19198
+ }
19131
19199
  }
19132
19200
  /**
19133
19201
  * Creates `MCPServerBase` proxy objects for each server connection in this
@@ -19212,12 +19280,12 @@ onListChanged: async (serverName, handler) => {
19212
19280
  try {
19213
19281
  await mcpClient.connect();
19214
19282
  } catch (e) {
19215
- throw this.handleConnectError(name, e);
19283
+ throw this.handleConnectError(name, e, mcpClient);
19216
19284
  }
19217
19285
  this.logger.debug("Connected to MCP server", { name });
19218
19286
  return mcpClient;
19219
19287
  }
19220
- handleConnectError(name, error) {
19288
+ handleConnectError(name, error, client) {
19221
19289
  const mastraError = new MastraError({
19222
19290
  id: "MCP_CLIENT_CONNECT_FAILED",
19223
19291
  domain: ErrorDomain.MCP,
@@ -19227,7 +19295,7 @@ onListChanged: async (serverName, handler) => {
19227
19295
  }, error);
19228
19296
  this.logger.trackException(mastraError);
19229
19297
  this.logger.error("MCPClient errored connecting to MCP server:", { error: mastraError.toString() });
19230
- if (!(error instanceof UnauthorizedError)) this.mcpClientsById.delete(name);
19298
+ if (!(error instanceof UnauthorizedError) && !client.hasPendingConnect() && this.mcpClientsById.get(name) === client) this.mcpClientsById.delete(name);
19231
19299
  return mastraError;
19232
19300
  }
19233
19301
  async getConnectedClientForServer(serverName) {
@@ -22638,7 +22706,8 @@ var MCPServer = class extends MCPServerBase {
22638
22706
  tracingContext: {},
22639
22707
  mastra: this.mastra,
22640
22708
  logger: this.logger,
22641
- description: "description" in tool ? tool.description : void 0
22709
+ description: "description" in tool ? tool.description : void 0,
22710
+ resolveExecuteFGA: ({ user, requestContext }) => this.toolFGACheck(name, user, requestContext)
22642
22711
  });
22643
22712
  converted[name] = {
22644
22713
  ...coreTool,
@@ -23951,37 +24020,49 @@ var MCPServer = class extends MCPServerBase {
23951
24020
  const fgaProvider = this.mastra?.getServer?.()?.fga;
23952
24021
  if (!fgaProvider) return;
23953
24022
  const { getMCPToolFGAResourceId, requireFGA, FGADeniedError, MastraFGAPermissions } = await import("@mastra/core/auth/ee");
23954
- const resourceId = getMCPToolFGAResourceId(this.id, toolId);
23955
24023
  const user = requestContext.get("user");
23956
24024
  if (!user) throw new FGADeniedError({ id: "unknown" }, {
23957
24025
  type: "tool",
23958
- id: resourceId
24026
+ id: getMCPToolFGAResourceId(this.id, toolId)
23959
24027
  }, MastraFGAPermissions.TOOLS_EXECUTE);
23960
- const permission = this.fga?.permissionMapping?.[MastraFGAPermissions.TOOLS_EXECUTE] ?? MastraFGAPermissions.TOOLS_EXECUTE;
23961
- const mapping = this.fga?.resourceMapping?.tool ?? this.fga?.resourceMapping?.tools;
23962
24028
  await requireFGA({
23963
24029
  fgaProvider,
23964
24030
  user,
24031
+ requestContext,
24032
+ ...await this.toolFGACheck(toolId, user, requestContext)
24033
+ });
24034
+ }
24035
+ /**
24036
+ * The check for one of this server's tools: its MCP tool resource, or the server's `fga` mapping.
24037
+ * The tools it builds run the same check, so they keep it when used outside the server, such as
24038
+ * through `tools()` on an agent.
24039
+ */
24040
+ async toolFGACheck(toolId, user, requestContext) {
24041
+ const { getMCPToolFGAResourceId, MastraFGAPermissions } = await import("@mastra/core/auth/ee");
24042
+ const resourceId = getMCPToolFGAResourceId(this.id, toolId);
24043
+ const permission = this.fga?.permissionMapping?.[MastraFGAPermissions.TOOLS_EXECUTE] ?? MastraFGAPermissions.TOOLS_EXECUTE;
24044
+ const mapping = this.fga?.resourceMapping?.tool ?? this.fga?.resourceMapping?.tools;
24045
+ const derivedId = mapping && user ? mapping.deriveId?.({
24046
+ user,
24047
+ resourceId,
24048
+ requestContext
24049
+ }) : void 0;
24050
+ return {
23965
24051
  resource: mapping ? {
23966
24052
  type: mapping.fgaResourceType,
23967
- id: mapping.deriveId?.({
23968
- user,
23969
- resourceId,
23970
- requestContext
23971
- }) ?? resourceId
24053
+ id: derivedId ?? resourceId
23972
24054
  } : {
23973
24055
  type: "tool",
23974
24056
  id: resourceId
23975
24057
  },
23976
24058
  permission,
23977
- requestContext,
23978
24059
  context: { resourceId },
23979
24060
  metadata: {
23980
24061
  mcpServerId: this.id,
23981
24062
  mcpServerName: this.name,
23982
24063
  toolId
23983
24064
  }
23984
- });
24065
+ };
23985
24066
  }
23986
24067
  async authorizedToolEntries(requestContext) {
23987
24068
  const entries = Object.entries(this.convertedTools);
@@ -25106,6 +25187,6 @@ var MCPEventsSignalProvider = class MCPEventsSignalProvider extends SignalProvid
25106
25187
  else console.warn(`[${this.id}] ${info.key}:`, error);
25107
25188
  }
25108
25189
  };
25109
- export { InMemoryMCPEventCursorStore, InMemoryMCPTaskStore, InMemoryOAuthStorage, InMemoryWebhookSubscriptionStore, InternalMastraMCPClient, MCPClient, MCPClientServerProxy, MCPEventsSignalProvider, MCPOAuthClientProvider, MCPServer, MCPTaskLimitError, MCP_CALL_TOOL_CONTENT, MCP_CALL_TOOL_META, MCP_CLIENT_PROTOCOL_VERSION, MCP_PROTOCOL_VERSION, ServerEventActions, ServerPromptActions, ServerResourceActions, ServerToolActions, UnauthorizedError, auth, buildDiscoveryUrls, createIntrospectionValidator, createOAuthCallbackServer, createOAuthMiddleware, createSimpleTokenProvider, createStaticTokenValidator, defineEvent, discoverAuthorizationServerMetadata, discoverOAuthMetadata, discoverOAuthProtectedResourceMetadata, exchangeAuthorization, extractBearerToken, extractResourceMetadataUrl, generateProtectedResourceMetadata, generateWWWAuthenticateHeader, getCallbackUrlCandidates, getMcpCallToolContent, getMcpCallToolMeta, isMCPTaskStoreAdmin, isWorkflowTaskContext, parseErrorResponse, principalKeyOf, refreshAuthorization, selectResourceURL, startAuthorization, taskKeyOf, traceContextFromMeta, traceContextToMeta };
25190
+ export { InMemoryMCPEventCursorStore, InMemoryMCPTaskStore, InMemoryOAuthStorage, InMemoryWebhookSubscriptionStore, InternalMastraMCPClient, MCPClient, MCPClientServerProxy, MCPEventsSignalProvider, MCPOAuthClientProvider, MCPServer, MCPTaskLimitError, MCP_CALL_TOOL_CONTENT, MCP_CALL_TOOL_META, MCP_CLIENT_PROTOCOL_VERSION, MCP_DISCOVERY_TIMEOUT_CODE, MCP_PROTOCOL_VERSION, ServerEventActions, ServerPromptActions, ServerResourceActions, ServerToolActions, UnauthorizedError, auth, buildDiscoveryUrls, createIntrospectionValidator, createOAuthCallbackServer, createOAuthMiddleware, createSimpleTokenProvider, createStaticTokenValidator, defineEvent, discoverAuthorizationServerMetadata, discoverOAuthMetadata, discoverOAuthProtectedResourceMetadata, exchangeAuthorization, extractBearerToken, extractResourceMetadataUrl, generateProtectedResourceMetadata, generateWWWAuthenticateHeader, getCallbackUrlCandidates, getMcpCallToolContent, getMcpCallToolMeta, isMCPTaskStoreAdmin, isWorkflowTaskContext, parseErrorResponse, principalKeyOf, refreshAuthorization, selectResourceURL, startAuthorization, taskKeyOf, traceContextFromMeta, traceContextToMeta };
25110
25191
 
25111
25192
  //# sourceMappingURL=index.js.map