@salesforce/sfdx-agent-harness-mastra 0.49.0 → 0.51.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,17 @@
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.51.0] - 2026-08-24
7
+
8
+ ### Features
9
+ - **agent-sdk,harness-claude,harness-mastra,harness-openai**: support OAuth authProvider on remote MCP servers @W-23939243@ ([#757](https://github.com/forcedotcom/agentic-dx/pull/757))
10
+
11
+ ## [0.50.0] - 2026-08-18
12
+
13
+ ### Chores
14
+ - **deps-dev**: bump the eslint group across 1 directory with 2 updates ([#754](https://github.com/forcedotcom/agentic-dx/pull/754))
15
+ - **deps-dev**: bump the dev-dependencies group with 2 updates ([#753](https://github.com/forcedotcom/agentic-dx/pull/753))
16
+
6
17
  ## [0.49.0] - 2026-08-17
7
18
 
8
19
  ### Features
@@ -1,7 +1,7 @@
1
1
  import { Agent } from '@mastra/core/agent';
2
2
  import { SkillSearchProcessor, ToolSearchProcessor } from '@mastra/core/processors';
3
3
  import { MCPClient } from '@mastra/mcp';
4
- import { McpServerStatus, type AgentHarness, type WithAgentConfig, type ChatStreamResult, type HarnessAgentConfig, type MCPConfiguration, type McpServerErrorDetail, type McpServerInfo, type Message, type MessagePart, type ModelConnectivityInfo, type SessionContext, type StreamOptions, type TelemetryEventCallback, type ToolResultInfo, type WireCommunicationEventCallback } from '@salesforce/sfdx-agent-sdk';
4
+ import { McpServerStatus, type AgentHarness, type WithAgentConfig, type ChatStreamResult, type HarnessAgentConfig, type MCPConfiguration, type McpServerErrorDetail, type McpServerInfo, type Message, type MessagePart, type McpAuthProviders, type ModelConnectivityInfo, type SessionContext, type StreamOptions, type TelemetryEventCallback, type ToolResultInfo, type WireCommunicationEventCallback } from '@salesforce/sfdx-agent-sdk';
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';
@@ -108,6 +108,7 @@ interface MastraAgentState {
108
108
  mcpServers: Map<string, McpServerState>;
109
109
  /** Original MCP configuration, retained so getMcpServerInfo can report disabled servers. */
110
110
  mcpConfig?: MCPConfiguration;
111
+ mcpAuthProviders: Map<string, McpAuthProviders[string]>;
111
112
  /** Mastra Workspace providing built-in file tools to the agent. */
112
113
  mastraWorkspace?: Workspace;
113
114
  /**
@@ -319,6 +320,7 @@ export declare class MastraHarness implements MastraAgentHarness {
319
320
  createAgent(agentId: string, projectRoot: string, modelConnectivityInfo: ModelConnectivityInfo, config?: HarnessAgentConfig, options?: {
320
321
  abortSignal?: AbortSignal;
321
322
  hooks?: AgentHooks;
323
+ mcpAuthProviders?: McpAuthProviders;
322
324
  }): Promise<void>;
323
325
  /**
324
326
  * @requirements
@@ -366,6 +368,7 @@ export declare class MastraHarness implements MastraAgentHarness {
366
368
  updateAgent(agentId: string, modelConnectivityInfo: ModelConnectivityInfo, config?: HarnessAgentConfig, options?: {
367
369
  abortSignal?: AbortSignal;
368
370
  hooks?: AgentHooks;
371
+ mcpAuthProviders?: McpAuthProviders;
369
372
  }): Promise<void>;
370
373
  /**
371
374
  * @requirements
@@ -510,13 +510,14 @@ export class MastraHarness {
510
510
  const composedInstructions = await composeInstructionsWithRules(config.instructions, config.rules);
511
511
  // ── MCP setup (non-blocking, per-server) ──────────────────────────
512
512
  const mcpServerStates = new Map();
513
+ const mcpAuthProviders = new Map(Object.entries(options?.mcpAuthProviders ?? {}).filter(([name]) => config.mcpServers?.[name]?.enabled !== false));
513
514
  if (hasEnabledServers(config.mcpServers)) {
514
515
  options?.abortSignal?.throwIfAborted();
515
516
  // Empty capture map; the mapper creates a per-remote-server capture
516
517
  // object and stores it here so we can attach each to its
517
518
  // McpServerState below (W-23231658).
518
519
  const httpErrorCaptures = new Map();
519
- const enabledServers = mapToMastraMCPServers(config.mcpServers, config.orgJwt, this.innerFetch, httpErrorCaptures);
520
+ const enabledServers = mapToMastraMCPServers(config.mcpServers, config.orgJwt, this.innerFetch, httpErrorCaptures, options?.mcpAuthProviders);
520
521
  for (const [serverName, serverDef] of Object.entries(enabledServers)) {
521
522
  const client = new MCPClient({
522
523
  id: `${agentId}:${serverName}`,
@@ -694,6 +695,7 @@ export class MastraHarness {
694
695
  validateFiles,
695
696
  mcpServers: mcpServerStates,
696
697
  mcpConfig: config.mcpServers,
698
+ mcpAuthProviders,
697
699
  mastraWorkspace,
698
700
  coordinators: new Map(),
699
701
  consumerToolNames,
@@ -866,7 +868,13 @@ export class MastraHarness {
866
868
  // error `@mastra/mcp` rethrows on a fallback-eligible connect
867
869
  // failure (W-23231658); same recovery as the discovery catch.
868
870
  const captured = serverState.httpErrorCapture?.latest;
869
- const effectiveError = captured ? synthesizeHttpTransportError(captured.status, captured.body, err) : err;
871
+ const oauthRequired = serverState.client.getServerAuthState(serverName) === 'needs-auth';
872
+ const effectiveError = oauthRequired
873
+ ? new (class UnauthorizedError extends Error {
874
+ })(sanitizeMcpErrorMessage(err))
875
+ : captured
876
+ ? synthesizeHttpTransportError(captured.status, captured.body, err)
877
+ : err;
870
878
  const message = sanitizeMcpErrorMessage(effectiveError);
871
879
  const errorDetail = buildMcpErrorDetail(effectiveError);
872
880
  serverState.error = message;
@@ -982,16 +990,23 @@ export class MastraHarness {
982
990
  // ── Phase 1: MCP diff against state.mcpConfig ─────────────────────
983
991
  const prevConfig = state.mcpConfig ?? {};
984
992
  const nextConfig = config.mcpServers ?? {};
985
- const allNames = new Set([...Object.keys(prevConfig), ...Object.keys(nextConfig)]);
993
+ const nextProviders = options?.mcpAuthProviders ?? {};
994
+ const allNames = new Set([
995
+ ...Object.keys(prevConfig),
996
+ ...Object.keys(nextConfig),
997
+ ...state.mcpAuthProviders.keys(),
998
+ ...Object.keys(nextProviders),
999
+ ]);
986
1000
  const nextHttpErrorCaptures = new Map();
987
1001
  const nextEnabledServers = hasEnabledServers(nextConfig)
988
- ? mapToMastraMCPServers(nextConfig, config.orgJwt, this.innerFetch, nextHttpErrorCaptures)
1002
+ ? mapToMastraMCPServers(nextConfig, config.orgJwt, this.innerFetch, nextHttpErrorCaptures, nextProviders)
989
1003
  : {};
990
1004
  for (const name of allNames) {
991
1005
  const prev = prevConfig[name];
992
1006
  const next = nextConfig[name];
993
- if (mcpServerConfigEqual(prev, next))
1007
+ if (mcpServerConfigEqual(prev, next) && state.mcpAuthProviders.get(name) === nextProviders[name])
994
1008
  continue;
1009
+ const nextProvider = nextProviders[name];
995
1010
  // Tear down whatever was previously attached for this name. Both
996
1011
  // "removed" and "changed" go through this branch; "added" hits no
997
1012
  // previous entry so the disconnect is a no-op.
@@ -1001,6 +1016,7 @@ export class MastraHarness {
1001
1016
  await prevEntry.ready.catch(() => undefined);
1002
1017
  await prevEntry.client.disconnect().catch(() => undefined);
1003
1018
  }
1019
+ state.mcpAuthProviders.delete(name);
1004
1020
  // Connect the new one if the next config has it enabled.
1005
1021
  if (next && next.enabled !== false) {
1006
1022
  const serverDef = nextEnabledServers[name];
@@ -1020,6 +1036,9 @@ export class MastraHarness {
1020
1036
  httpErrorCapture: nextHttpErrorCaptures.get(name),
1021
1037
  };
1022
1038
  state.mcpServers.set(name, entry);
1039
+ if (nextProvider !== undefined) {
1040
+ state.mcpAuthProviders.set(name, nextProvider);
1041
+ }
1023
1042
  entry.ready = this.runMcpDiscovery(agentId, name, client, state.mcpServers, 0, options?.abortSignal);
1024
1043
  // Issue #591: same MCP-progress wiring as createAgent path.
1025
1044
  this.registerMcpProgressListener(agentId, name, client);
@@ -2042,9 +2061,13 @@ export class MastraHarness {
2042
2061
  // the original `err` rides along as `cause` so its stack is
2043
2062
  // preserved on the failed event.
2044
2063
  const captured = s.httpErrorCapture?.latest;
2045
- const effectiveError = captured
2046
- ? synthesizeHttpTransportError(captured.status, captured.body, err)
2047
- : err;
2064
+ const oauthRequired = s.client.getServerAuthState(serverName) === 'needs-auth';
2065
+ const effectiveError = oauthRequired
2066
+ ? new (class UnauthorizedError extends Error {
2067
+ })(sanitizeMcpErrorMessage(err))
2068
+ : captured
2069
+ ? synthesizeHttpTransportError(captured.status, captured.body, err)
2070
+ : err;
2048
2071
  const errorMessage = sanitizeMcpErrorMessage(effectiveError);
2049
2072
  const errorDetail = buildMcpErrorDetail(effectiveError);
2050
2073
  s.error = errorMessage;
@@ -1,6 +1,6 @@
1
1
  import type { MastraMCPServerDefinition } from '@mastra/mcp';
2
2
  import type { JSONWebToken } from '@salesforce/agentic-common';
3
- import { type MCPConfiguration } from '@salesforce/sfdx-agent-sdk';
3
+ import { type McpAuthProviders, type MCPConfiguration } from '@salesforce/sfdx-agent-sdk';
4
4
  /**
5
5
  * Returns `true` if `config` contains at least one server that is not explicitly disabled.
6
6
  * A server is considered enabled when its `enabled` property is `true` or absent.
@@ -38,8 +38,8 @@ export type HttpErrorCapture = {
38
38
  * (typed as `StreamableHTTPClientTransportOptions['reconnectionOptions']`). Unspecified fields
39
39
  * are filled from {@link DEFAULT_RECONNECTION_OPTIONS} so partial overrides leave the rest at
40
40
  * the MCP SDK's built-in defaults.
41
- * - When `orgJwt` is provided, remote servers get a `fetch` that delegates to
42
- * {@link resolveMcpServerHeaders} per-request to inject auth for Salesforce Platform URLs.
41
+ * - A supplied runtime OAuth provider is forwarded to the remote transport with the
42
+ * proxy-aware `innerFetch`; without one, `orgJwt` provides the existing JWT fetch.
43
43
  */
44
44
  export declare function mapToMastraMCPServers(config: MCPConfiguration, orgJwt?: JSONWebToken,
45
45
  /**
@@ -57,4 +57,4 @@ innerFetch?: typeof fetch,
57
57
  * (W-23231658). Stdio servers contribute no entry. Omitted in call sites
58
58
  * that don't need recovery (e.g. unit tests of the mapping shape).
59
59
  */
60
- captures?: Map<string, HttpErrorCapture>): Record<string, MastraMCPServerDefinition>;
60
+ captures?: Map<string, HttpErrorCapture>, providers?: McpAuthProviders): Record<string, MastraMCPServerDefinition>;
@@ -2,7 +2,7 @@
2
2
  * Copyright 2026, Salesforce, Inc. All rights reserved.
3
3
  * See LICENSE.txt for license terms.
4
4
  */
5
- import { resolveMcpServerHeaders } from '@salesforce/sfdx-agent-sdk';
5
+ import { resolveMcpServerHeaders, } from '@salesforce/sfdx-agent-sdk';
6
6
  /**
7
7
  * Returns `true` if `config` contains at least one server that is not explicitly disabled.
8
8
  * A server is considered enabled when its `enabled` property is `true` or absent.
@@ -48,8 +48,8 @@ const DEFAULT_RECONNECTION_OPTIONS = {
48
48
  * (typed as `StreamableHTTPClientTransportOptions['reconnectionOptions']`). Unspecified fields
49
49
  * are filled from {@link DEFAULT_RECONNECTION_OPTIONS} so partial overrides leave the rest at
50
50
  * the MCP SDK's built-in defaults.
51
- * - When `orgJwt` is provided, remote servers get a `fetch` that delegates to
52
- * {@link resolveMcpServerHeaders} per-request to inject auth for Salesforce Platform URLs.
51
+ * - A supplied runtime OAuth provider is forwarded to the remote transport with the
52
+ * proxy-aware `innerFetch`; without one, `orgJwt` provides the existing JWT fetch.
53
53
  */
54
54
  export function mapToMastraMCPServers(config, orgJwt,
55
55
  /**
@@ -67,7 +67,7 @@ innerFetch = globalThis.fetch,
67
67
  * (W-23231658). Stdio servers contribute no entry. Omitted in call sites
68
68
  * that don't need recovery (e.g. unit tests of the mapping shape).
69
69
  */
70
- captures) {
70
+ captures, providers = {}) {
71
71
  const result = {};
72
72
  for (const [name, serverConfig] of Object.entries(config)) {
73
73
  if (serverConfig.enabled === false) {
@@ -91,9 +91,8 @@ captures) {
91
91
  }
92
92
  else if (serverConfig.type === 'remote') {
93
93
  const url = serverConfig.url instanceof URL ? serverConfig.url : new URL(serverConfig.url);
94
- // Base transport fetch: auth-injecting when an orgJwt is present,
95
- // otherwise the bare inner (proxy-aware) fetch.
96
- const baseFetch = orgJwt
94
+ const provider = providers[name];
95
+ const baseFetch = provider === undefined && orgJwt
97
96
  ? createAuthFetch(serverConfig.url, orgJwt, innerFetch, serverConfig.headers)
98
97
  : innerFetch;
99
98
  // Wrap with HTTP-error capture when the harness asked for it, so a
@@ -111,13 +110,14 @@ captures) {
111
110
  transportFetch = wrapFetchWithErrorCapture(baseFetch, capture);
112
111
  }
113
112
  else {
114
- transportFetch = orgJwt ? baseFetch : undefined;
113
+ transportFetch = orgJwt || provider !== undefined ? baseFetch : undefined;
115
114
  }
116
115
  const def = {
117
116
  url,
118
117
  ...(serverConfig.headers !== undefined && {
119
118
  requestInit: { headers: serverConfig.headers },
120
119
  }),
120
+ ...(provider !== undefined && { authProvider: provider }),
121
121
  ...(transportFetch && { fetch: transportFetch }),
122
122
  ...(serverConfig.timeout !== undefined && {
123
123
  timeout: serverConfig.timeout,
@@ -17,11 +17,13 @@ export declare function sanitizeMcpErrorMessage(err: unknown): string;
17
17
  *
18
18
  * 1. `DOMException` `AbortError` and Node's `code === 'ABORT_ERR'` map to
19
19
  * `'aborted'` regardless of the `.message`.
20
- * 2. MCP-shaped errors (an `Error` carrying a numeric `code`, see
20
+ * 2. MCP SDK `UnauthorizedError` maps to `'oauth-required'` so the host can
21
+ * complete consumer-owned authorization before reconnecting.
22
+ * 3. MCP-shaped errors (an `Error` carrying a numeric `code`, see
21
23
  * {@link isMcpErrorLike}) match by code: a positive code in `[100, 599]` is
22
24
  * an HTTP status (see {@link categoryForHttpStatus}); a negative code is a
23
25
  * JSON-RPC 2.0 error (`-32001` etc.).
24
- * 3. Free-form `.message` text is matched against substrings the underlying
26
+ * 4. Free-form `.message` text is matched against substrings the underlying
25
27
  * transports surface (`401`, `403`, `5xx`, `ECONNREFUSED`, etc.). This is
26
28
  * intentionally a small lookup — categories are stable, message shapes
27
29
  * are not, so the heuristic stays narrow and falls back to `'unknown'`.
@@ -23,6 +23,9 @@ import { getErrorMessage, isAbortError } from '@salesforce/agentic-common';
23
23
  function isMcpErrorLike(err) {
24
24
  return err instanceof Error && typeof err.code === 'number';
25
25
  }
26
+ function isOAuthRequiredError(err) {
27
+ return err instanceof Error && err.constructor.name === 'UnauthorizedError';
28
+ }
26
29
  /**
27
30
  * Strip V8 stack frames (" at <frame>") and any framing that arrived inside
28
31
  * the message string. The result is a single, human-readable summary line safe
@@ -74,11 +77,13 @@ function categoryForHttpStatus(status) {
74
77
  *
75
78
  * 1. `DOMException` `AbortError` and Node's `code === 'ABORT_ERR'` map to
76
79
  * `'aborted'` regardless of the `.message`.
77
- * 2. MCP-shaped errors (an `Error` carrying a numeric `code`, see
80
+ * 2. MCP SDK `UnauthorizedError` maps to `'oauth-required'` so the host can
81
+ * complete consumer-owned authorization before reconnecting.
82
+ * 3. MCP-shaped errors (an `Error` carrying a numeric `code`, see
78
83
  * {@link isMcpErrorLike}) match by code: a positive code in `[100, 599]` is
79
84
  * an HTTP status (see {@link categoryForHttpStatus}); a negative code is a
80
85
  * JSON-RPC 2.0 error (`-32001` etc.).
81
- * 3. Free-form `.message` text is matched against substrings the underlying
86
+ * 4. Free-form `.message` text is matched against substrings the underlying
82
87
  * transports surface (`401`, `403`, `5xx`, `ECONNREFUSED`, etc.). This is
83
88
  * intentionally a small lookup — categories are stable, message shapes
84
89
  * are not, so the heuristic stays narrow and falls back to `'unknown'`.
@@ -86,6 +91,8 @@ function categoryForHttpStatus(status) {
86
91
  export function classifyMcpError(err) {
87
92
  if (isAbortError(err))
88
93
  return 'aborted';
94
+ if (isOAuthRequiredError(err))
95
+ return 'oauth-required';
89
96
  if (isMcpErrorLike(err)) {
90
97
  // HTTP status (positive, 100–599) before the JSON-RPC switch — the two
91
98
  // code spaces are disjoint (JSON-RPC codes are negative), so an HTTP
@@ -125,8 +132,8 @@ export function classifyMcpError(err) {
125
132
  }
126
133
  /**
127
134
  * Whether the SDK considers the failure transient (worth a retry via
128
- * `Agent.reconnectMcpServer` / `Agent.refreshMcpAuth`). Aborted operations and
129
- * permanent protocol violations are non-retriable; everything else is retriable
135
+ * `Agent.reconnectMcpServer`). Aborted operations and
136
+ * permanent protocol violations and interactive OAuth requirements are non-retriable; everything else is retriable
130
137
  * by default — failing-open here is correct because the consumer always has
131
138
  * the option to ignore the hint.
132
139
  */
@@ -135,6 +142,7 @@ function isCategoryRetriable(category) {
135
142
  case 'aborted':
136
143
  case 'protocol-error':
137
144
  case 'config-error':
145
+ case 'oauth-required':
138
146
  return false;
139
147
  default:
140
148
  return true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/sfdx-agent-harness-mastra",
3
- "version": "0.49.0",
3
+ "version": "0.51.0",
4
4
  "description": "Mastra-backed AgentHarness implementation for @salesforce/sfdx-agent-sdk",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -42,27 +42,27 @@
42
42
  "openai": "^7.0.0"
43
43
  },
44
44
  "peerDependencies": {
45
- "@salesforce/sfdx-agent-sdk": "0.50.0"
45
+ "@salesforce/sfdx-agent-sdk": "0.52.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@eslint/js": "^10.0.1",
49
49
  "@salesforce/harness-conformance": "0.0.1",
50
- "@salesforce/sfdx-agent-sdk": "0.50.0",
50
+ "@salesforce/sfdx-agent-sdk": "0.52.0",
51
51
  "@types/node": "^22.20.1",
52
52
  "@vitest/coverage-istanbul": "^4.1.10",
53
53
  "@vitest/eslint-plugin": "^1.6.26",
54
- "eslint": "^10.8.0",
54
+ "eslint": "^10.8.1",
55
55
  "eslint-config-prettier": "^10.1.8",
56
56
  "eslint-import-resolver-typescript": "^4.4.5",
57
57
  "eslint-plugin-import": "^2.32.0",
58
- "eslint-plugin-n": "^18.2.2",
58
+ "eslint-plugin-n": "^18.3.0",
59
59
  "globals": "^17.9.0",
60
60
  "lint-staged": "^17.3.0",
61
61
  "prettier": "^3.9.6",
62
62
  "rimraf": "^6.1.3",
63
- "tsx": "^4.23.5",
63
+ "tsx": "^4.23.12",
64
64
  "typescript": "^7.0.2",
65
- "typescript-eslint": "^8.66.0",
65
+ "typescript-eslint": "^8.67.0",
66
66
  "vitest": "^4.1.8"
67
67
  },
68
68
  "engines": {