@mastra/mcp 2.9.0-alpha.1 → 2.9.0-alpha.3

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
@@ -15844,6 +15844,16 @@ function shouldDetachPersistentTransportRequest(init) {
15844
15844
  return false;
15845
15845
  }
15846
15846
  }
15847
+ /** Whether a request body is a single JSON-RPC response: the client's reply to a server request. */
15848
+ function isJsonRpcResponseBody(body) {
15849
+ if (typeof body !== "string") return false;
15850
+ try {
15851
+ const message = JSON.parse(body);
15852
+ return typeof message === "object" && message !== null && !Array.isArray(message) && !("method" in message) && "id" in message && ("result" in message || "error" in message);
15853
+ } catch {
15854
+ return false;
15855
+ }
15856
+ }
15847
15857
  const TASK_WAIT_ERROR_IDS = {
15848
15858
  timeout: "MCP_CLIENT_TOOL_TASK_TIMEOUT",
15849
15859
  expired: "MCP_CLIENT_TOOL_TASK_EXPIRED",
@@ -16072,6 +16082,19 @@ var InternalMastraMCPClient = class extends MastraBase {
16072
16082
  clientConnectionOnClose;
16073
16083
  _authState;
16074
16084
  operationContextStore = new AsyncLocalStorage();
16085
+ /**
16086
+ * The requestContext of the tool call whose execution opened the connection. Only the
16087
+ * transport's fetch reads it, so the handshake and the streams the connection keeps open are
16088
+ * sent as that call. Messages arriving on those streams are delivered outside it, so server logs
16089
+ * are not labelled with it and requests their handlers send are not sent as that call.
16090
+ */
16091
+ connectionContextStore = new AsyncLocalStorage();
16092
+ /**
16093
+ * The connection context of the transport a server message arrived on, as it was when that
16094
+ * transport was created, set while the message is delivered. The transport's fetch reads it only for the client's replies to server requests,
16095
+ * which belong to the connection rather than to the handler that produced them.
16096
+ */
16097
+ replyContextStore = new AsyncLocalStorage();
16075
16098
  exitHookUnsubscribe;
16076
16099
  sigTermHandler;
16077
16100
  sigHupHandler;
@@ -16264,7 +16287,7 @@ var InternalMastraMCPClient = class extends MastraBase {
16264
16287
  if (allowedHosts !== void 0) assertHostAllowed(url, allowedHosts);
16265
16288
  const policyUserFetch = userFetch && allowedHosts !== void 0 ? wrapFetchWithHostPolicy(userFetch, allowedHosts) : void 0;
16266
16289
  const fetch = (requestUrl, init) => {
16267
- const requestContext = this.operationContextStore.getStore() ?? null;
16290
+ const requestContext = this.fetchRequestContext(init);
16268
16291
  const executeFetch = () => {
16269
16292
  if (allowedHosts === void 0) return userFetch ? userFetch(requestUrl, init, requestContext) : globalThis.fetch(requestUrl, init);
16270
16293
  if (policyUserFetch) return policyUserFetch(requestUrl, init, requestContext);
@@ -16279,6 +16302,7 @@ var InternalMastraMCPClient = class extends MastraBase {
16279
16302
  fetch
16280
16303
  });
16281
16304
  observeEventStreamRequests(transport, this.noteEventStreamSent);
16305
+ this.deliverOutsideConnectionContext(transport);
16282
16306
  this.connectingTransport = transport;
16283
16307
  try {
16284
16308
  this.assertSameLifecycle(generation);
@@ -16300,6 +16324,59 @@ var InternalMastraMCPClient = class extends MastraBase {
16300
16324
  }
16301
16325
  }
16302
16326
  /**
16327
+ * Connects as a tool call: the handshake and the streams the connection keeps open reach the
16328
+ * transport's fetch with the call's requestContext, through the connection context. Any enclosing
16329
+ * operation scope is exited first. It would take priority in fetch, and the new streams and stdio
16330
+ * child would deliver every later server message in it.
16331
+ */
16332
+ connectAsCall(requestContext, connect) {
16333
+ return this.operationContextStore.exit(() => this.connectionContextStore.run(requestContext, connect));
16334
+ }
16335
+ /**
16336
+ * The requestContext the transport's fetch passes on for an outgoing request. An operation scope
16337
+ * owns its requests, even when its requestContext is null. Outside one, the handshake and the
16338
+ * streams the connection keeps open use the connection context, and a reply to a server request
16339
+ * uses the context of the connection that delivered the request.
16340
+ */
16341
+ fetchRequestContext(init) {
16342
+ const operationContext = this.operationContextStore.getStore();
16343
+ if (operationContext !== void 0) return operationContext;
16344
+ const connectionContext = this.connectionContextStore.getStore();
16345
+ if (connectionContext !== void 0) return connectionContext;
16346
+ const replyContext = this.replyContextStore.getStore();
16347
+ if (replyContext !== void 0 && isJsonRpcResponseBody(init?.body)) return replyContext;
16348
+ return null;
16349
+ }
16350
+ /**
16351
+ * Delivers every message the server sends outside the connection context, from the handshake on.
16352
+ * A server log, notification or request arriving on a stream the opening call started belongs to
16353
+ * no call, so a request its handler sends must not be sent as that call. The client's reply to a
16354
+ * server request still is, through `replyContextStore`.
16355
+ *
16356
+ * The reply context is the connection context this transport was created in, captured once. Read
16357
+ * at delivery time, it would carry over to a connection that a handler of an older connection's
16358
+ * message opens, such as a `reconnectServer()` from a server log callback.
16359
+ *
16360
+ * The SDK assigns `onmessage` during `connect()` and swaps it while probing the server, so every
16361
+ * assignment is wrapped. A handler that wraps an already wrapped one (`interceptTaskResults`, or
16362
+ * the probe restoring its saved handler) runs with the same reply context.
16363
+ */
16364
+ deliverOutsideConnectionContext(transport) {
16365
+ const replyContext = this.connectionContextStore.getStore() ?? null;
16366
+ let deliver;
16367
+ Object.defineProperty(transport, "onmessage", {
16368
+ configurable: true,
16369
+ enumerable: true,
16370
+ get: () => deliver,
16371
+ set: (handler) => {
16372
+ deliver = handler && ((message, extra) => {
16373
+ const dispatch = () => handler.call(transport, message, extra);
16374
+ this.connectionContextStore.exit(() => this.replyContextStore.run(replyContext, dispatch));
16375
+ });
16376
+ }
16377
+ });
16378
+ }
16379
+ /**
16303
16380
  * Detaches whatever transport is still attached to the underlying SDK Client.
16304
16381
  *
16305
16382
  * The SDK assigns its internal `_transport` before `transport.start()` and never
@@ -16637,7 +16714,8 @@ var InternalMastraMCPClient = class extends MastraBase {
16637
16714
  this.serverImplementation = void 0;
16638
16715
  }
16639
16716
  this.assertSameLifecycle(generation);
16640
- await this.connect(generation);
16717
+ const callContext = this.operationContextStore.getStore();
16718
+ await (callContext === void 0 ? this.connect(generation) : this.connectAsCall(callContext, () => this.connect(generation)));
16641
16719
  this.log("debug", "Successfully reconnected to MCP server");
16642
16720
  })();
16643
16721
  this.reconnectPromise = reconnectPromise;
@@ -17130,8 +17208,8 @@ var InternalMastraMCPClient = class extends MastraBase {
17130
17208
  },
17131
17209
  ...tool.outputSchema ? { toModelOutput: createStructuredToolToModelOutput() } : {},
17132
17210
  execute: async (input, context) => {
17133
- if (serverMeta.connectFirst) await this.connect();
17134
17211
  const operationContext = context?.requestContext ?? null;
17212
+ if (serverMeta.connectFirst) await this.connectAsCall(operationContext, () => this.connect());
17135
17213
  return this.operationContextStore.run(operationContext, async () => {
17136
17214
  const suspendHost = context?.agent ?? context?.workflow;
17137
17215
  const toolCallSpan = context?.tracingContext?.currentSpan;
@@ -19070,6 +19148,7 @@ onListChanged: async (serverName, handler) => {
19070
19148
  errorId: "MCP_CLIENT_GET_TOOLS_FAILED",
19071
19149
  logMessage: "Failed to list tools from server:"
19072
19150
  }, options);
19151
+ const keySources = /* @__PURE__ */ new Map();
19073
19152
  for (const { serverName, value, error, duration } of settled) {
19074
19153
  durations[serverName] = duration;
19075
19154
  if (error !== void 0) {
@@ -19077,7 +19156,7 @@ onListChanged: async (serverName, handler) => {
19077
19156
  errorDetails[serverName] = error;
19078
19157
  continue;
19079
19158
  }
19080
- for (const [toolName, toolConfig] of Object.entries(value)) connectedTools[`${serverName}_${toolName}`] = toolConfig;
19159
+ for (const [toolName, toolConfig] of Object.entries(value)) this.setNamespacedTool(connectedTools, keySources, serverName, toolName, toolConfig);
19081
19160
  }
19082
19161
  const result = {
19083
19162
  tools: connectedTools,
@@ -19249,16 +19328,18 @@ onListChanged: async (serverName, handler) => {
19249
19328
  async toolsFromDefinitions({ definitions: catalog }) {
19250
19329
  const configuredServers = new Set(Object.keys(this.serverConfigs));
19251
19330
  const tools = {};
19331
+ const keySources = /* @__PURE__ */ new Map();
19252
19332
  for (const [serverName, definitions] of Object.entries(catalog)) {
19253
19333
  if (!configuredServers.has(serverName)) {
19254
19334
  this.logger.warn("Skipping cached MCP tool definitions for a server that is no longer configured", { serverName });
19255
19335
  continue;
19256
19336
  }
19257
19337
  for (const [toolName, definition] of Object.entries(modelVisibleDefinitions(definitions))) try {
19258
- tools[`${serverName}_${toolName}`] = await this.toolFromDefinition({
19338
+ const tool = await this.toolFromDefinition({
19259
19339
  serverName,
19260
19340
  definition
19261
19341
  });
19342
+ this.setNamespacedTool(tools, keySources, serverName, toolName, tool);
19262
19343
  } catch (error) {
19263
19344
  if (!(error instanceof MastraError && error.id === "MCP_CLIENT_INVALID_TOOL_INPUT_SCHEMA")) throw error;
19264
19345
  }
@@ -19319,6 +19400,28 @@ onListChanged: async (serverName, handler) => {
19319
19400
  } : result;
19320
19401
  }
19321
19402
  /**
19403
+ * Stores a tool under its `serverName_toolName` key. Server and tool names can both contain `_`,
19404
+ * so two different pairs can produce one key (`local` + `x_status` and `local_x` + `status`).
19405
+ * The later pair keeps the key, as before; this logs a warning instead of dropping a tool silently.
19406
+ */
19407
+ setNamespacedTool(tools, keySources, serverName, toolName, tool) {
19408
+ const key = `${serverName}_${toolName}`;
19409
+ const replaced = keySources.get(key);
19410
+ if (replaced) this.logger.warn(`MCP tool key "${key}" from server "${serverName}" (tool "${toolName}") replaces the tool from server "${replaced.serverName}" (tool "${replaced.toolName}"), which produces the same key. Only the tool from "${serverName}" is kept. Rename one of the servers in \`servers\` to keep both.`, {
19411
+ key,
19412
+ replaced,
19413
+ replacedBy: {
19414
+ serverName,
19415
+ toolName
19416
+ }
19417
+ });
19418
+ keySources.set(key, {
19419
+ serverName,
19420
+ toolName
19421
+ });
19422
+ tools[key] = tool;
19423
+ }
19424
+ /**
19322
19425
  * Runs a per-server discovery `operation` against every configured server
19323
19426
  * concurrently, isolating and logging per-server failures. Results are
19324
19427
  * returned in configuration order (`Object.keys(serverConfigs)`) so callers