@salesforce/sfdx-agent-harness-mastra 0.25.0 → 0.27.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 +18 -0
- package/dist/mastra-approval-coordinator.js +6 -1
- package/dist/mastra-harness.d.ts +10 -0
- package/dist/mastra-harness.js +45 -9
- package/dist/mcp-config-mapper.d.ts +27 -1
- package/dist/mcp-config-mapper.js +70 -2
- package/dist/mcp-error-classifier.d.ts +22 -1
- package/dist/mcp-error-classifier.js +52 -1
- package/package.json +10 -10
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,24 @@
|
|
|
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.27.0] - 2026-06-30
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
- **sdk,harness-mastra,harness-claude**: add bareToolName to MCP tool events for remember matcher @W-23090030 ([#634](https://github.com/forcedotcom/agentic-dx/pull/634))
|
|
10
|
+
|
|
11
|
+
### Fixes
|
|
12
|
+
- **harness-mastra**: recover discarded remote-MCP HTTP error body so consumers can classify org_not_enabled @W-23231658 ([#633](https://github.com/forcedotcom/agentic-dx/pull/633))
|
|
13
|
+
|
|
14
|
+
### Chores
|
|
15
|
+
- **deps**: bump the mastra group across 1 directory with 4 updates ([#630](https://github.com/forcedotcom/agentic-dx/pull/630))
|
|
16
|
+
- **deps**: bump @anthropic-ai/sdk from 0.105.0 to 0.106.0 ([#632](https://github.com/forcedotcom/agentic-dx/pull/632))
|
|
17
|
+
- **deps-dev**: bump the eslint group across 1 directory with 2 updates ([#629](https://github.com/forcedotcom/agentic-dx/pull/629))
|
|
18
|
+
- **deps-dev**: bump prettier from 3.8.4 to 3.9.0 in the dev-dependencies group ([#628](https://github.com/forcedotcom/agentic-dx/pull/628))
|
|
19
|
+
|
|
20
|
+
## [0.26.0] - 2026-06-29
|
|
21
|
+
|
|
22
|
+
_No changes — released alongside dependent packages._
|
|
23
|
+
|
|
6
24
|
## [0.25.0] - 2026-06-29
|
|
7
25
|
|
|
8
26
|
### Features
|
|
@@ -759,11 +759,16 @@ export class MastraApprovalCoordinator {
|
|
|
759
759
|
const invocation = this.buildToolInvocation(toolName);
|
|
760
760
|
// No serverName ⇒ built-in / consumer tool ⇒ nothing to stamp. The
|
|
761
761
|
// event keeps its Mastra-namespaced display `toolName`; enrichment only
|
|
762
|
-
// adds `serverName` / `annotations`.
|
|
762
|
+
// adds `serverName` / `annotations` / `bareToolName`.
|
|
763
763
|
if (invocation.serverName === undefined)
|
|
764
764
|
return event;
|
|
765
|
+
// The display `toolName` stays Mastra-namespaced (`${server}_${tool}`);
|
|
766
|
+
// `bareToolName` carries the un-namespaced leaf so the SDK can build a
|
|
767
|
+
// matching `mcp` policy matcher (the AG-UI "Allow always" / remember flow)
|
|
768
|
+
// without string-splitting the display name.
|
|
765
769
|
const fields = {
|
|
766
770
|
serverName: invocation.serverName,
|
|
771
|
+
bareToolName: invocation.toolName,
|
|
767
772
|
...(invocation.annotations ? { annotations: invocation.annotations } : {}),
|
|
768
773
|
};
|
|
769
774
|
if (event.type === 'tool-call') {
|
package/dist/mastra-harness.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { MCPClient } from '@mastra/mcp';
|
|
|
4
4
|
import { McpServerStatus, type AgentHarness, type WithAgentConfig, type ChatStreamResult, type HarnessAgentConfig, type MCPConfiguration, type McpServerErrorDetail, type McpServerInfo, type Message, type MessagePart, type ModelConnectivityInfo, 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 { MastraApprovalCoordinator } from './mastra-approval-coordinator.js';
|
|
7
|
+
import { type HttpErrorCapture } from './mcp-config-mapper.js';
|
|
7
8
|
import { type MastraAgentConfig, type SkillSearchHandle, type ToolSearchHandle } from './tool-search.js';
|
|
8
9
|
import { type MultimodalFile } from '@salesforce/sfdx-agent-sdk';
|
|
9
10
|
import { Clock, type LogRecord, type UniqueIDGenerator, type Unsubscribe } from '@salesforce/agentic-common';
|
|
@@ -51,6 +52,15 @@ interface McpServerState {
|
|
|
51
52
|
* on (tool merging, alwaysActive expansion, MCP-tool metadata lookup).
|
|
52
53
|
*/
|
|
53
54
|
tools: ToolsInput;
|
|
55
|
+
/**
|
|
56
|
+
* HTTP-error capture slot for remote servers (W-23231658). The remote
|
|
57
|
+
* transport fetch (built by `mapToMastraMCPServers`) writes the latest
|
|
58
|
+
* non-2xx `{ status, body }` here; the discovery / reconnect catch reads
|
|
59
|
+
* `latest` to recover the HTTP status `@mastra/mcp` discards when a
|
|
60
|
+
* fallback-eligible connect fails. `undefined` for stdio servers and for
|
|
61
|
+
* remote servers when no capture map was threaded in.
|
|
62
|
+
*/
|
|
63
|
+
httpErrorCapture?: HttpErrorCapture;
|
|
54
64
|
}
|
|
55
65
|
/**
|
|
56
66
|
* Internal state for a single agent. Holds Mastra-specific instances so the
|
package/dist/mastra-harness.js
CHANGED
|
@@ -20,7 +20,7 @@ import { mapMastraToMessage, mapMessageToMastra, partToTranscriptText } from './
|
|
|
20
20
|
import { isStandardSchemaWithJSON, standardSchemaToJSONSchema } from '@mastra/schema-compat';
|
|
21
21
|
import { mapToolDefinitionsToMastraTools } from './mastra-tool-mapper.js';
|
|
22
22
|
import { hasEnabledServers, mapToMastraMCPServers } from './mcp-config-mapper.js';
|
|
23
|
-
import { buildMcpErrorDetail, sanitizeMcpErrorMessage } from './mcp-error-classifier.js';
|
|
23
|
+
import { buildMcpErrorDetail, sanitizeMcpErrorMessage, synthesizeHttpTransportError } from './mcp-error-classifier.js';
|
|
24
24
|
import { partitionPool, resolveAlwaysActive, } from './tool-search.js';
|
|
25
25
|
import { validateMultimodalFiles } from '@salesforce/sfdx-agent-sdk';
|
|
26
26
|
import { buildAnthropicDirectLanguageModel, buildAnthropicLanguageModel } from './pass-through-anthropic.js';
|
|
@@ -490,7 +490,11 @@ export class MastraHarness {
|
|
|
490
490
|
const mcpServerStates = new Map();
|
|
491
491
|
if (hasEnabledServers(config.mcpServers)) {
|
|
492
492
|
options?.abortSignal?.throwIfAborted();
|
|
493
|
-
|
|
493
|
+
// Empty capture map; the mapper creates a per-remote-server capture
|
|
494
|
+
// object and stores it here so we can attach each to its
|
|
495
|
+
// McpServerState below (W-23231658).
|
|
496
|
+
const httpErrorCaptures = new Map();
|
|
497
|
+
const enabledServers = mapToMastraMCPServers(config.mcpServers, config.orgJwt, this.innerFetch, httpErrorCaptures);
|
|
494
498
|
for (const [serverName, serverDef] of Object.entries(enabledServers)) {
|
|
495
499
|
const client = new MCPClient({
|
|
496
500
|
id: `${agentId}:${serverName}`,
|
|
@@ -508,6 +512,7 @@ export class MastraHarness {
|
|
|
508
512
|
ready: Promise.resolve(),
|
|
509
513
|
settled: false,
|
|
510
514
|
tools: {},
|
|
515
|
+
httpErrorCapture: httpErrorCaptures.get(serverName),
|
|
511
516
|
};
|
|
512
517
|
mcpServerStates.set(serverName, entry);
|
|
513
518
|
entry.ready = this.runMcpDiscovery(agentId, serverName, client, mcpServerStates, 0, options?.abortSignal);
|
|
@@ -815,6 +820,11 @@ export class MastraHarness {
|
|
|
815
820
|
agentId,
|
|
816
821
|
serverName,
|
|
817
822
|
});
|
|
823
|
+
// W-23231658: clear any stale HTTP-error capture so a prior failure's
|
|
824
|
+
// status can't be mistaken for this reconnect's. The wrapper repopulates
|
|
825
|
+
// `latest` only if this reconnect's connect sees a non-2xx response.
|
|
826
|
+
if (serverState.httpErrorCapture)
|
|
827
|
+
serverState.httpErrorCapture.latest = undefined;
|
|
818
828
|
// Transport-level reconnect can throw (stdio spawn failure, remote handshake
|
|
819
829
|
// failure). Record on serverState and emit only `failed` (not started+failed
|
|
820
830
|
// — `started` already fired above). This keeps the public contract
|
|
@@ -826,8 +836,13 @@ export class MastraHarness {
|
|
|
826
836
|
// Drop telemetry + state writes if a newer epoch has superseded us.
|
|
827
837
|
if (state.mcpServers.get(serverName)?.epoch !== epoch)
|
|
828
838
|
return;
|
|
829
|
-
|
|
830
|
-
|
|
839
|
+
// Prefer the captured HTTP status + body over the bodyless generic
|
|
840
|
+
// error `@mastra/mcp` rethrows on a fallback-eligible connect
|
|
841
|
+
// failure (W-23231658); same recovery as the discovery catch.
|
|
842
|
+
const captured = serverState.httpErrorCapture?.latest;
|
|
843
|
+
const effectiveError = captured ? synthesizeHttpTransportError(captured.status, captured.body, err) : err;
|
|
844
|
+
const message = sanitizeMcpErrorMessage(effectiveError);
|
|
845
|
+
const errorDetail = buildMcpErrorDetail(effectiveError);
|
|
831
846
|
serverState.error = message;
|
|
832
847
|
serverState.errorDetail = errorDetail;
|
|
833
848
|
serverState.settled = true;
|
|
@@ -844,7 +859,7 @@ export class MastraHarness {
|
|
|
844
859
|
agentId,
|
|
845
860
|
serverName,
|
|
846
861
|
durationMs: this.clock.now().getTime() - startTime,
|
|
847
|
-
error:
|
|
862
|
+
error: effectiveError instanceof Error ? effectiveError : new Error(message),
|
|
848
863
|
errorDetail,
|
|
849
864
|
});
|
|
850
865
|
}
|
|
@@ -932,8 +947,9 @@ export class MastraHarness {
|
|
|
932
947
|
const prevConfig = state.mcpConfig ?? {};
|
|
933
948
|
const nextConfig = config.mcpServers ?? {};
|
|
934
949
|
const allNames = new Set([...Object.keys(prevConfig), ...Object.keys(nextConfig)]);
|
|
950
|
+
const nextHttpErrorCaptures = new Map();
|
|
935
951
|
const nextEnabledServers = hasEnabledServers(nextConfig)
|
|
936
|
-
? mapToMastraMCPServers(nextConfig, config.orgJwt, this.innerFetch)
|
|
952
|
+
? mapToMastraMCPServers(nextConfig, config.orgJwt, this.innerFetch, nextHttpErrorCaptures)
|
|
937
953
|
: {};
|
|
938
954
|
for (const name of allNames) {
|
|
939
955
|
const prev = prevConfig[name];
|
|
@@ -965,6 +981,7 @@ export class MastraHarness {
|
|
|
965
981
|
ready: Promise.resolve(),
|
|
966
982
|
settled: false,
|
|
967
983
|
tools: {},
|
|
984
|
+
httpErrorCapture: nextHttpErrorCaptures.get(name),
|
|
968
985
|
};
|
|
969
986
|
state.mcpServers.set(name, entry);
|
|
970
987
|
entry.ready = this.runMcpDiscovery(agentId, name, client, state.mcpServers, 0, options?.abortSignal);
|
|
@@ -1824,6 +1841,13 @@ export class MastraHarness {
|
|
|
1824
1841
|
const s = serverStates.get(serverName);
|
|
1825
1842
|
return s && s.epoch === epoch ? s : undefined;
|
|
1826
1843
|
};
|
|
1844
|
+
// W-23231658: clear any HTTP-error capture left from a prior discovery
|
|
1845
|
+
// run on this (reused) client/capture so a stale status can't bleed into
|
|
1846
|
+
// this run's failure classification. The wrapper repopulates `latest`
|
|
1847
|
+
// only if a non-2xx response actually occurs during this connect.
|
|
1848
|
+
const captureSlot = serverStates.get(serverName)?.httpErrorCapture;
|
|
1849
|
+
if (captureSlot)
|
|
1850
|
+
captureSlot.latest = undefined;
|
|
1827
1851
|
return makeAbortable(client.listToolsetsWithErrors(), abortSignal)
|
|
1828
1852
|
.then(({ toolsets, errors }) => {
|
|
1829
1853
|
const serverError = errors[serverName];
|
|
@@ -1877,8 +1901,20 @@ export class MastraHarness {
|
|
|
1877
1901
|
// Flip `settled` AND `currentStatus` BEFORE emitting any
|
|
1878
1902
|
// terminal telemetry — see the matching comment in the
|
|
1879
1903
|
// success path above (issue #373).
|
|
1880
|
-
|
|
1881
|
-
|
|
1904
|
+
//
|
|
1905
|
+
// W-23231658: when the remote transport captured a non-2xx
|
|
1906
|
+
// HTTP response, prefer it over the bodyless generic error
|
|
1907
|
+
// `@mastra/mcp` rethrows. The capture is only ever populated
|
|
1908
|
+
// when a real HTTP error occurred on this server's transport,
|
|
1909
|
+
// so by construction it IS the (or a) cause of this failure;
|
|
1910
|
+
// the original `err` rides along as `cause` so its stack is
|
|
1911
|
+
// preserved on the failed event.
|
|
1912
|
+
const captured = s.httpErrorCapture?.latest;
|
|
1913
|
+
const effectiveError = captured
|
|
1914
|
+
? synthesizeHttpTransportError(captured.status, captured.body, err)
|
|
1915
|
+
: err;
|
|
1916
|
+
const errorMessage = sanitizeMcpErrorMessage(effectiveError);
|
|
1917
|
+
const errorDetail = buildMcpErrorDetail(effectiveError);
|
|
1882
1918
|
s.error = errorMessage;
|
|
1883
1919
|
s.errorDetail = errorDetail;
|
|
1884
1920
|
s.settled = true;
|
|
@@ -1895,7 +1931,7 @@ export class MastraHarness {
|
|
|
1895
1931
|
agentId,
|
|
1896
1932
|
serverName,
|
|
1897
1933
|
durationMs: this.clock.now().getTime() - startTime,
|
|
1898
|
-
error:
|
|
1934
|
+
error: effectiveError instanceof Error ? effectiveError : new Error(String(effectiveError)),
|
|
1899
1935
|
errorDetail,
|
|
1900
1936
|
});
|
|
1901
1937
|
}
|
|
@@ -6,6 +6,23 @@ import { type MCPConfiguration } from '@salesforce/sfdx-agent-sdk';
|
|
|
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
|
+
};
|
|
9
26
|
/**
|
|
10
27
|
* Converts our {@link MCPConfiguration} into the `servers` map expected by Mastra's `MCPClient`.
|
|
11
28
|
*
|
|
@@ -31,4 +48,13 @@ export declare function mapToMastraMCPServers(config: MCPConfiguration, orgJwt?:
|
|
|
31
48
|
* here so MCP remote transports honor `HTTPS_PROXY` without `globalThis`
|
|
32
49
|
* mutation. Stdio transports ignore it (they're child processes).
|
|
33
50
|
*/
|
|
34
|
-
innerFetch?: typeof fetch
|
|
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>): Record<string, MastraMCPServerDefinition>;
|
|
@@ -12,6 +12,13 @@ export function hasEnabledServers(config) {
|
|
|
12
12
|
return false;
|
|
13
13
|
return Object.values(config).some((s) => s.enabled !== false);
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Cap on the captured error-body text. A connect-error body is a small JSON
|
|
17
|
+
* envelope (e.g. `{"error":{"code":404,"message":"..."}}`); the slice guards
|
|
18
|
+
* against an unexpectedly large body bloating the `McpServerInfo.error` string
|
|
19
|
+
* and telemetry payloads.
|
|
20
|
+
*/
|
|
21
|
+
const MAX_CAPTURED_BODY_CHARS = 8192;
|
|
15
22
|
/**
|
|
16
23
|
* MCP SDK's built-in `StreamableHTTPReconnectionOptions` defaults. Mirrored
|
|
17
24
|
* here so partial `MCPRemoteServerConfig.reconnectionOptions` inputs merge
|
|
@@ -51,7 +58,16 @@ export function mapToMastraMCPServers(config, orgJwt,
|
|
|
51
58
|
* here so MCP remote transports honor `HTTPS_PROXY` without `globalThis`
|
|
52
59
|
* mutation. Stdio transports ignore it (they're child processes).
|
|
53
60
|
*/
|
|
54
|
-
innerFetch = globalThis.fetch
|
|
61
|
+
innerFetch = globalThis.fetch,
|
|
62
|
+
/**
|
|
63
|
+
* Optional per-server HTTP-error capture map. When supplied, every remote
|
|
64
|
+
* server's transport fetch is wrapped so the latest non-2xx `{ status, body }`
|
|
65
|
+
* is recorded into `captures.get(serverName)`. The harness reads this in its
|
|
66
|
+
* discovery / reconnect catch to recover the status `@mastra/mcp` discards
|
|
67
|
+
* (W-23231658). Stdio servers contribute no entry. Omitted in call sites
|
|
68
|
+
* that don't need recovery (e.g. unit tests of the mapping shape).
|
|
69
|
+
*/
|
|
70
|
+
captures) {
|
|
55
71
|
const result = {};
|
|
56
72
|
for (const [name, serverConfig] of Object.entries(config)) {
|
|
57
73
|
if (serverConfig.enabled === false) {
|
|
@@ -75,12 +91,34 @@ innerFetch = globalThis.fetch) {
|
|
|
75
91
|
}
|
|
76
92
|
else if (serverConfig.type === 'remote') {
|
|
77
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
|
|
97
|
+
? createAuthFetch(serverConfig.url, orgJwt, innerFetch, serverConfig.headers)
|
|
98
|
+
: innerFetch;
|
|
99
|
+
// Wrap with HTTP-error capture when the harness asked for it, so a
|
|
100
|
+
// non-2xx connect response's status + body survive the error
|
|
101
|
+
// `@mastra/mcp` discards (W-23231658). The mapper owns the capture
|
|
102
|
+
// object's creation and stores it on the supplied map keyed by
|
|
103
|
+
// server name so the caller can read `latest` back from the matching
|
|
104
|
+
// `McpServerState`. When no capture map is supplied (e.g.
|
|
105
|
+
// mapping-shape unit tests) and there's no orgJwt, the def omits
|
|
106
|
+
// `fetch` entirely — preserving prior behavior.
|
|
107
|
+
let transportFetch;
|
|
108
|
+
if (captures) {
|
|
109
|
+
const capture = {};
|
|
110
|
+
captures.set(name, capture);
|
|
111
|
+
transportFetch = wrapFetchWithErrorCapture(baseFetch, capture);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
transportFetch = orgJwt ? baseFetch : undefined;
|
|
115
|
+
}
|
|
78
116
|
const def = {
|
|
79
117
|
url,
|
|
80
118
|
...(serverConfig.headers !== undefined && {
|
|
81
119
|
requestInit: { headers: serverConfig.headers },
|
|
82
120
|
}),
|
|
83
|
-
...(
|
|
121
|
+
...(transportFetch && { fetch: transportFetch }),
|
|
84
122
|
...(serverConfig.timeout !== undefined && {
|
|
85
123
|
timeout: serverConfig.timeout,
|
|
86
124
|
connectTimeout: serverConfig.timeout,
|
|
@@ -116,4 +154,34 @@ function createAuthFetch(serverUrl, orgJwt, innerFetch, existingHeaders) {
|
|
|
116
154
|
return innerFetch(url, { ...init, headers: merged });
|
|
117
155
|
};
|
|
118
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Wrap a transport fetch so every non-2xx response's status + body is recorded
|
|
159
|
+
* into `capture.latest` before the response is handed back to `@mastra/mcp`.
|
|
160
|
+
* This is the only point where the connect-error body still exists: when the
|
|
161
|
+
* Streamable-HTTP transport sees a fallback-eligible status it throws a
|
|
162
|
+
* `StreamableHTTPError(status, ...)`, but `connectHttp` then discards it on the
|
|
163
|
+
* SSE-fallback path and the harness only ever sees the bodyless generic
|
|
164
|
+
* `Error("Could not connect...")` (W-23231658, #617).
|
|
165
|
+
*
|
|
166
|
+
* The body is read from a clone so the original response stream stays intact for
|
|
167
|
+
* the transport. A clone-read failure (already-consumed body, network hiccup) is
|
|
168
|
+
* swallowed — capture is best-effort and must never perturb the real request.
|
|
169
|
+
*/
|
|
170
|
+
function wrapFetchWithErrorCapture(baseFetch, capture) {
|
|
171
|
+
return async (input, init) => {
|
|
172
|
+
const response = await baseFetch(input, init);
|
|
173
|
+
if (!response.ok) {
|
|
174
|
+
try {
|
|
175
|
+
const body = await response.clone().text();
|
|
176
|
+
capture.latest = { status: response.status, body: body.slice(0, MAX_CAPTURED_BODY_CHARS) };
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// Body unreadable (already consumed / stream error). Record the
|
|
180
|
+
// status alone so classification still has the HTTP code.
|
|
181
|
+
capture.latest = { status: response.status, body: '' };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return response;
|
|
185
|
+
};
|
|
186
|
+
}
|
|
119
187
|
//# sourceMappingURL=mcp-config-mapper.js.map
|
|
@@ -18,13 +18,34 @@ export declare function sanitizeMcpErrorMessage(err: unknown): string;
|
|
|
18
18
|
* 1. `DOMException` `AbortError` and Node's `code === 'ABORT_ERR'` map to
|
|
19
19
|
* `'aborted'` regardless of the `.message`.
|
|
20
20
|
* 2. MCP-shaped errors (an `Error` carrying a numeric `code`, see
|
|
21
|
-
* {@link isMcpErrorLike}) match by
|
|
21
|
+
* {@link isMcpErrorLike}) match by code: a positive code in `[100, 599]` is
|
|
22
|
+
* an HTTP status (see {@link categoryForHttpStatus}); a negative code is a
|
|
23
|
+
* JSON-RPC 2.0 error (`-32001` etc.).
|
|
22
24
|
* 3. Free-form `.message` text is matched against substrings the underlying
|
|
23
25
|
* transports surface (`401`, `403`, `5xx`, `ECONNREFUSED`, etc.). This is
|
|
24
26
|
* intentionally a small lookup — categories are stable, message shapes
|
|
25
27
|
* are not, so the heuristic stays narrow and falls back to `'unknown'`.
|
|
26
28
|
*/
|
|
27
29
|
export declare function classifyMcpError(err: unknown): McpServerErrorCategory;
|
|
30
|
+
/**
|
|
31
|
+
* Synthesize an MCP-shaped error (`Error & { code: number }`) from a captured
|
|
32
|
+
* non-2xx HTTP transport response. `@mastra/mcp`'s `connectHttp` discards the
|
|
33
|
+
* underlying `StreamableHTTPError` (which carries the HTTP status on `code`)
|
|
34
|
+
* when the SSE-fallback path also fails, throwing a bodyless generic error
|
|
35
|
+
* instead. The harness's remote-transport fetch wrapper records the
|
|
36
|
+
* `{ status, body }` it saw; this rebuilds the shape `classifyMcpError` /
|
|
37
|
+
* {@link buildMcpErrorDetail} need so the status survives to `McpServerInfo`
|
|
38
|
+
* and the `mcp-server-discovery-failed` event (W-23231658, #617).
|
|
39
|
+
*
|
|
40
|
+
* The resulting message mirrors the MCP SDK's own `StreamableHTTPError` text
|
|
41
|
+
* (`Streamable HTTP error: ...`) so message-substring consumers see a familiar
|
|
42
|
+
* shape, and `code` carries the HTTP status so the layered classifier routes it
|
|
43
|
+
* to `http-4xx` / `http-5xx` / `http-401` / `http-403`. The original generic
|
|
44
|
+
* error is attached as `cause` so its stack is preserved on the failed event.
|
|
45
|
+
*/
|
|
46
|
+
export declare function synthesizeHttpTransportError(status: number, body: string, cause?: unknown): Error & {
|
|
47
|
+
code: number;
|
|
48
|
+
};
|
|
28
49
|
/**
|
|
29
50
|
* Build the SDK-side {@link McpServerErrorDetail} from a caught error. The
|
|
30
51
|
* underlying JSON-RPC `code` (when the error is MCP-shaped, see
|
|
@@ -47,6 +47,27 @@ export function sanitizeMcpErrorMessage(err) {
|
|
|
47
47
|
// All lines were stack frames — fall back to the raw single-line input.
|
|
48
48
|
return lines[0]?.trim() ?? '';
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Map an HTTP status code to the SDK's stable {@link McpServerErrorCategory}.
|
|
52
|
+
* HTTP status codes are positive (100–599); JSON-RPC error codes are negative,
|
|
53
|
+
* so the two never collide on the numeric `code` field. The MCP SDK's
|
|
54
|
+
* `StreamableHTTPError` carries the HTTP status verbatim on `code`
|
|
55
|
+
* (`@modelcontextprotocol/sdk` `streamableHttp.js`), and the harness synthesizes
|
|
56
|
+
* the same shape from a captured remote-transport error body (W-23231658), so a
|
|
57
|
+
* 404 "org not enabled" surfaces as `http-4xx` + `code: 404` rather than
|
|
58
|
+
* collapsing to `unknown`.
|
|
59
|
+
*/
|
|
60
|
+
function categoryForHttpStatus(status) {
|
|
61
|
+
if (status === 401)
|
|
62
|
+
return 'http-401';
|
|
63
|
+
if (status === 403)
|
|
64
|
+
return 'http-403';
|
|
65
|
+
if (status >= 400 && status <= 499)
|
|
66
|
+
return 'http-4xx';
|
|
67
|
+
if (status >= 500 && status <= 599)
|
|
68
|
+
return 'http-5xx';
|
|
69
|
+
return 'unknown';
|
|
70
|
+
}
|
|
50
71
|
/**
|
|
51
72
|
* Map a caught error to the SDK's stable {@link McpServerErrorCategory}. The
|
|
52
73
|
* lookup is layered:
|
|
@@ -54,7 +75,9 @@ export function sanitizeMcpErrorMessage(err) {
|
|
|
54
75
|
* 1. `DOMException` `AbortError` and Node's `code === 'ABORT_ERR'` map to
|
|
55
76
|
* `'aborted'` regardless of the `.message`.
|
|
56
77
|
* 2. MCP-shaped errors (an `Error` carrying a numeric `code`, see
|
|
57
|
-
* {@link isMcpErrorLike}) match by
|
|
78
|
+
* {@link isMcpErrorLike}) match by code: a positive code in `[100, 599]` is
|
|
79
|
+
* an HTTP status (see {@link categoryForHttpStatus}); a negative code is a
|
|
80
|
+
* JSON-RPC 2.0 error (`-32001` etc.).
|
|
58
81
|
* 3. Free-form `.message` text is matched against substrings the underlying
|
|
59
82
|
* transports surface (`401`, `403`, `5xx`, `ECONNREFUSED`, etc.). This is
|
|
60
83
|
* intentionally a small lookup — categories are stable, message shapes
|
|
@@ -64,6 +87,12 @@ export function classifyMcpError(err) {
|
|
|
64
87
|
if (isAbortError(err))
|
|
65
88
|
return 'aborted';
|
|
66
89
|
if (isMcpErrorLike(err)) {
|
|
90
|
+
// HTTP status (positive, 100–599) before the JSON-RPC switch — the two
|
|
91
|
+
// code spaces are disjoint (JSON-RPC codes are negative), so an HTTP
|
|
92
|
+
// 404 no longer falls through the JSON-RPC `default` to `unknown`.
|
|
93
|
+
if (err.code >= 100 && err.code <= 599) {
|
|
94
|
+
return categoryForHttpStatus(err.code);
|
|
95
|
+
}
|
|
67
96
|
switch (err.code) {
|
|
68
97
|
case -32001: // RequestTimeout
|
|
69
98
|
return 'connect-timeout';
|
|
@@ -111,6 +140,28 @@ function isCategoryRetriable(category) {
|
|
|
111
140
|
return true;
|
|
112
141
|
}
|
|
113
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Synthesize an MCP-shaped error (`Error & { code: number }`) from a captured
|
|
145
|
+
* non-2xx HTTP transport response. `@mastra/mcp`'s `connectHttp` discards the
|
|
146
|
+
* underlying `StreamableHTTPError` (which carries the HTTP status on `code`)
|
|
147
|
+
* when the SSE-fallback path also fails, throwing a bodyless generic error
|
|
148
|
+
* instead. The harness's remote-transport fetch wrapper records the
|
|
149
|
+
* `{ status, body }` it saw; this rebuilds the shape `classifyMcpError` /
|
|
150
|
+
* {@link buildMcpErrorDetail} need so the status survives to `McpServerInfo`
|
|
151
|
+
* and the `mcp-server-discovery-failed` event (W-23231658, #617).
|
|
152
|
+
*
|
|
153
|
+
* The resulting message mirrors the MCP SDK's own `StreamableHTTPError` text
|
|
154
|
+
* (`Streamable HTTP error: ...`) so message-substring consumers see a familiar
|
|
155
|
+
* shape, and `code` carries the HTTP status so the layered classifier routes it
|
|
156
|
+
* to `http-4xx` / `http-5xx` / `http-401` / `http-403`. The original generic
|
|
157
|
+
* error is attached as `cause` so its stack is preserved on the failed event.
|
|
158
|
+
*/
|
|
159
|
+
export function synthesizeHttpTransportError(status, body, cause) {
|
|
160
|
+
const suffix = body ? `: ${body}` : '';
|
|
161
|
+
const err = new Error(`Streamable HTTP error: HTTP ${status}${suffix}`, cause !== undefined ? { cause } : undefined);
|
|
162
|
+
err.code = status;
|
|
163
|
+
return err;
|
|
164
|
+
}
|
|
114
165
|
/**
|
|
115
166
|
* Build the SDK-side {@link McpServerErrorDetail} from a caught error. The
|
|
116
167
|
* underlying JSON-RPC `code` (when the error is MCP-shaped, see
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/sfdx-agent-harness-mastra",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "Mastra-backed AgentHarness implementation for @salesforce/sfdx-agent-sdk",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -30,32 +30,32 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@ai-sdk/provider": "^3.0.10",
|
|
32
32
|
"@anthropic-ai/bedrock-sdk": "^0.31.0",
|
|
33
|
-
"@anthropic-ai/sdk": "^0.
|
|
34
|
-
"@mastra/core": "^1.
|
|
35
|
-
"@mastra/libsql": "^1.
|
|
33
|
+
"@anthropic-ai/sdk": "^0.106.0",
|
|
34
|
+
"@mastra/core": "^1.47.0",
|
|
35
|
+
"@mastra/libsql": "^1.14.2",
|
|
36
36
|
"@mastra/mcp": "^1.10.0",
|
|
37
|
-
"@mastra/memory": "^1.
|
|
37
|
+
"@mastra/memory": "^1.21.2",
|
|
38
38
|
"@mastra/schema-compat": "^1.2.11",
|
|
39
39
|
"@salesforce/agentic-common": "0.13.0",
|
|
40
40
|
"openai": "^6.0.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@salesforce/sfdx-agent-sdk": "0.
|
|
43
|
+
"@salesforce/sfdx-agent-sdk": "0.28.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@eslint/js": "^10.0.1",
|
|
47
|
-
"@salesforce/sfdx-agent-sdk": "0.
|
|
47
|
+
"@salesforce/sfdx-agent-sdk": "0.28.0",
|
|
48
48
|
"@types/node": "^22.20.0",
|
|
49
49
|
"@vitest/coverage-istanbul": "^4.1.8",
|
|
50
50
|
"@vitest/eslint-plugin": "^1.6.20",
|
|
51
|
-
"eslint": "^10.
|
|
51
|
+
"eslint": "^10.6.0",
|
|
52
52
|
"eslint-config-prettier": "^10.1.8",
|
|
53
53
|
"eslint-import-resolver-typescript": "^4.4.5",
|
|
54
54
|
"eslint-plugin-import": "^2.32.0",
|
|
55
|
-
"eslint-plugin-n": "^18.1
|
|
55
|
+
"eslint-plugin-n": "^18.2.1",
|
|
56
56
|
"globals": "^17.6.0",
|
|
57
57
|
"lint-staged": "^17.0.7",
|
|
58
|
-
"prettier": "^3.
|
|
58
|
+
"prettier": "^3.9.0",
|
|
59
59
|
"rimraf": "^6.1.3",
|
|
60
60
|
"tsx": "^4.22.4",
|
|
61
61
|
"typescript": "^6.0.3",
|