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

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,
@@ -17202,7 +17231,26 @@ var InternalMastraMCPClient = class extends MastraBase {
17202
17231
  try {
17203
17232
  return await executeToolCall();
17204
17233
  } catch (e) {
17205
- if (!(e instanceof MastraError && (e.id === "MCP_CLIENT_TOOL_EXECUTION_FAILED" || e.id.startsWith("MCP_CLIENT_TOOL_TASK_"))) && isReconnectableMCPError(e)) {
17234
+ const isToolExecutionError = e instanceof MastraError && (e.id === "MCP_CLIENT_TOOL_EXECUTION_FAILED" || e.id.startsWith("MCP_CLIENT_TOOL_TASK_"));
17235
+ if (!isToolExecutionError && isReconnectableMCPError(e) && !isUnsentRequestError(e)) {
17236
+ if (annotations?.readOnlyHint === true || annotations?.idempotentHint === true) {
17237
+ this.log("debug", `Retrying replay-safe tool ${tool.name} after a transport error`, { error: errorKind(e) });
17238
+ if (this.transport !== failedTransport || this.reconnectPromise) await this.reconnectAfterTransportFailure(failedTransport, lifecycleGeneration);
17239
+ return await executeToolCall();
17240
+ }
17241
+ this.log("error", `Lost the connection during tool ${tool.name}; it may have run`, { error: errorKind(e) });
17242
+ throw new MastraError({
17243
+ id: "MCP_CLIENT_TOOL_CALL_OUTCOME_UNKNOWN",
17244
+ domain: ErrorDomain.MCP,
17245
+ category: ErrorCategory.THIRD_PARTY,
17246
+ 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.`,
17247
+ details: {
17248
+ toolName: tool.name,
17249
+ serverName: this.name
17250
+ }
17251
+ }, e);
17252
+ }
17253
+ if (!isToolExecutionError && isReconnectableMCPError(e)) {
17206
17254
  this.log("debug", `Transport error detected for tool ${tool.name}, attempting reconnection...`, { error: errorKind(e) });
17207
17255
  try {
17208
17256
  await this.reconnectAfterTransportFailure(failedTransport, lifecycleGeneration);
@@ -17644,12 +17692,12 @@ async function createOAuthCallbackServer(options) {
17644
17692
  },
17645
17693
  close() {
17646
17694
  settle({ error: /* @__PURE__ */ new Error("OAuth callback server closed before receiving an authorization code") });
17647
- server.closeIdleConnections();
17648
17695
  return new Promise((resolve, reject) => {
17649
17696
  server.close((error) => {
17650
17697
  if (error && error.code !== "ERR_SERVER_NOT_RUNNING") reject(error);
17651
17698
  else resolve();
17652
17699
  });
17700
+ server.closeAllConnections();
17653
17701
  });
17654
17702
  }
17655
17703
  };
@@ -22925,6 +22973,136 @@ function stepAt(workflow, path) {
22925
22973
  }
22926
22974
  return step;
22927
22975
  }
22976
+ // src/server/uri-template.ts
22977
+ /**
22978
+ * Whether `uri` is an RFC 6570 expansion of `template`. Decides membership only,
22979
+ * without extracting variables.
22980
+ *
22981
+ * The SDK's `UriTemplate.match` is a backtracking regex that takes minutes on a
22982
+ * few KB of client URI when variables are adjacent (`x://{a}-{b}-{c}-{d}`); this
22983
+ * walks the URI once per template segment instead.
22984
+ *
22985
+ * Plain `{x}` and `{+x}` must be non-empty, since an empty path segment never
22986
+ * names a resource. Outside `{+x}` and `{#x}`, values must be percent-encoded:
22987
+ * an ASCII reserved character other than `,` ends the value. Raw non-ASCII
22988
+ * characters are accepted because clients often send them unencoded.
22989
+ */
22990
+ function matchesUriTemplate(template, uri) {
22991
+ return advance(uri, /* @__PURE__ */ new Set([0]), parse(template)).has(uri.length);
22992
+ }
22993
+ const encoded = (char) => /[\w\-.~,]/.test(char) || char.charCodeAt(0) > 127;
22994
+ const anything = () => true;
22995
+ const literal = (text) => ({
22996
+ kind: "literal",
22997
+ text
22998
+ });
22999
+ const run = (allowed, min = 0) => ({
23000
+ kind: "run",
23001
+ allowed,
23002
+ min
23003
+ });
23004
+ const optional = (...segments) => ({
23005
+ kind: "optional",
23006
+ segments
23007
+ });
23008
+ const repeat = (...segments) => ({
23009
+ kind: "repeat",
23010
+ segments
23011
+ });
23012
+ const choice = (options) => ({
23013
+ kind: "choice",
23014
+ options
23015
+ });
23016
+ function parse(template) {
23017
+ const segments = [];
23018
+ let i = 0;
23019
+ while (i < template.length) {
23020
+ const open = template.indexOf("{", i);
23021
+ const close = open === -1 ? -1 : template.indexOf("}", open);
23022
+ if (close === -1) {
23023
+ segments.push(literal(template.slice(i)));
23024
+ break;
23025
+ }
23026
+ if (open > i) segments.push(literal(template.slice(i, open)));
23027
+ segments.push(...expression(template.slice(open + 1, close)));
23028
+ i = close + 1;
23029
+ }
23030
+ return segments;
23031
+ }
23032
+ function expression(body) {
23033
+ const operator = "+#./;?&".includes(body[0] ?? "") ? body[0] : "";
23034
+ const specs = body.slice(operator.length).split(",").map((spec) => ({
23035
+ name: spec.replace(/[*]|:\d*$/g, "").trim(),
23036
+ exploded: spec.includes("*")
23037
+ })).filter((spec) => spec.name);
23038
+ switch (operator) {
23039
+ case "": return [run(encoded, 1)];
23040
+ case "+": return [run(anything, 1)];
23041
+ case "#": return [optional(literal("#"), run(anything))];
23042
+ case "/": return specs.map(({ exploded }) => exploded ? repeat(literal("/"), run(encoded)) : optional(literal("/"), run(encoded)));
23043
+ case ".": return specs.map(() => optional(literal("."), run(encoded)));
23044
+ case ";": return specs.map(({ name, exploded }) => (exploded ? repeat : optional)(literal(`;${name}`), optional(literal("="), run(encoded))));
23045
+ default: {
23046
+ const pair = ({ name, exploded }, separator) => {
23047
+ const first = [literal(`${separator}${name}=`), run(encoded)];
23048
+ return exploded ? [...first, repeat(literal(`&${name}=`), run(encoded))] : first;
23049
+ };
23050
+ const firstPresent = specs.map((spec, i) => [...pair(spec, operator), ...specs.slice(i + 1).map((later) => optional(...pair(later, "&")))]);
23051
+ return [optional(choice(firstPresent))];
23052
+ }
23053
+ }
23054
+ }
23055
+ /** Every URI offset reachable after matching `segments` from any offset in `from`. */
23056
+ function advance(uri, from, segments) {
23057
+ let positions = from;
23058
+ for (const segment of segments) {
23059
+ if (positions.size === 0) break;
23060
+ positions = step(uri, positions, segment);
23061
+ }
23062
+ return positions;
23063
+ }
23064
+ function step(uri, positions, segment) {
23065
+ const next = /* @__PURE__ */ new Set();
23066
+ switch (segment.kind) {
23067
+ case "literal":
23068
+ for (const p of positions) if (uri.startsWith(segment.text, p)) next.add(p + segment.text.length);
23069
+ return next;
23070
+ case "choice":
23071
+ for (const option of segment.options) for (const p of advance(uri, positions, option)) next.add(p);
23072
+ return next;
23073
+ case "run": {
23074
+ let end = 0;
23075
+ for (const p of [...positions].sort((a, b) => a - b)) {
23076
+ const covered = p < end && next.has(p);
23077
+ if (segment.min === 0) next.add(p);
23078
+ if (covered) continue;
23079
+ end = p;
23080
+ for (let width = charWidth(uri, end, segment.allowed); width > 0; width = charWidth(uri, end, segment.allowed)) next.add(end += width);
23081
+ }
23082
+ return next;
23083
+ }
23084
+ case "optional":
23085
+ for (const p of positions) next.add(p);
23086
+ for (const p of advance(uri, positions, segment.segments)) next.add(p);
23087
+ return next;
23088
+ case "repeat": {
23089
+ for (const p of positions) next.add(p);
23090
+ let frontier = positions;
23091
+ while (frontier.size > 0) {
23092
+ const reached = [...advance(uri, frontier, segment.segments)].filter((p) => !next.has(p));
23093
+ for (const p of reached) next.add(p);
23094
+ frontier = new Set(reached);
23095
+ }
23096
+ return next;
23097
+ }
23098
+ }
23099
+ }
23100
+ /** Characters a run consumes at `i`: a whole `%HH` triplet, one allowed character, or none. */
23101
+ function charWidth(uri, i, allowed) {
23102
+ if (i >= uri.length) return 0;
23103
+ if (uri[i] === "%") return /^%[0-9A-Fa-f]{2}/.test(uri.slice(i, i + 3)) ? 3 : 0;
23104
+ return allowed(uri[i]) ? 1 : 0;
23105
+ }
22928
23106
  // src/server/server.ts
22929
23107
  const EMPTY_OBJECT_SCHEMA = {
22930
23108
  type: "object",
@@ -24297,10 +24475,24 @@ var MCPServer = class extends MCPServerBase {
24297
24475
  extra: toToolExecutionContext(ctx, this.name).extra,
24298
24476
  requestContext
24299
24477
  }) ?? []];
24478
+ const findResource = async (uri, ctx, requestContext) => {
24479
+ const listed = (await listResources(ctx, requestContext)).find((r) => r.uri === uri);
24480
+ if (listed || !options?.resourceTemplates) return listed;
24481
+ const template = (await options.resourceTemplates({
24482
+ extra: toToolExecutionContext(ctx, this.name).extra,
24483
+ requestContext
24484
+ })).find((t) => matchesUriTemplate(t.uriTemplate, uri));
24485
+ return template && {
24486
+ uri,
24487
+ name: template.name,
24488
+ mimeType: template.mimeType,
24489
+ _meta: template._meta
24490
+ };
24491
+ };
24300
24492
  this.setTracedHandler(server, "resources/list", async (_request, ctx, trace) => ({ resources: await listResources(ctx, trace.requestContext) }));
24301
24493
  this.setTracedHandler(server, "resources/read", async (request, ctx, trace) => {
24302
24494
  const uri = request.params.uri;
24303
- const resource = (await listResources(ctx, trace.requestContext)).find((r) => r.uri === uri);
24495
+ const resource = await findResource(uri, ctx, trace.requestContext);
24304
24496
  if (!resource) throw new ProtocolError$1(ProtocolErrorCode$1.InvalidParams, `Resource not found: ${uri}`);
24305
24497
  const html = this.appResourceHtml.get(uri);
24306
24498
  if (html !== void 0) return { contents: [this.resourceContents(resource, { text: html })] };