@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.cjs CHANGED
@@ -14411,6 +14411,35 @@ function isReconnectableMCPError(error) {
14411
14411
  const errorMessage = error.message.toLowerCase();
14412
14412
  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");
14413
14413
  }
14414
+ const CONNECTION_NOT_ESTABLISHED_CODES = /* @__PURE__ */ new Set([
14415
+ "ECONNREFUSED",
14416
+ "ENOTFOUND",
14417
+ "EAI_AGAIN",
14418
+ "UND_ERR_CONNECT_TIMEOUT"
14419
+ ]);
14420
+ const REJECTED_BEFORE_DISPATCH_STATUSES = /* @__PURE__ */ new Set([
14421
+ 400,
14422
+ 401,
14423
+ 403,
14424
+ 404
14425
+ ]);
14426
+ /**
14427
+ * Whether a reconnectable failure proves the request never reached the server's
14428
+ * handler, so sending it again cannot repeat its effects.
14429
+ *
14430
+ * A reset socket (`fetch failed` with `UND_ERR_SOCKET`), a cut-off response
14431
+ * (`terminated`) or a closed transport (`Connection closed`) can all follow a
14432
+ * request the server already received and ran, so they return false.
14433
+ */
14434
+ function isUnsentRequestError(error) {
14435
+ if (!isReconnectableMCPError(error)) return false;
14436
+ const { httpStatus, code } = getMCPDiscoveryErrorDetails(error);
14437
+ if (typeof code === "number") return false;
14438
+ if (httpStatus !== void 0) return REJECTED_BEFORE_DISPATCH_STATUSES.has(httpStatus);
14439
+ const message = error.message.toLowerCase();
14440
+ if (message.includes("not connected") || message.includes("econnrefused") || message.includes("connection refused") || /\bhttp 40[0134]\b/.test(message)) return true;
14441
+ return typeof code === "string" && CONNECTION_NOT_ESTABLISHED_CODES.has(code);
14442
+ }
14414
14443
  const GATEWAY_UNAVAILABLE_STATUSES = /* @__PURE__ */ new Set([
14415
14444
  502,
14416
14445
  503,
@@ -15841,6 +15870,16 @@ function shouldDetachPersistentTransportRequest(init) {
15841
15870
  return false;
15842
15871
  }
15843
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
+ }
15844
15883
  const TASK_WAIT_ERROR_IDS = {
15845
15884
  timeout: "MCP_CLIENT_TOOL_TASK_TIMEOUT",
15846
15885
  expired: "MCP_CLIENT_TOOL_TASK_EXPIRED",
@@ -16069,6 +16108,19 @@ var InternalMastraMCPClient = class extends _mastra_core_base.MastraBase {
16069
16108
  clientConnectionOnClose;
16070
16109
  _authState;
16071
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();
16072
16124
  exitHookUnsubscribe;
16073
16125
  sigTermHandler;
16074
16126
  sigHupHandler;
@@ -16261,7 +16313,7 @@ var InternalMastraMCPClient = class extends _mastra_core_base.MastraBase {
16261
16313
  if (allowedHosts !== void 0) assertHostAllowed(url, allowedHosts);
16262
16314
  const policyUserFetch = userFetch && allowedHosts !== void 0 ? wrapFetchWithHostPolicy(userFetch, allowedHosts) : void 0;
16263
16315
  const fetch = (requestUrl, init) => {
16264
- const requestContext = this.operationContextStore.getStore() ?? null;
16316
+ const requestContext = this.fetchRequestContext(init);
16265
16317
  const executeFetch = () => {
16266
16318
  if (allowedHosts === void 0) return userFetch ? userFetch(requestUrl, init, requestContext) : globalThis.fetch(requestUrl, init);
16267
16319
  if (policyUserFetch) return policyUserFetch(requestUrl, init, requestContext);
@@ -16276,6 +16328,7 @@ var InternalMastraMCPClient = class extends _mastra_core_base.MastraBase {
16276
16328
  fetch
16277
16329
  });
16278
16330
  observeEventStreamRequests(transport, this.noteEventStreamSent);
16331
+ this.deliverOutsideConnectionContext(transport);
16279
16332
  this.connectingTransport = transport;
16280
16333
  try {
16281
16334
  this.assertSameLifecycle(generation);
@@ -16297,6 +16350,59 @@ var InternalMastraMCPClient = class extends _mastra_core_base.MastraBase {
16297
16350
  }
16298
16351
  }
16299
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
+ /**
16300
16406
  * Detaches whatever transport is still attached to the underlying SDK Client.
16301
16407
  *
16302
16408
  * The SDK assigns its internal `_transport` before `transport.start()` and never
@@ -16634,7 +16740,8 @@ var InternalMastraMCPClient = class extends _mastra_core_base.MastraBase {
16634
16740
  this.serverImplementation = void 0;
16635
16741
  }
16636
16742
  this.assertSameLifecycle(generation);
16637
- await this.connect(generation);
16743
+ const callContext = this.operationContextStore.getStore();
16744
+ await (callContext === void 0 ? this.connect(generation) : this.connectAsCall(callContext, () => this.connect(generation)));
16638
16745
  this.log("debug", "Successfully reconnected to MCP server");
16639
16746
  })();
16640
16747
  this.reconnectPromise = reconnectPromise;
@@ -17127,8 +17234,8 @@ var InternalMastraMCPClient = class extends _mastra_core_base.MastraBase {
17127
17234
  },
17128
17235
  ...tool.outputSchema ? { toModelOutput: createStructuredToolToModelOutput() } : {},
17129
17236
  execute: async (input, context) => {
17130
- if (serverMeta.connectFirst) await this.connect();
17131
17237
  const operationContext = context?.requestContext ?? null;
17238
+ if (serverMeta.connectFirst) await this.connectAsCall(operationContext, () => this.connect());
17132
17239
  return this.operationContextStore.run(operationContext, async () => {
17133
17240
  const suspendHost = context?.agent ?? context?.workflow;
17134
17241
  const toolCallSpan = context?.tracingContext?.currentSpan;
@@ -17228,7 +17335,26 @@ var InternalMastraMCPClient = class extends _mastra_core_base.MastraBase {
17228
17335
  try {
17229
17336
  return await executeToolCall();
17230
17337
  } catch (e) {
17231
- if (!(e instanceof _mastra_core_error.MastraError && (e.id === "MCP_CLIENT_TOOL_EXECUTION_FAILED" || e.id.startsWith("MCP_CLIENT_TOOL_TASK_"))) && isReconnectableMCPError(e)) {
17338
+ const isToolExecutionError = e instanceof _mastra_core_error.MastraError && (e.id === "MCP_CLIENT_TOOL_EXECUTION_FAILED" || e.id.startsWith("MCP_CLIENT_TOOL_TASK_"));
17339
+ if (!isToolExecutionError && isReconnectableMCPError(e) && !isUnsentRequestError(e)) {
17340
+ if (annotations?.readOnlyHint === true || annotations?.idempotentHint === true) {
17341
+ this.log("debug", `Retrying replay-safe tool ${tool.name} after a transport error`, { error: errorKind(e) });
17342
+ if (this.transport !== failedTransport || this.reconnectPromise) await this.reconnectAfterTransportFailure(failedTransport, lifecycleGeneration);
17343
+ return await executeToolCall();
17344
+ }
17345
+ this.log("error", `Lost the connection during tool ${tool.name}; it may have run`, { error: errorKind(e) });
17346
+ throw new _mastra_core_error.MastraError({
17347
+ id: "MCP_CLIENT_TOOL_CALL_OUTCOME_UNKNOWN",
17348
+ domain: _mastra_core_error.ErrorDomain.MCP,
17349
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
17350
+ 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.`,
17351
+ details: {
17352
+ toolName: tool.name,
17353
+ serverName: this.name
17354
+ }
17355
+ }, e);
17356
+ }
17357
+ if (!isToolExecutionError && isReconnectableMCPError(e)) {
17232
17358
  this.log("debug", `Transport error detected for tool ${tool.name}, attempting reconnection...`, { error: errorKind(e) });
17233
17359
  try {
17234
17360
  await this.reconnectAfterTransportFailure(failedTransport, lifecycleGeneration);
@@ -17670,12 +17796,12 @@ async function createOAuthCallbackServer(options) {
17670
17796
  },
17671
17797
  close() {
17672
17798
  settle({ error: /* @__PURE__ */ new Error("OAuth callback server closed before receiving an authorization code") });
17673
- server.closeIdleConnections();
17674
17799
  return new Promise((resolve, reject) => {
17675
17800
  server.close((error) => {
17676
17801
  if (error && error.code !== "ERR_SERVER_NOT_RUNNING") reject(error);
17677
17802
  else resolve();
17678
17803
  });
17804
+ server.closeAllConnections();
17679
17805
  });
17680
17806
  }
17681
17807
  };
@@ -22951,6 +23077,136 @@ function stepAt(workflow, path) {
22951
23077
  }
22952
23078
  return step;
22953
23079
  }
23080
+ // src/server/uri-template.ts
23081
+ /**
23082
+ * Whether `uri` is an RFC 6570 expansion of `template`. Decides membership only,
23083
+ * without extracting variables.
23084
+ *
23085
+ * The SDK's `UriTemplate.match` is a backtracking regex that takes minutes on a
23086
+ * few KB of client URI when variables are adjacent (`x://{a}-{b}-{c}-{d}`); this
23087
+ * walks the URI once per template segment instead.
23088
+ *
23089
+ * Plain `{x}` and `{+x}` must be non-empty, since an empty path segment never
23090
+ * names a resource. Outside `{+x}` and `{#x}`, values must be percent-encoded:
23091
+ * an ASCII reserved character other than `,` ends the value. Raw non-ASCII
23092
+ * characters are accepted because clients often send them unencoded.
23093
+ */
23094
+ function matchesUriTemplate(template, uri) {
23095
+ return advance(uri, /* @__PURE__ */ new Set([0]), parse(template)).has(uri.length);
23096
+ }
23097
+ const encoded = (char) => /[\w\-.~,]/.test(char) || char.charCodeAt(0) > 127;
23098
+ const anything = () => true;
23099
+ const literal = (text) => ({
23100
+ kind: "literal",
23101
+ text
23102
+ });
23103
+ const run = (allowed, min = 0) => ({
23104
+ kind: "run",
23105
+ allowed,
23106
+ min
23107
+ });
23108
+ const optional = (...segments) => ({
23109
+ kind: "optional",
23110
+ segments
23111
+ });
23112
+ const repeat = (...segments) => ({
23113
+ kind: "repeat",
23114
+ segments
23115
+ });
23116
+ const choice = (options) => ({
23117
+ kind: "choice",
23118
+ options
23119
+ });
23120
+ function parse(template) {
23121
+ const segments = [];
23122
+ let i = 0;
23123
+ while (i < template.length) {
23124
+ const open = template.indexOf("{", i);
23125
+ const close = open === -1 ? -1 : template.indexOf("}", open);
23126
+ if (close === -1) {
23127
+ segments.push(literal(template.slice(i)));
23128
+ break;
23129
+ }
23130
+ if (open > i) segments.push(literal(template.slice(i, open)));
23131
+ segments.push(...expression(template.slice(open + 1, close)));
23132
+ i = close + 1;
23133
+ }
23134
+ return segments;
23135
+ }
23136
+ function expression(body) {
23137
+ const operator = "+#./;?&".includes(body[0] ?? "") ? body[0] : "";
23138
+ const specs = body.slice(operator.length).split(",").map((spec) => ({
23139
+ name: spec.replace(/[*]|:\d*$/g, "").trim(),
23140
+ exploded: spec.includes("*")
23141
+ })).filter((spec) => spec.name);
23142
+ switch (operator) {
23143
+ case "": return [run(encoded, 1)];
23144
+ case "+": return [run(anything, 1)];
23145
+ case "#": return [optional(literal("#"), run(anything))];
23146
+ case "/": return specs.map(({ exploded }) => exploded ? repeat(literal("/"), run(encoded)) : optional(literal("/"), run(encoded)));
23147
+ case ".": return specs.map(() => optional(literal("."), run(encoded)));
23148
+ case ";": return specs.map(({ name, exploded }) => (exploded ? repeat : optional)(literal(`;${name}`), optional(literal("="), run(encoded))));
23149
+ default: {
23150
+ const pair = ({ name, exploded }, separator) => {
23151
+ const first = [literal(`${separator}${name}=`), run(encoded)];
23152
+ return exploded ? [...first, repeat(literal(`&${name}=`), run(encoded))] : first;
23153
+ };
23154
+ const firstPresent = specs.map((spec, i) => [...pair(spec, operator), ...specs.slice(i + 1).map((later) => optional(...pair(later, "&")))]);
23155
+ return [optional(choice(firstPresent))];
23156
+ }
23157
+ }
23158
+ }
23159
+ /** Every URI offset reachable after matching `segments` from any offset in `from`. */
23160
+ function advance(uri, from, segments) {
23161
+ let positions = from;
23162
+ for (const segment of segments) {
23163
+ if (positions.size === 0) break;
23164
+ positions = step(uri, positions, segment);
23165
+ }
23166
+ return positions;
23167
+ }
23168
+ function step(uri, positions, segment) {
23169
+ const next = /* @__PURE__ */ new Set();
23170
+ switch (segment.kind) {
23171
+ case "literal":
23172
+ for (const p of positions) if (uri.startsWith(segment.text, p)) next.add(p + segment.text.length);
23173
+ return next;
23174
+ case "choice":
23175
+ for (const option of segment.options) for (const p of advance(uri, positions, option)) next.add(p);
23176
+ return next;
23177
+ case "run": {
23178
+ let end = 0;
23179
+ for (const p of [...positions].sort((a, b) => a - b)) {
23180
+ const covered = p < end && next.has(p);
23181
+ if (segment.min === 0) next.add(p);
23182
+ if (covered) continue;
23183
+ end = p;
23184
+ for (let width = charWidth(uri, end, segment.allowed); width > 0; width = charWidth(uri, end, segment.allowed)) next.add(end += width);
23185
+ }
23186
+ return next;
23187
+ }
23188
+ case "optional":
23189
+ for (const p of positions) next.add(p);
23190
+ for (const p of advance(uri, positions, segment.segments)) next.add(p);
23191
+ return next;
23192
+ case "repeat": {
23193
+ for (const p of positions) next.add(p);
23194
+ let frontier = positions;
23195
+ while (frontier.size > 0) {
23196
+ const reached = [...advance(uri, frontier, segment.segments)].filter((p) => !next.has(p));
23197
+ for (const p of reached) next.add(p);
23198
+ frontier = new Set(reached);
23199
+ }
23200
+ return next;
23201
+ }
23202
+ }
23203
+ }
23204
+ /** Characters a run consumes at `i`: a whole `%HH` triplet, one allowed character, or none. */
23205
+ function charWidth(uri, i, allowed) {
23206
+ if (i >= uri.length) return 0;
23207
+ if (uri[i] === "%") return /^%[0-9A-Fa-f]{2}/.test(uri.slice(i, i + 3)) ? 3 : 0;
23208
+ return allowed(uri[i]) ? 1 : 0;
23209
+ }
22954
23210
  // src/server/server.ts
22955
23211
  const EMPTY_OBJECT_SCHEMA = {
22956
23212
  type: "object",
@@ -24323,10 +24579,24 @@ var MCPServer = class extends _mastra_core_mcp.MCPServerBase {
24323
24579
  extra: toToolExecutionContext(ctx, this.name).extra,
24324
24580
  requestContext
24325
24581
  }) ?? []];
24582
+ const findResource = async (uri, ctx, requestContext) => {
24583
+ const listed = (await listResources(ctx, requestContext)).find((r) => r.uri === uri);
24584
+ if (listed || !options?.resourceTemplates) return listed;
24585
+ const template = (await options.resourceTemplates({
24586
+ extra: toToolExecutionContext(ctx, this.name).extra,
24587
+ requestContext
24588
+ })).find((t) => matchesUriTemplate(t.uriTemplate, uri));
24589
+ return template && {
24590
+ uri,
24591
+ name: template.name,
24592
+ mimeType: template.mimeType,
24593
+ _meta: template._meta
24594
+ };
24595
+ };
24326
24596
  this.setTracedHandler(server, "resources/list", async (_request, ctx, trace) => ({ resources: await listResources(ctx, trace.requestContext) }));
24327
24597
  this.setTracedHandler(server, "resources/read", async (request, ctx, trace) => {
24328
24598
  const uri = request.params.uri;
24329
- const resource = (await listResources(ctx, trace.requestContext)).find((r) => r.uri === uri);
24599
+ const resource = await findResource(uri, ctx, trace.requestContext);
24330
24600
  if (!resource) throw new _modelcontextprotocol_server.ProtocolError(_modelcontextprotocol_server.ProtocolErrorCode.InvalidParams, `Resource not found: ${uri}`);
24331
24601
  const html = this.appResourceHtml.get(uri);
24332
24602
  if (html !== void 0) return { contents: [this.resourceContents(resource, { text: html })] };