@mastra/mcp 2.9.0-alpha.0 → 2.9.0-alpha.2

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
@@ -14385,6 +14385,35 @@ function isReconnectableMCPError(error) {
14385
14385
  const errorMessage = error.message.toLowerCase();
14386
14386
  return errorMessage.includes("not connected") || errorMessage.includes("http 400") || errorMessage.includes("http 401") || errorMessage.includes("http 403") || errorMessage.includes("http 404") || errorMessage.includes("econnrefused") || errorMessage.includes("fetch failed") || errorMessage.includes("connection refused") || errorMessage.includes("connection closed") || errorMessage.includes("typeerror: terminated");
14387
14387
  }
14388
+ const CONNECTION_NOT_ESTABLISHED_CODES = /* @__PURE__ */ new Set([
14389
+ "ECONNREFUSED",
14390
+ "ENOTFOUND",
14391
+ "EAI_AGAIN",
14392
+ "UND_ERR_CONNECT_TIMEOUT"
14393
+ ]);
14394
+ const REJECTED_BEFORE_DISPATCH_STATUSES = /* @__PURE__ */ new Set([
14395
+ 400,
14396
+ 401,
14397
+ 403,
14398
+ 404
14399
+ ]);
14400
+ /**
14401
+ * Whether a reconnectable failure proves the request never reached the server's
14402
+ * handler, so sending it again cannot repeat its effects.
14403
+ *
14404
+ * A reset socket (`fetch failed` with `UND_ERR_SOCKET`), a cut-off response
14405
+ * (`terminated`) or a closed transport (`Connection closed`) can all follow a
14406
+ * request the server already received and ran, so they return false.
14407
+ */
14408
+ function isUnsentRequestError(error) {
14409
+ if (!isReconnectableMCPError(error)) return false;
14410
+ const { httpStatus, code } = getMCPDiscoveryErrorDetails(error);
14411
+ if (typeof code === "number") return false;
14412
+ if (httpStatus !== void 0) return REJECTED_BEFORE_DISPATCH_STATUSES.has(httpStatus);
14413
+ const message = error.message.toLowerCase();
14414
+ if (message.includes("not connected") || message.includes("econnrefused") || message.includes("connection refused") || /\bhttp 40[0134]\b/.test(message)) return true;
14415
+ return typeof code === "string" && CONNECTION_NOT_ESTABLISHED_CODES.has(code);
14416
+ }
14388
14417
  const GATEWAY_UNAVAILABLE_STATUSES = /* @__PURE__ */ new Set([
14389
14418
  502,
14390
14419
  503,
@@ -15815,6 +15844,16 @@ function shouldDetachPersistentTransportRequest(init) {
15815
15844
  return false;
15816
15845
  }
15817
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
+ }
15818
15857
  const TASK_WAIT_ERROR_IDS = {
15819
15858
  timeout: "MCP_CLIENT_TOOL_TASK_TIMEOUT",
15820
15859
  expired: "MCP_CLIENT_TOOL_TASK_EXPIRED",
@@ -16043,6 +16082,19 @@ var InternalMastraMCPClient = class extends MastraBase {
16043
16082
  clientConnectionOnClose;
16044
16083
  _authState;
16045
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();
16046
16098
  exitHookUnsubscribe;
16047
16099
  sigTermHandler;
16048
16100
  sigHupHandler;
@@ -16235,7 +16287,7 @@ var InternalMastraMCPClient = class extends MastraBase {
16235
16287
  if (allowedHosts !== void 0) assertHostAllowed(url, allowedHosts);
16236
16288
  const policyUserFetch = userFetch && allowedHosts !== void 0 ? wrapFetchWithHostPolicy(userFetch, allowedHosts) : void 0;
16237
16289
  const fetch = (requestUrl, init) => {
16238
- const requestContext = this.operationContextStore.getStore() ?? null;
16290
+ const requestContext = this.fetchRequestContext(init);
16239
16291
  const executeFetch = () => {
16240
16292
  if (allowedHosts === void 0) return userFetch ? userFetch(requestUrl, init, requestContext) : globalThis.fetch(requestUrl, init);
16241
16293
  if (policyUserFetch) return policyUserFetch(requestUrl, init, requestContext);
@@ -16250,6 +16302,7 @@ var InternalMastraMCPClient = class extends MastraBase {
16250
16302
  fetch
16251
16303
  });
16252
16304
  observeEventStreamRequests(transport, this.noteEventStreamSent);
16305
+ this.deliverOutsideConnectionContext(transport);
16253
16306
  this.connectingTransport = transport;
16254
16307
  try {
16255
16308
  this.assertSameLifecycle(generation);
@@ -16271,6 +16324,59 @@ var InternalMastraMCPClient = class extends MastraBase {
16271
16324
  }
16272
16325
  }
16273
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
+ /**
16274
16380
  * Detaches whatever transport is still attached to the underlying SDK Client.
16275
16381
  *
16276
16382
  * The SDK assigns its internal `_transport` before `transport.start()` and never
@@ -16608,7 +16714,8 @@ var InternalMastraMCPClient = class extends MastraBase {
16608
16714
  this.serverImplementation = void 0;
16609
16715
  }
16610
16716
  this.assertSameLifecycle(generation);
16611
- await this.connect(generation);
16717
+ const callContext = this.operationContextStore.getStore();
16718
+ await (callContext === void 0 ? this.connect(generation) : this.connectAsCall(callContext, () => this.connect(generation)));
16612
16719
  this.log("debug", "Successfully reconnected to MCP server");
16613
16720
  })();
16614
16721
  this.reconnectPromise = reconnectPromise;
@@ -17101,8 +17208,8 @@ var InternalMastraMCPClient = class extends MastraBase {
17101
17208
  },
17102
17209
  ...tool.outputSchema ? { toModelOutput: createStructuredToolToModelOutput() } : {},
17103
17210
  execute: async (input, context) => {
17104
- if (serverMeta.connectFirst) await this.connect();
17105
17211
  const operationContext = context?.requestContext ?? null;
17212
+ if (serverMeta.connectFirst) await this.connectAsCall(operationContext, () => this.connect());
17106
17213
  return this.operationContextStore.run(operationContext, async () => {
17107
17214
  const suspendHost = context?.agent ?? context?.workflow;
17108
17215
  const toolCallSpan = context?.tracingContext?.currentSpan;
@@ -17202,7 +17309,26 @@ var InternalMastraMCPClient = class extends MastraBase {
17202
17309
  try {
17203
17310
  return await executeToolCall();
17204
17311
  } catch (e) {
17205
- if (!(e instanceof MastraError && (e.id === "MCP_CLIENT_TOOL_EXECUTION_FAILED" || e.id.startsWith("MCP_CLIENT_TOOL_TASK_"))) && isReconnectableMCPError(e)) {
17312
+ const isToolExecutionError = e instanceof MastraError && (e.id === "MCP_CLIENT_TOOL_EXECUTION_FAILED" || e.id.startsWith("MCP_CLIENT_TOOL_TASK_"));
17313
+ if (!isToolExecutionError && isReconnectableMCPError(e) && !isUnsentRequestError(e)) {
17314
+ if (annotations?.readOnlyHint === true || annotations?.idempotentHint === true) {
17315
+ this.log("debug", `Retrying replay-safe tool ${tool.name} after a transport error`, { error: errorKind(e) });
17316
+ if (this.transport !== failedTransport || this.reconnectPromise) await this.reconnectAfterTransportFailure(failedTransport, lifecycleGeneration);
17317
+ return await executeToolCall();
17318
+ }
17319
+ this.log("error", `Lost the connection during tool ${tool.name}; it may have run`, { error: errorKind(e) });
17320
+ throw new MastraError({
17321
+ id: "MCP_CLIENT_TOOL_CALL_OUTCOME_UNKNOWN",
17322
+ domain: ErrorDomain.MCP,
17323
+ category: ErrorCategory.THIRD_PARTY,
17324
+ text: `The connection to MCP server "${this.name}" failed before tool "${tool.name}" returned a result. The tool may have run on the server, so it was not called again.`,
17325
+ details: {
17326
+ toolName: tool.name,
17327
+ serverName: this.name
17328
+ }
17329
+ }, e);
17330
+ }
17331
+ if (!isToolExecutionError && isReconnectableMCPError(e)) {
17206
17332
  this.log("debug", `Transport error detected for tool ${tool.name}, attempting reconnection...`, { error: errorKind(e) });
17207
17333
  try {
17208
17334
  await this.reconnectAfterTransportFailure(failedTransport, lifecycleGeneration);
@@ -17644,12 +17770,12 @@ async function createOAuthCallbackServer(options) {
17644
17770
  },
17645
17771
  close() {
17646
17772
  settle({ error: /* @__PURE__ */ new Error("OAuth callback server closed before receiving an authorization code") });
17647
- server.closeIdleConnections();
17648
17773
  return new Promise((resolve, reject) => {
17649
17774
  server.close((error) => {
17650
17775
  if (error && error.code !== "ERR_SERVER_NOT_RUNNING") reject(error);
17651
17776
  else resolve();
17652
17777
  });
17778
+ server.closeAllConnections();
17653
17779
  });
17654
17780
  }
17655
17781
  };
@@ -22925,6 +23051,136 @@ function stepAt(workflow, path) {
22925
23051
  }
22926
23052
  return step;
22927
23053
  }
23054
+ // src/server/uri-template.ts
23055
+ /**
23056
+ * Whether `uri` is an RFC 6570 expansion of `template`. Decides membership only,
23057
+ * without extracting variables.
23058
+ *
23059
+ * The SDK's `UriTemplate.match` is a backtracking regex that takes minutes on a
23060
+ * few KB of client URI when variables are adjacent (`x://{a}-{b}-{c}-{d}`); this
23061
+ * walks the URI once per template segment instead.
23062
+ *
23063
+ * Plain `{x}` and `{+x}` must be non-empty, since an empty path segment never
23064
+ * names a resource. Outside `{+x}` and `{#x}`, values must be percent-encoded:
23065
+ * an ASCII reserved character other than `,` ends the value. Raw non-ASCII
23066
+ * characters are accepted because clients often send them unencoded.
23067
+ */
23068
+ function matchesUriTemplate(template, uri) {
23069
+ return advance(uri, /* @__PURE__ */ new Set([0]), parse(template)).has(uri.length);
23070
+ }
23071
+ const encoded = (char) => /[\w\-.~,]/.test(char) || char.charCodeAt(0) > 127;
23072
+ const anything = () => true;
23073
+ const literal = (text) => ({
23074
+ kind: "literal",
23075
+ text
23076
+ });
23077
+ const run = (allowed, min = 0) => ({
23078
+ kind: "run",
23079
+ allowed,
23080
+ min
23081
+ });
23082
+ const optional = (...segments) => ({
23083
+ kind: "optional",
23084
+ segments
23085
+ });
23086
+ const repeat = (...segments) => ({
23087
+ kind: "repeat",
23088
+ segments
23089
+ });
23090
+ const choice = (options) => ({
23091
+ kind: "choice",
23092
+ options
23093
+ });
23094
+ function parse(template) {
23095
+ const segments = [];
23096
+ let i = 0;
23097
+ while (i < template.length) {
23098
+ const open = template.indexOf("{", i);
23099
+ const close = open === -1 ? -1 : template.indexOf("}", open);
23100
+ if (close === -1) {
23101
+ segments.push(literal(template.slice(i)));
23102
+ break;
23103
+ }
23104
+ if (open > i) segments.push(literal(template.slice(i, open)));
23105
+ segments.push(...expression(template.slice(open + 1, close)));
23106
+ i = close + 1;
23107
+ }
23108
+ return segments;
23109
+ }
23110
+ function expression(body) {
23111
+ const operator = "+#./;?&".includes(body[0] ?? "") ? body[0] : "";
23112
+ const specs = body.slice(operator.length).split(",").map((spec) => ({
23113
+ name: spec.replace(/[*]|:\d*$/g, "").trim(),
23114
+ exploded: spec.includes("*")
23115
+ })).filter((spec) => spec.name);
23116
+ switch (operator) {
23117
+ case "": return [run(encoded, 1)];
23118
+ case "+": return [run(anything, 1)];
23119
+ case "#": return [optional(literal("#"), run(anything))];
23120
+ case "/": return specs.map(({ exploded }) => exploded ? repeat(literal("/"), run(encoded)) : optional(literal("/"), run(encoded)));
23121
+ case ".": return specs.map(() => optional(literal("."), run(encoded)));
23122
+ case ";": return specs.map(({ name, exploded }) => (exploded ? repeat : optional)(literal(`;${name}`), optional(literal("="), run(encoded))));
23123
+ default: {
23124
+ const pair = ({ name, exploded }, separator) => {
23125
+ const first = [literal(`${separator}${name}=`), run(encoded)];
23126
+ return exploded ? [...first, repeat(literal(`&${name}=`), run(encoded))] : first;
23127
+ };
23128
+ const firstPresent = specs.map((spec, i) => [...pair(spec, operator), ...specs.slice(i + 1).map((later) => optional(...pair(later, "&")))]);
23129
+ return [optional(choice(firstPresent))];
23130
+ }
23131
+ }
23132
+ }
23133
+ /** Every URI offset reachable after matching `segments` from any offset in `from`. */
23134
+ function advance(uri, from, segments) {
23135
+ let positions = from;
23136
+ for (const segment of segments) {
23137
+ if (positions.size === 0) break;
23138
+ positions = step(uri, positions, segment);
23139
+ }
23140
+ return positions;
23141
+ }
23142
+ function step(uri, positions, segment) {
23143
+ const next = /* @__PURE__ */ new Set();
23144
+ switch (segment.kind) {
23145
+ case "literal":
23146
+ for (const p of positions) if (uri.startsWith(segment.text, p)) next.add(p + segment.text.length);
23147
+ return next;
23148
+ case "choice":
23149
+ for (const option of segment.options) for (const p of advance(uri, positions, option)) next.add(p);
23150
+ return next;
23151
+ case "run": {
23152
+ let end = 0;
23153
+ for (const p of [...positions].sort((a, b) => a - b)) {
23154
+ const covered = p < end && next.has(p);
23155
+ if (segment.min === 0) next.add(p);
23156
+ if (covered) continue;
23157
+ end = p;
23158
+ for (let width = charWidth(uri, end, segment.allowed); width > 0; width = charWidth(uri, end, segment.allowed)) next.add(end += width);
23159
+ }
23160
+ return next;
23161
+ }
23162
+ case "optional":
23163
+ for (const p of positions) next.add(p);
23164
+ for (const p of advance(uri, positions, segment.segments)) next.add(p);
23165
+ return next;
23166
+ case "repeat": {
23167
+ for (const p of positions) next.add(p);
23168
+ let frontier = positions;
23169
+ while (frontier.size > 0) {
23170
+ const reached = [...advance(uri, frontier, segment.segments)].filter((p) => !next.has(p));
23171
+ for (const p of reached) next.add(p);
23172
+ frontier = new Set(reached);
23173
+ }
23174
+ return next;
23175
+ }
23176
+ }
23177
+ }
23178
+ /** Characters a run consumes at `i`: a whole `%HH` triplet, one allowed character, or none. */
23179
+ function charWidth(uri, i, allowed) {
23180
+ if (i >= uri.length) return 0;
23181
+ if (uri[i] === "%") return /^%[0-9A-Fa-f]{2}/.test(uri.slice(i, i + 3)) ? 3 : 0;
23182
+ return allowed(uri[i]) ? 1 : 0;
23183
+ }
22928
23184
  // src/server/server.ts
22929
23185
  const EMPTY_OBJECT_SCHEMA = {
22930
23186
  type: "object",
@@ -24297,10 +24553,24 @@ var MCPServer = class extends MCPServerBase {
24297
24553
  extra: toToolExecutionContext(ctx, this.name).extra,
24298
24554
  requestContext
24299
24555
  }) ?? []];
24556
+ const findResource = async (uri, ctx, requestContext) => {
24557
+ const listed = (await listResources(ctx, requestContext)).find((r) => r.uri === uri);
24558
+ if (listed || !options?.resourceTemplates) return listed;
24559
+ const template = (await options.resourceTemplates({
24560
+ extra: toToolExecutionContext(ctx, this.name).extra,
24561
+ requestContext
24562
+ })).find((t) => matchesUriTemplate(t.uriTemplate, uri));
24563
+ return template && {
24564
+ uri,
24565
+ name: template.name,
24566
+ mimeType: template.mimeType,
24567
+ _meta: template._meta
24568
+ };
24569
+ };
24300
24570
  this.setTracedHandler(server, "resources/list", async (_request, ctx, trace) => ({ resources: await listResources(ctx, trace.requestContext) }));
24301
24571
  this.setTracedHandler(server, "resources/read", async (request, ctx, trace) => {
24302
24572
  const uri = request.params.uri;
24303
- const resource = (await listResources(ctx, trace.requestContext)).find((r) => r.uri === uri);
24573
+ const resource = await findResource(uri, ctx, trace.requestContext);
24304
24574
  if (!resource) throw new ProtocolError$1(ProtocolErrorCode$1.InvalidParams, `Resource not found: ${uri}`);
24305
24575
  const html = this.appResourceHtml.get(uri);
24306
24576
  if (html !== void 0) return { contents: [this.resourceContents(resource, { text: html })] };