@salesforce/sfdx-agent-harness-mastra 0.100.0 → 0.102.0

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/CHANGELOG.md CHANGED
@@ -3,6 +3,16 @@
3
3
  All notable changes to `@salesforce/sfdx-agent-harness-mastra` are documented in this file.
4
4
  Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
+ ## [0.102.0] - 2026-10-13
7
+
8
+ ### Features
9
+ - **deps**: upgrade dependencies and move the Mastra harness to @mastra/mcp 2.x @W-24334288@ ([#877](https://github.com/forcedotcom/agentic-dx/pull/877))
10
+
11
+ ## [0.101.0] - 2026-10-13
12
+
13
+ ### Features
14
+ - **harness-mastra**: migrate pass-throughs to LanguageModelV4 and remove chat-generations @W-24224624@ ([#876](https://github.com/forcedotcom/agentic-dx/pull/876))
15
+
6
16
  ## [0.100.0] - 2026-10-09
7
17
 
8
18
  ### Chores
package/README.md CHANGED
@@ -59,8 +59,8 @@ Linux and macOS release the handle eagerly; the retry options are no-ops on thos
59
59
  | ----------------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
60
60
  | `MastraHarnessFactory` | class | `HarnessFactory` that produces a Mastra-backed `AgentHarness` bound to a storage root. |
61
61
  | `MastraHarnessFactoryConfig` | interface | Configuration for `MastraHarnessFactory` (`toolApprovalTimeoutMs?`, `languageModelBuilders?`). |
62
- | `MastraLanguageModelBuilderContext` | type | Context bag the harness threads into each `LanguageModelV3` builder (`getInfo`, `logBus`, `emitWireCommunication`, `innerFetch`). |
63
- | `MastraLanguageModelBuilder` | type | Factory function shape for a consumer-injected builder: `(ctx: MastraLanguageModelBuilderContext) => LanguageModelV3`. |
62
+ | `MastraLanguageModelBuilderContext` | type | Context bag the harness threads into each `LanguageModelV4` builder (`getInfo`, `logBus`, `emitWireCommunication`, `innerFetch`). |
63
+ | `MastraLanguageModelBuilder` | type | Factory function shape for a consumer-injected builder: `(ctx: MastraLanguageModelBuilderContext) => LanguageModelV4`. |
64
64
  | `MASTRA_BUILT_IN_TOOL_POLICIES` | const | Frozen `ReadonlyArray<ToolPolicyRule>` the harness feeds the SDK's `resolveToolApprovalPolicy` as the `tiers.harness` slice — `allow` rules for Mastra's built-in `updateWorkingMemory` / `skill` / `skill_search` / `skill_read` tools. |
65
65
  | `MASTRA_BUILT_IN_TOOL_NAMES` | const | The four built-in tool leaf names `MASTRA_BUILT_IN_TOOL_POLICIES` covers. |
66
66
 
@@ -81,20 +81,14 @@ no `globalThis` mutation. When no proxy env is set, `fetch` runs unwrapped (zero
81
81
 
82
82
  #### Injecting custom wire shapes
83
83
 
84
- A consumer can register a `LanguageModelV3` builder for a custom `providerHint` via
84
+ A consumer can register a `LanguageModelV4` builder for a custom `providerHint` via
85
85
  `MastraHarnessFactoryConfig.languageModelBuilders` without editing the harness's internal switch. Each injected key
86
- becomes a supported provider hint and a dispatch arm. Used to inject wire shapes the harness does not ship natively
87
- (e.g., a temporary GovCloud `chat/generations` fallback).
86
+ becomes a supported provider hint and a dispatch arm. Used to inject wire shapes the harness does not ship natively.
88
87
 
89
88
  ```ts
90
- import {
91
- CHAT_GENERATIONS_PROVIDER_HINT,
92
- buildChatGenerationsLanguageModel,
93
- } from '@salesforce/sfdx-agent-chat-generations';
94
-
95
89
  const factory = new MastraHarnessFactory({
96
90
  languageModelBuilders: {
97
- [CHAT_GENERATIONS_PROVIDER_HINT]: buildChatGenerationsLanguageModel,
91
+ 'my-custom-wire-shape': (ctx) => buildMyLanguageModel(ctx),
98
92
  },
99
93
  });
100
94
  ```
package/dist/index.d.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  import type { LogBus } from '@salesforce/agentic-common';
2
2
  import type { ModelConnectivityInfo, WireCommunicationEvent } from '@salesforce/sfdx-agent-sdk';
3
- import type { LanguageModelV3 } from '@ai-sdk/provider';
3
+ import type { LanguageModelV4 } from '@ai-sdk/provider';
4
4
  export { MastraHarnessFactory, type MastraHarnessFactoryConfig } from './mastra-harness-factory.js';
5
5
  export type { MastraAgentHarness } from './mastra-harness.js';
6
6
  export { MASTRA_BUILT_IN_TOOL_NAMES, MASTRA_BUILT_IN_TOOL_POLICIES } from './mastra-built-in-policies.js';
7
7
  export { type MastraAgentConfig, type MastraSkillSearchConfig, type MastraToolSearchConfig, type SkillSearchHandle, type ToolSearchHandle, } from './tool-search.js';
8
8
  /**
9
- * Context the harness threads into each `LanguageModelV3` builder — both the
9
+ * Context the harness threads into each `LanguageModelV4` builder — both the
10
10
  * built-in builders and any consumer-injected builders registered via
11
11
  * `MastraHarnessFactoryConfig.languageModelBuilders`. The shape mirrors what
12
12
  * `MastraHarness.buildLanguageModel` currently hands its internal builders, so an
@@ -33,10 +33,10 @@ export type MastraLanguageModelBuilderContext = {
33
33
  innerFetch: typeof fetch;
34
34
  };
35
35
  /**
36
- * Factory function shape for a consumer-injected `LanguageModelV3` builder.
36
+ * Factory function shape for a consumer-injected `LanguageModelV4` builder.
37
37
  * Registered via `MastraHarnessFactoryConfig.languageModelBuilders` keyed by a custom
38
38
  * `providerHint`, used to inject wire shapes the harness does not ship natively
39
- * (e.g. a temporary GovCloud `chat/generations` fallback). The builder receives the
39
+ * (e.g. a bespoke gateway wire format). The builder receives the
40
40
  * same context bag the harness's built-in pass-through builders get, so an injected
41
41
  * builder can emit `WireCommunicationEvent`s and honor `HTTPS_PROXY` routing without
42
42
  * duplicating the harness's own fetch wrapper / wire-monitoring plumbing. The
@@ -55,4 +55,4 @@ export type MastraLanguageModelBuilderContext = {
55
55
  * });
56
56
  * ```
57
57
  */
58
- export type MastraLanguageModelBuilder = (ctx: MastraLanguageModelBuilderContext) => LanguageModelV3;
58
+ export type MastraLanguageModelBuilder = (ctx: MastraLanguageModelBuilderContext) => LanguageModelV4;
@@ -12,10 +12,10 @@ export interface MastraHarnessFactoryConfig {
12
12
  */
13
13
  toolApprovalTimeoutMs?: number;
14
14
  /**
15
- * Consumer-registered `LanguageModelV3` builders keyed by `providerHint`. Each key
15
+ * Consumer-registered `LanguageModelV4` builders keyed by `providerHint`. Each key
16
16
  * becomes a supported provider hint and a dispatch arm in `buildLanguageModel`. Used
17
- * to inject wire shapes the harness does not ship natively (e.g. a temporary
18
- * GovCloud `chat/generations` fallback path). Keys MUST NOT collide with the
17
+ * to inject wire shapes the harness does not ship natively (e.g. a bespoke
18
+ * gateway wire format). Keys MUST NOT collide with the
19
19
  * harness's built-in hints (`anthropic`, `bedrock-anthropic`, `openai`,
20
20
  * `openai-responses`, `openai-compatible`) — constructing with a colliding key
21
21
  * throws at factory construction time rather than silently shadowing the built-in.
@@ -5,10 +5,9 @@ import { McpServerStatus, type AgentHarness, type WithAgentConfig, type HarnessS
5
5
  import { type AgentHooks } from '@salesforce/sfdx-agent-sdk/harness';
6
6
  import type { MastraLanguageModelBuilderContext } from './index.js';
7
7
  import { MastraApprovalCoordinator } from './mastra-approval-coordinator.js';
8
- import { type HttpErrorCapture } from './mcp-config-mapper.js';
9
8
  import { type MastraAgentConfig, type SkillSearchHandle, type ToolSearchHandle } from './tool-search.js';
10
9
  import { type MultimodalFile } from '@salesforce/sfdx-agent-sdk';
11
- import type { LanguageModelV3 } from '@ai-sdk/provider';
10
+ import type { LanguageModelV4 } from '@ai-sdk/provider';
12
11
  import { Clock, type LogRecord, type BackoffRetryerDeps, type UniqueIDGenerator, type Unsubscribe } from '@salesforce/agentic-common';
13
12
  import { type ToolsInput } from './utils.js';
14
13
  import { Workspace } from '@mastra/core/workspace';
@@ -67,15 +66,6 @@ interface McpServerState {
67
66
  * on (tool merging, alwaysActive expansion, MCP-tool metadata lookup).
68
67
  */
69
68
  tools: ToolsInput;
70
- /**
71
- * HTTP-error capture slot for remote servers (W-23231658). The remote
72
- * transport fetch (built by `mapToMastraMCPServers`) writes the latest
73
- * non-2xx `{ status, body }` here; the discovery / reconnect catch reads
74
- * `latest` to recover the HTTP status `@mastra/mcp` discards when a
75
- * fallback-eligible connect fails. `undefined` for stdio servers and for
76
- * remote servers when no capture map was threaded in.
77
- */
78
- httpErrorCapture?: HttpErrorCapture;
79
69
  }
80
70
  /**
81
71
  * Internal state for a single agent. Holds Mastra-specific instances so the
@@ -247,13 +237,13 @@ export interface MastraHarnessOptions {
247
237
  */
248
238
  innerFetch?: typeof fetch;
249
239
  /**
250
- * Consumer-registered `LanguageModelV3` builders keyed by `providerHint`.
240
+ * Consumer-registered `LanguageModelV4` builders keyed by `providerHint`.
251
241
  * Forwarded from `MastraHarnessFactoryConfig.languageModelBuilders`. The
252
242
  * harness consults this map in `buildLanguageModel`'s `default` arm before
253
243
  * throwing `MODEL_NOT_SUPPORTED_BY_HARNESS`, so an injected builder can
254
244
  * handle a custom hint without editing the harness's internal switch.
255
245
  */
256
- languageModelBuilders?: Readonly<Record<string, (ctx: MastraLanguageModelBuilderContext) => LanguageModelV3>>;
246
+ languageModelBuilders?: Readonly<Record<string, (ctx: MastraLanguageModelBuilderContext) => LanguageModelV4>>;
257
247
  /**
258
248
  * Clock / sleep / RNG for the pre-first-token LLM retry loop (W-24236393), whose policy is
259
249
  * `PRE_FIRST_TOKEN_RETRY_OPTIONS`. Tests inject a no-sleep `sleep`.
@@ -514,7 +504,7 @@ export declare class MastraHarness implements MastraAgentHarness {
514
504
  * non-empty paths.
515
505
  */
516
506
  /**
517
- * Dispatches on the bag's `providerHint` and returns a `LanguageModelV3` that
507
+ * Dispatches on the bag's `providerHint` and returns a `LanguageModelV4` that
518
508
  * speaks the provider-native pass-through endpoint. The builder closes over
519
509
  * `getInfo` so within-shape `modelId` swaps, JWT rotations, and
520
510
  * premium-rate-limit gate flips land on the next outbound call without
@@ -588,8 +578,8 @@ export declare class MastraHarness implements MastraAgentHarness {
588
578
  * `agent.stream()` accepts.
589
579
  *
590
580
  * `text` parts collapse into text content blocks; `image` / `file` parts become v5 `file`
591
- * content blocks (`mediaType` + base64 `data` + optional `filename`) that Mastra forwards to the
592
- * gateway bridge, which collects them into `ChatMessageIn.files[]`. Pre-stream validation and the
581
+ * content blocks (`mediaType` + base64 `data` + optional `filename`) that Mastra normalizes to V4
582
+ * `file` parts for the provider pass-through translators. Pre-stream validation and the
593
583
  * input-part guard live in the SDK's {@link lowerStreamInput}, so file caps and the
594
584
  * `MULTIMODAL_NOT_SUPPORTED` / `INVALID_MESSAGE_CONTENT` semantics are identical to the Claude
595
585
  * harness; this method only supplies the Mastra-specific lowering of each part.
@@ -737,6 +727,14 @@ export declare class MastraHarness implements MastraAgentHarness {
737
727
  * after `shutdown()` has disposed the buses.
738
728
  */
739
729
  private prepareMcpStatusChange;
730
+ /**
731
+ * Surface the one MCP option this harness cannot honor. `@mastra/mcp` 2.x speaks the
732
+ * stateless 2026-07-28 protocol, has no SSE stream to resume, and dropped
733
+ * `reconnectionOptions`, so the mapper does not forward it; the Claude harness still does.
734
+ * Without this a consumer's tuning would silently stop applying on Mastra, so warn once per
735
+ * server definition that is (re)built.
736
+ */
737
+ private warnIfReconnectionOptionsIgnored;
740
738
  /**
741
739
  * Issue #591: install a `progress.onUpdate(serverName, ...)` listener
742
740
  * on the MCP client. Incoming `notifications/progress` are dispatched
@@ -21,13 +21,13 @@ import { isStandardSchemaWithJSON, standardSchemaToJSONSchema } from '@mastra/sc
21
21
  import { SessionContextStore, sortJsonValue } from './session-context-store.js';
22
22
  import { mapToolDefinitionsToMastraTools } from './mastra-tool-mapper.js';
23
23
  import { hasEnabledServers, mapToMastraMCPServers } from './mcp-config-mapper.js';
24
- import { buildMcpErrorDetail, sanitizeMcpErrorMessage, synthesizeHttpTransportError } from './mcp-error-classifier.js';
24
+ import { buildMcpErrorDetail, errorFromDiscoveryDetails, sanitizeMcpErrorMessage } from './mcp-error-classifier.js';
25
25
  import { partitionPool, resolveAlwaysActive, } from './tool-search.js';
26
26
  import { validateMultimodalFiles } from '@salesforce/sfdx-agent-sdk';
27
27
  import { buildAnthropicDirectLanguageModel, buildAnthropicLanguageModel } from './pass-through-anthropic.js';
28
28
  import { buildOpenAIChatCompletionsLanguageModel } from './pass-through-openai-chat-completions.js';
29
29
  import { buildOpenAILanguageModel } from './pass-through-openai.js';
30
- import { readToolArgsRetryScope, TOOL_ARGS_RETRY_SCOPE_AGENT_ID, TOOL_ARGS_RETRY_SCOPE_PROVIDER, TOOL_ARGS_RETRY_SCOPE_THREAD_ID, } from './v3-stream-mapping.js';
30
+ import { readToolArgsRetryScope, TOOL_ARGS_RETRY_SCOPE_AGENT_ID, TOOL_ARGS_RETRY_SCOPE_PROVIDER, TOOL_ARGS_RETRY_SCOPE_THREAD_ID, } from './stream-mapping.js';
31
31
  import { ToolResultRedactionProcessor } from './mastra-tool-result-redaction-processor.js';
32
32
  import { backfillCreatedAt, buildSummaryPrompt, Clock, getErrorMessage, RealClock, splitFrontmatterAndBody, UUIDGenerator, } from '@salesforce/agentic-common';
33
33
  import { withPreFirstTokenRetry } from './retry-pre-first-token.js';
@@ -192,6 +192,33 @@ const DEFAULT_MCP_TIMEOUT_MS = 600_000;
192
192
  * factory/consumer knob — same posture as `resumeRpcTimeoutMs`, 6c).
193
193
  */
194
194
  const DEFAULT_IDLE_READ_TIMEOUT_MS = 120_000;
195
+ // Issue #529 step 2c: the pre-#529 `ActiveRun` interface has been collapsed
196
+ // into `MastraApprovalCoordinator`. The coordinator owns the per-turn pump
197
+ // loop, the multi-source `fullStream` rotation, the per-tool-callId approval
198
+ // timeouts, and the consumer-tool parker — all of which previously lived on
199
+ // `ActiveRun` + `MastraStreamBroker` as two parallel state machines for the
200
+ // same `(agentId, threadId)`. See `mastra-approval-coordinator.ts`.
201
+ /**
202
+ * Resolve a failed MCP connect (the discovery catch and the `reconnectServer` catch share this) into what the
203
+ * harness records and emits: the error to report, its sanitized one-line message, and the structured
204
+ * {@link McpServerErrorDetail}. A server `@mastra/mcp` reports as `needs-auth` is surfaced as an OAuth
205
+ * requirement (`oauth-required`, non-retriable) regardless of the transport error it produced.
206
+ *
207
+ * The telemetry / log / status-change emits around the call differ between the two sites and stay at the
208
+ * call sites on purpose.
209
+ */
210
+ function resolveMcpFailure(client, serverName, err) {
211
+ const oauthRequired = client.getServerAuthState(serverName) === 'needs-auth';
212
+ const effectiveError = oauthRequired
213
+ ? new (class UnauthorizedError extends Error {
214
+ })(sanitizeMcpErrorMessage(err))
215
+ : err;
216
+ return {
217
+ effectiveError,
218
+ message: sanitizeMcpErrorMessage(effectiveError),
219
+ errorDetail: buildMcpErrorDetail(effectiveError),
220
+ };
221
+ }
195
222
  /**
196
223
  * Build the `Map<namespacedName, ToolSource>` index `resolveAlwaysActive`
197
224
  * needs to match `{ serverName }` / `{ serverName, toolName }` / `{ toolName }`
@@ -618,12 +645,9 @@ export class MastraHarness {
618
645
  const mcpAuthProviders = new Map(Object.entries(options?.mcpAuthProviders ?? {}).filter(([name]) => config.mcpServers?.[name]?.enabled !== false));
619
646
  if (hasEnabledServers(config.mcpServers)) {
620
647
  options?.abortSignal?.throwIfAborted();
621
- // Empty capture map; the mapper creates a per-remote-server capture
622
- // object and stores it here so we can attach each to its
623
- // McpServerState below (W-23231658).
624
- const httpErrorCaptures = new Map();
625
- const enabledServers = mapToMastraMCPServers(config.mcpServers, config.orgJwt, this.innerFetch, httpErrorCaptures, options?.mcpAuthProviders, config.name);
648
+ const enabledServers = mapToMastraMCPServers(config.mcpServers, config.orgJwt, this.innerFetch, options?.mcpAuthProviders, config.name);
626
649
  for (const [serverName, serverDef] of Object.entries(enabledServers)) {
650
+ this.warnIfReconnectionOptionsIgnored(agentId, serverName, config.mcpServers[serverName]);
627
651
  const client = new MCPClient({
628
652
  id: `${agentId}:${serverName}`,
629
653
  servers: { [serverName]: serverDef },
@@ -640,7 +664,6 @@ export class MastraHarness {
640
664
  ready: Promise.resolve(),
641
665
  settled: false,
642
666
  tools: {},
643
- httpErrorCapture: httpErrorCaptures.get(serverName),
644
667
  };
645
668
  mcpServerStates.set(serverName, entry);
646
669
  entry.ready = this.runMcpDiscovery(agentId, serverName, client, mcpServerStates, 0, options?.abortSignal);
@@ -955,11 +978,6 @@ export class MastraHarness {
955
978
  agentId,
956
979
  serverName,
957
980
  });
958
- // W-23231658: clear any stale HTTP-error capture so a prior failure's
959
- // status can't be mistaken for this reconnect's. The wrapper repopulates
960
- // `latest` only if this reconnect's connect sees a non-2xx response.
961
- if (serverState.httpErrorCapture)
962
- serverState.httpErrorCapture.latest = undefined;
963
981
  // Transport-level reconnect can throw (stdio spawn failure, remote handshake
964
982
  // failure). Record on serverState and emit only `failed` (not started+failed
965
983
  // — `started` already fired above). This keeps the public contract
@@ -971,19 +989,12 @@ export class MastraHarness {
971
989
  // Drop telemetry + state writes if a newer epoch has superseded us.
972
990
  if (state.mcpServers.get(serverName)?.epoch !== epoch)
973
991
  return;
974
- // Prefer the captured HTTP status + body over the bodyless generic
975
- // error `@mastra/mcp` rethrows on a fallback-eligible connect
976
- // failure (W-23231658); same recovery as the discovery catch.
977
- const captured = serverState.httpErrorCapture?.latest;
978
- const oauthRequired = serverState.client.getServerAuthState(serverName) === 'needs-auth';
979
- const effectiveError = oauthRequired
980
- ? new (class UnauthorizedError extends Error {
981
- })(sanitizeMcpErrorMessage(err))
982
- : captured
983
- ? synthesizeHttpTransportError(captured.status, captured.body, err)
984
- : err;
985
- const message = sanitizeMcpErrorMessage(effectiveError);
986
- const errorDetail = buildMcpErrorDetail(effectiveError);
992
+ // `reconnectServer` rethrows the live error: the raw `SdkHttpError` /
993
+ // `SdkError` for a connected client, or a `MastraError` wrapping it once a
994
+ // prior connect failed. Either way the HTTP status / error code is on the
995
+ // error or its `cause` chain (W-23231658), which `buildMcpErrorDetail`
996
+ // reads directly.
997
+ const { effectiveError, message, errorDetail } = resolveMcpFailure(serverState.client, serverName, err);
987
998
  serverState.error = message;
988
999
  serverState.errorDetail = errorDetail;
989
1000
  serverState.settled = true;
@@ -1104,9 +1115,8 @@ export class MastraHarness {
1104
1115
  ...state.mcpAuthProviders.keys(),
1105
1116
  ...Object.keys(nextProviders),
1106
1117
  ]);
1107
- const nextHttpErrorCaptures = new Map();
1108
1118
  const nextEnabledServers = hasEnabledServers(nextConfig)
1109
- ? mapToMastraMCPServers(nextConfig, config.orgJwt, this.innerFetch, nextHttpErrorCaptures, nextProviders, config.name)
1119
+ ? mapToMastraMCPServers(nextConfig, config.orgJwt, this.innerFetch, nextProviders, config.name)
1110
1120
  : {};
1111
1121
  for (const name of allNames) {
1112
1122
  const prev = prevConfig[name];
@@ -1128,6 +1138,7 @@ export class MastraHarness {
1128
1138
  if (next && next.enabled !== false) {
1129
1139
  const serverDef = nextEnabledServers[name];
1130
1140
  if (serverDef) {
1141
+ this.warnIfReconnectionOptionsIgnored(agentId, name, next);
1131
1142
  const client = new MCPClient({
1132
1143
  id: `${agentId}:${name}`,
1133
1144
  servers: { [name]: serverDef },
@@ -1140,7 +1151,6 @@ export class MastraHarness {
1140
1151
  ready: Promise.resolve(),
1141
1152
  settled: false,
1142
1153
  tools: {},
1143
- httpErrorCapture: nextHttpErrorCaptures.get(name),
1144
1154
  };
1145
1155
  state.mcpServers.set(name, entry);
1146
1156
  if (nextProvider !== undefined) {
@@ -1550,7 +1560,7 @@ export class MastraHarness {
1550
1560
  * non-empty paths.
1551
1561
  */
1552
1562
  /**
1553
- * Dispatches on the bag's `providerHint` and returns a `LanguageModelV3` that
1563
+ * Dispatches on the bag's `providerHint` and returns a `LanguageModelV4` that
1554
1564
  * speaks the provider-native pass-through endpoint. The builder closes over
1555
1565
  * `getInfo` so within-shape `modelId` swaps, JWT rotations, and
1556
1566
  * premium-rate-limit gate flips land on the next outbound call without
@@ -1671,9 +1681,9 @@ export class MastraHarness {
1671
1681
  }));
1672
1682
  default: {
1673
1683
  // Consult the injected builders map before throwing. This is the
1674
- // extension point for consumer-registered wire shapes (e.g. a temporary
1675
- // GovCloud chat/generations fallback, W-23560954). The builder receives
1676
- // the same context bag the built-in builders get.
1684
+ // extension point for consumer-registered wire shapes (e.g. a bespoke
1685
+ // gateway wire format). The builder receives the same context bag the
1686
+ // built-in builders get.
1677
1687
  const custom = this.languageModelBuilders?.[initial.providerHint];
1678
1688
  if (custom) {
1679
1689
  return this.wrapLanguageModel(custom({ getInfo, logBus, emitWireCommunication, innerFetch }));
@@ -1773,8 +1783,8 @@ export class MastraHarness {
1773
1783
  * `agent.stream()` accepts.
1774
1784
  *
1775
1785
  * `text` parts collapse into text content blocks; `image` / `file` parts become v5 `file`
1776
- * content blocks (`mediaType` + base64 `data` + optional `filename`) that Mastra forwards to the
1777
- * gateway bridge, which collects them into `ChatMessageIn.files[]`. Pre-stream validation and the
1786
+ * content blocks (`mediaType` + base64 `data` + optional `filename`) that Mastra normalizes to V4
1787
+ * `file` parts for the provider pass-through translators. Pre-stream validation and the
1778
1788
  * input-part guard live in the SDK's {@link lowerStreamInput}, so file caps and the
1779
1789
  * `MULTIMODAL_NOT_SUPPORTED` / `INVALID_MESSAGE_CONTENT` semantics are identical to the Claude
1780
1790
  * harness; this method only supplies the Mastra-specific lowering of each part.
@@ -2295,6 +2305,28 @@ export class MastraHarness {
2295
2305
  }
2296
2306
  };
2297
2307
  }
2308
+ /**
2309
+ * Surface the one MCP option this harness cannot honor. `@mastra/mcp` 2.x speaks the
2310
+ * stateless 2026-07-28 protocol, has no SSE stream to resume, and dropped
2311
+ * `reconnectionOptions`, so the mapper does not forward it; the Claude harness still does.
2312
+ * Without this a consumer's tuning would silently stop applying on Mastra, so warn once per
2313
+ * server definition that is (re)built.
2314
+ */
2315
+ warnIfReconnectionOptionsIgnored(agentId, serverName, serverConfig) {
2316
+ if (serverConfig?.type !== 'remote' || serverConfig.reconnectionOptions === undefined)
2317
+ return;
2318
+ this.buses.emitLog({
2319
+ level: 'warn',
2320
+ message: `MCP server "${serverName}": reconnectionOptions is ignored by the Mastra harness`,
2321
+ timestamp: this.clock.now(),
2322
+ context: {
2323
+ event_type: 'mcp-reconnection-options-ignored',
2324
+ agentId,
2325
+ serverName,
2326
+ reason: '@mastra/mcp 2.x is stateless and has no reconnection options; only the Claude harness honors them',
2327
+ },
2328
+ });
2329
+ }
2298
2330
  /**
2299
2331
  * Issue #591: install a `progress.onUpdate(serverName, ...)` listener
2300
2332
  * on the MCP client. Incoming `notifications/progress` are dispatched
@@ -2384,18 +2416,14 @@ export class MastraHarness {
2384
2416
  const s = serverStates.get(serverName);
2385
2417
  return s && s.epoch === epoch ? s : undefined;
2386
2418
  };
2387
- // W-23231658: clear any HTTP-error capture left from a prior discovery
2388
- // run on this (reused) client/capture so a stale status can't bleed into
2389
- // this run's failure classification. The wrapper repopulates `latest`
2390
- // only if a non-2xx response actually occurs during this connect.
2391
- const captureSlot = serverStates.get(serverName)?.httpErrorCapture;
2392
- if (captureSlot)
2393
- captureSlot.latest = undefined;
2394
2419
  return makeAbortable(client.listToolsetsWithErrors(), abortSignal)
2395
- .then(({ toolsets, errors }) => {
2420
+ .then(({ toolsets, errors, errorDetails }) => {
2396
2421
  const serverError = errors[serverName];
2397
2422
  if (serverError) {
2398
- throw new Error(serverError);
2423
+ // `errorDetails` keeps the HTTP status and deepest error code that the
2424
+ // legacy per-server string drops (W-23231658); stubs without it fall
2425
+ // back to the bare message.
2426
+ throw errorFromDiscoveryDetails(errorDetails?.[serverName] ?? { message: serverError });
2399
2427
  }
2400
2428
  const tools = toolsets[serverName] ?? {};
2401
2429
  const namespacedTools = {};
@@ -2461,23 +2489,7 @@ export class MastraHarness {
2461
2489
  // terminal telemetry — see the matching comment in the
2462
2490
  // success path above (issue #373).
2463
2491
  //
2464
- // W-23231658: when the remote transport captured a non-2xx
2465
- // HTTP response, prefer it over the bodyless generic error
2466
- // `@mastra/mcp` rethrows. The capture is only ever populated
2467
- // when a real HTTP error occurred on this server's transport,
2468
- // so by construction it IS the (or a) cause of this failure;
2469
- // the original `err` rides along as `cause` so its stack is
2470
- // preserved on the failed event.
2471
- const captured = s.httpErrorCapture?.latest;
2472
- const oauthRequired = s.client.getServerAuthState(serverName) === 'needs-auth';
2473
- const effectiveError = oauthRequired
2474
- ? new (class UnauthorizedError extends Error {
2475
- })(sanitizeMcpErrorMessage(err))
2476
- : captured
2477
- ? synthesizeHttpTransportError(captured.status, captured.body, err)
2478
- : err;
2479
- const errorMessage = sanitizeMcpErrorMessage(effectiveError);
2480
- const errorDetail = buildMcpErrorDetail(effectiveError);
2492
+ const { effectiveError, message: errorMessage, errorDetail, } = resolveMcpFailure(s.client, serverName, err);
2481
2493
  s.error = errorMessage;
2482
2494
  s.errorDetail = errorDetail;
2483
2495
  s.settled = true;
@@ -6,23 +6,6 @@ import { type McpAuthProviders, type MCPConfiguration } from '@salesforce/sfdx-a
6
6
  * A server is considered enabled when its `enabled` property is `true` or absent.
7
7
  */
8
8
  export declare function hasEnabledServers(config: MCPConfiguration | undefined): config is MCPConfiguration;
9
- /** A captured non-2xx HTTP response from a remote MCP transport request. */
10
- export type CapturedHttpError = {
11
- status: number;
12
- body: string;
13
- };
14
- /**
15
- * Per-server mutable slot the remote-MCP fetch wrapper writes the latest
16
- * non-2xx response into. The harness reads `latest` in its discovery /
17
- * reconnect catch blocks to recover the HTTP status + body that
18
- * `@mastra/mcp`'s `connectHttp` discards when a fallback-eligible status
19
- * (`400` / `404` / `405`) triggers the SSE fallback and both transports fail —
20
- * the thrown `Error("Could not connect to server with any available HTTP
21
- * transport")` carries no status, no body, and no `cause` (W-23231658, #617).
22
- */
23
- export type HttpErrorCapture = {
24
- latest?: CapturedHttpError;
25
- };
26
9
  /**
27
10
  * Converts our {@link MCPConfiguration} into the `servers` map expected by Mastra's `MCPClient`.
28
11
  *
@@ -34,10 +17,13 @@ export type HttpErrorCapture = {
34
17
  * Mastra's `timeout` (post-connect request lifetime) and `connectTimeout` (initial transport
35
18
  * handshake) so the configured value covers both phases — Mastra's hardcoded 3s
36
19
  * `connectTimeout` default would otherwise abort cold-start handshakes before `timeout` applied.
37
- * - Per-server `reconnectionOptions` is forwarded to Mastra's `HttpServerDefinition.reconnectionOptions`
38
- * (typed as `StreamableHTTPClientTransportOptions['reconnectionOptions']`). Unspecified fields
39
- * are filled from {@link DEFAULT_RECONNECTION_OPTIONS} so partial overrides leave the rest at
40
- * the MCP SDK's built-in defaults.
20
+ * - Every definition carries `clientInfo.name` from {@link resolveMcpClientName} (agent name, else
21
+ * JWT feature id, else the SDK default — never the server-map key `@mastra/mcp` would default
22
+ * to). Mastra sends it on `server/discover`, on every modern-era request, and on a legacy
23
+ * `initialize`, for stdio and remote servers alike.
24
+ * - `MCPRemoteServerConfig.reconnectionOptions` is deliberately NOT forwarded: `@mastra/mcp` 2.x
25
+ * speaks the stateless 2026-07-28 protocol, has no SSE stream to resume, and dropped the option.
26
+ * The Claude harness still honors it. See the compile-time guard below.
41
27
  * - A supplied runtime OAuth provider is forwarded to the remote transport with the
42
28
  * proxy-aware `innerFetch`; without one, `orgJwt` provides the existing JWT fetch.
43
29
  */
@@ -48,15 +34,6 @@ export declare function mapToMastraMCPServers(config: MCPConfiguration, orgJwt?:
48
34
  * here so MCP remote transports honor `HTTPS_PROXY` without `globalThis`
49
35
  * mutation. Stdio transports ignore it (they're child processes).
50
36
  */
51
- innerFetch?: typeof fetch,
52
- /**
53
- * Optional per-server HTTP-error capture map. When supplied, every remote
54
- * server's transport fetch is wrapped so the latest non-2xx `{ status, body }`
55
- * is recorded into `captures.get(serverName)`. The harness reads this in its
56
- * discovery / reconnect catch to recover the status `@mastra/mcp` discards
57
- * (W-23231658). Stdio servers contribute no entry. Omitted in call sites
58
- * that don't need recovery (e.g. unit tests of the mapping shape).
59
- */
60
- captures?: Map<string, HttpErrorCapture>, providers?: McpAuthProviders,
37
+ innerFetch?: typeof fetch, providers?: McpAuthProviders,
61
38
  /** AgentConfig.name; see {@link resolveMcpClientName}. */
62
39
  agentName?: string): Record<string, MastraMCPServerDefinition>;