@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.cjs CHANGED
@@ -15870,6 +15870,16 @@ function shouldDetachPersistentTransportRequest(init) {
15870
15870
  return false;
15871
15871
  }
15872
15872
  }
15873
+ /** Whether a request body is a single JSON-RPC response: the client's reply to a server request. */
15874
+ function isJsonRpcResponseBody(body) {
15875
+ if (typeof body !== "string") return false;
15876
+ try {
15877
+ const message = JSON.parse(body);
15878
+ return typeof message === "object" && message !== null && !Array.isArray(message) && !("method" in message) && "id" in message && ("result" in message || "error" in message);
15879
+ } catch {
15880
+ return false;
15881
+ }
15882
+ }
15873
15883
  const TASK_WAIT_ERROR_IDS = {
15874
15884
  timeout: "MCP_CLIENT_TOOL_TASK_TIMEOUT",
15875
15885
  expired: "MCP_CLIENT_TOOL_TASK_EXPIRED",
@@ -16098,6 +16108,19 @@ var InternalMastraMCPClient = class extends _mastra_core_base.MastraBase {
16098
16108
  clientConnectionOnClose;
16099
16109
  _authState;
16100
16110
  operationContextStore = new async_hooks.AsyncLocalStorage();
16111
+ /**
16112
+ * The requestContext of the tool call whose execution opened the connection. Only the
16113
+ * transport's fetch reads it, so the handshake and the streams the connection keeps open are
16114
+ * sent as that call. Messages arriving on those streams are delivered outside it, so server logs
16115
+ * are not labelled with it and requests their handlers send are not sent as that call.
16116
+ */
16117
+ connectionContextStore = new async_hooks.AsyncLocalStorage();
16118
+ /**
16119
+ * The connection context of the transport a server message arrived on, as it was when that
16120
+ * transport was created, set while the message is delivered. The transport's fetch reads it only for the client's replies to server requests,
16121
+ * which belong to the connection rather than to the handler that produced them.
16122
+ */
16123
+ replyContextStore = new async_hooks.AsyncLocalStorage();
16101
16124
  exitHookUnsubscribe;
16102
16125
  sigTermHandler;
16103
16126
  sigHupHandler;
@@ -16290,7 +16313,7 @@ var InternalMastraMCPClient = class extends _mastra_core_base.MastraBase {
16290
16313
  if (allowedHosts !== void 0) assertHostAllowed(url, allowedHosts);
16291
16314
  const policyUserFetch = userFetch && allowedHosts !== void 0 ? wrapFetchWithHostPolicy(userFetch, allowedHosts) : void 0;
16292
16315
  const fetch = (requestUrl, init) => {
16293
- const requestContext = this.operationContextStore.getStore() ?? null;
16316
+ const requestContext = this.fetchRequestContext(init);
16294
16317
  const executeFetch = () => {
16295
16318
  if (allowedHosts === void 0) return userFetch ? userFetch(requestUrl, init, requestContext) : globalThis.fetch(requestUrl, init);
16296
16319
  if (policyUserFetch) return policyUserFetch(requestUrl, init, requestContext);
@@ -16305,6 +16328,7 @@ var InternalMastraMCPClient = class extends _mastra_core_base.MastraBase {
16305
16328
  fetch
16306
16329
  });
16307
16330
  observeEventStreamRequests(transport, this.noteEventStreamSent);
16331
+ this.deliverOutsideConnectionContext(transport);
16308
16332
  this.connectingTransport = transport;
16309
16333
  try {
16310
16334
  this.assertSameLifecycle(generation);
@@ -16326,6 +16350,59 @@ var InternalMastraMCPClient = class extends _mastra_core_base.MastraBase {
16326
16350
  }
16327
16351
  }
16328
16352
  /**
16353
+ * Connects as a tool call: the handshake and the streams the connection keeps open reach the
16354
+ * transport's fetch with the call's requestContext, through the connection context. Any enclosing
16355
+ * operation scope is exited first. It would take priority in fetch, and the new streams and stdio
16356
+ * child would deliver every later server message in it.
16357
+ */
16358
+ connectAsCall(requestContext, connect) {
16359
+ return this.operationContextStore.exit(() => this.connectionContextStore.run(requestContext, connect));
16360
+ }
16361
+ /**
16362
+ * The requestContext the transport's fetch passes on for an outgoing request. An operation scope
16363
+ * owns its requests, even when its requestContext is null. Outside one, the handshake and the
16364
+ * streams the connection keeps open use the connection context, and a reply to a server request
16365
+ * uses the context of the connection that delivered the request.
16366
+ */
16367
+ fetchRequestContext(init) {
16368
+ const operationContext = this.operationContextStore.getStore();
16369
+ if (operationContext !== void 0) return operationContext;
16370
+ const connectionContext = this.connectionContextStore.getStore();
16371
+ if (connectionContext !== void 0) return connectionContext;
16372
+ const replyContext = this.replyContextStore.getStore();
16373
+ if (replyContext !== void 0 && isJsonRpcResponseBody(init?.body)) return replyContext;
16374
+ return null;
16375
+ }
16376
+ /**
16377
+ * Delivers every message the server sends outside the connection context, from the handshake on.
16378
+ * A server log, notification or request arriving on a stream the opening call started belongs to
16379
+ * no call, so a request its handler sends must not be sent as that call. The client's reply to a
16380
+ * server request still is, through `replyContextStore`.
16381
+ *
16382
+ * The reply context is the connection context this transport was created in, captured once. Read
16383
+ * at delivery time, it would carry over to a connection that a handler of an older connection's
16384
+ * message opens, such as a `reconnectServer()` from a server log callback.
16385
+ *
16386
+ * The SDK assigns `onmessage` during `connect()` and swaps it while probing the server, so every
16387
+ * assignment is wrapped. A handler that wraps an already wrapped one (`interceptTaskResults`, or
16388
+ * the probe restoring its saved handler) runs with the same reply context.
16389
+ */
16390
+ deliverOutsideConnectionContext(transport) {
16391
+ const replyContext = this.connectionContextStore.getStore() ?? null;
16392
+ let deliver;
16393
+ Object.defineProperty(transport, "onmessage", {
16394
+ configurable: true,
16395
+ enumerable: true,
16396
+ get: () => deliver,
16397
+ set: (handler) => {
16398
+ deliver = handler && ((message, extra) => {
16399
+ const dispatch = () => handler.call(transport, message, extra);
16400
+ this.connectionContextStore.exit(() => this.replyContextStore.run(replyContext, dispatch));
16401
+ });
16402
+ }
16403
+ });
16404
+ }
16405
+ /**
16329
16406
  * Detaches whatever transport is still attached to the underlying SDK Client.
16330
16407
  *
16331
16408
  * The SDK assigns its internal `_transport` before `transport.start()` and never
@@ -16663,7 +16740,8 @@ var InternalMastraMCPClient = class extends _mastra_core_base.MastraBase {
16663
16740
  this.serverImplementation = void 0;
16664
16741
  }
16665
16742
  this.assertSameLifecycle(generation);
16666
- await this.connect(generation);
16743
+ const callContext = this.operationContextStore.getStore();
16744
+ await (callContext === void 0 ? this.connect(generation) : this.connectAsCall(callContext, () => this.connect(generation)));
16667
16745
  this.log("debug", "Successfully reconnected to MCP server");
16668
16746
  })();
16669
16747
  this.reconnectPromise = reconnectPromise;
@@ -17156,8 +17234,8 @@ var InternalMastraMCPClient = class extends _mastra_core_base.MastraBase {
17156
17234
  },
17157
17235
  ...tool.outputSchema ? { toModelOutput: createStructuredToolToModelOutput() } : {},
17158
17236
  execute: async (input, context) => {
17159
- if (serverMeta.connectFirst) await this.connect();
17160
17237
  const operationContext = context?.requestContext ?? null;
17238
+ if (serverMeta.connectFirst) await this.connectAsCall(operationContext, () => this.connect());
17161
17239
  return this.operationContextStore.run(operationContext, async () => {
17162
17240
  const suspendHost = context?.agent ?? context?.workflow;
17163
17241
  const toolCallSpan = context?.tracingContext?.currentSpan;
@@ -19096,6 +19174,7 @@ onListChanged: async (serverName, handler) => {
19096
19174
  errorId: "MCP_CLIENT_GET_TOOLS_FAILED",
19097
19175
  logMessage: "Failed to list tools from server:"
19098
19176
  }, options);
19177
+ const keySources = /* @__PURE__ */ new Map();
19099
19178
  for (const { serverName, value, error, duration } of settled) {
19100
19179
  durations[serverName] = duration;
19101
19180
  if (error !== void 0) {
@@ -19103,7 +19182,7 @@ onListChanged: async (serverName, handler) => {
19103
19182
  errorDetails[serverName] = error;
19104
19183
  continue;
19105
19184
  }
19106
- for (const [toolName, toolConfig] of Object.entries(value)) connectedTools[`${serverName}_${toolName}`] = toolConfig;
19185
+ for (const [toolName, toolConfig] of Object.entries(value)) this.setNamespacedTool(connectedTools, keySources, serverName, toolName, toolConfig);
19107
19186
  }
19108
19187
  const result = {
19109
19188
  tools: connectedTools,
@@ -19275,16 +19354,18 @@ onListChanged: async (serverName, handler) => {
19275
19354
  async toolsFromDefinitions({ definitions: catalog }) {
19276
19355
  const configuredServers = new Set(Object.keys(this.serverConfigs));
19277
19356
  const tools = {};
19357
+ const keySources = /* @__PURE__ */ new Map();
19278
19358
  for (const [serverName, definitions] of Object.entries(catalog)) {
19279
19359
  if (!configuredServers.has(serverName)) {
19280
19360
  this.logger.warn("Skipping cached MCP tool definitions for a server that is no longer configured", { serverName });
19281
19361
  continue;
19282
19362
  }
19283
19363
  for (const [toolName, definition] of Object.entries(modelVisibleDefinitions(definitions))) try {
19284
- tools[`${serverName}_${toolName}`] = await this.toolFromDefinition({
19364
+ const tool = await this.toolFromDefinition({
19285
19365
  serverName,
19286
19366
  definition
19287
19367
  });
19368
+ this.setNamespacedTool(tools, keySources, serverName, toolName, tool);
19288
19369
  } catch (error) {
19289
19370
  if (!(error instanceof _mastra_core_error.MastraError && error.id === "MCP_CLIENT_INVALID_TOOL_INPUT_SCHEMA")) throw error;
19290
19371
  }
@@ -19345,6 +19426,28 @@ onListChanged: async (serverName, handler) => {
19345
19426
  } : result;
19346
19427
  }
19347
19428
  /**
19429
+ * Stores a tool under its `serverName_toolName` key. Server and tool names can both contain `_`,
19430
+ * so two different pairs can produce one key (`local` + `x_status` and `local_x` + `status`).
19431
+ * The later pair keeps the key, as before; this logs a warning instead of dropping a tool silently.
19432
+ */
19433
+ setNamespacedTool(tools, keySources, serverName, toolName, tool) {
19434
+ const key = `${serverName}_${toolName}`;
19435
+ const replaced = keySources.get(key);
19436
+ 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.`, {
19437
+ key,
19438
+ replaced,
19439
+ replacedBy: {
19440
+ serverName,
19441
+ toolName
19442
+ }
19443
+ });
19444
+ keySources.set(key, {
19445
+ serverName,
19446
+ toolName
19447
+ });
19448
+ tools[key] = tool;
19449
+ }
19450
+ /**
19348
19451
  * Runs a per-server discovery `operation` against every configured server
19349
19452
  * concurrently, isolating and logging per-server failures. Results are
19350
19453
  * returned in configuration order (`Object.keys(serverConfigs)`) so callers