@salesforce/sfdx-agent-harness-mastra 0.6.0 → 0.7.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/README.md CHANGED
@@ -109,6 +109,30 @@ Mastra-internal pool / index / loaded-tools state is intentionally not on the ha
109
109
  are `getStateStats()`, `clearState(threadId?)`, `clearAllState()`, and `cleanupNow()` on `ToolSearchHandle`, plus `id`
110
110
  on both handles.
111
111
 
112
+ ### MCP tool-event enrichment
113
+
114
+ The harness enriches every `tool-call`, `tool-result`, and `tool-approval-request` event with the originating MCP server
115
+ name and any annotations the server declared, so consumers can branch on tool hints or group by server without parsing
116
+ the namespaced tool name themselves. `serverName` is `undefined` for non-MCP tools (workspace tools, consumer-declared
117
+ tools); `annotations` is `undefined` when the server omitted the field (per the MCP spec).
118
+
119
+ ```ts
120
+ for await (const event of result.eventStream) {
121
+ if (event.type === 'tool-approval-request') {
122
+ if (event.annotations?.readOnlyHint) {
123
+ // Auto-approve safe reads.
124
+ await session.approveToolCall(event.toolCall.toolCallId);
125
+ } else {
126
+ // Group/label by server when present, e.g. "sfdx wants to: deploy_metadata".
127
+ const approved = await promptUser(event.serverName, event.toolCall);
128
+ approved
129
+ ? await session.approveToolCall(event.toolCall.toolCallId)
130
+ : await session.declineToolCall(event.toolCall.toolCallId);
131
+ }
132
+ }
133
+ }
134
+ ```
135
+
112
136
  ### Using LLM Gateway Express
113
137
 
114
138
  ```ts
@@ -168,6 +168,50 @@ async function listMarkdownFilesInDirectory(dir) {
168
168
  }
169
169
  return names.sort().map((name) => path.join(dir, name));
170
170
  }
171
+ /**
172
+ * Resolve the MCP server name and (optionally) annotations for a Mastra
173
+ * namespaced tool name (`${serverName}_${toolName}`). Returns `undefined` for
174
+ * tools that are not in any MCP server's catalog (consumer-declared client
175
+ * tools, workspace tools).
176
+ *
177
+ * Iterates `agentState.mcpServers` and prefix-matches rather than splitting on
178
+ * `_`, because server names themselves may contain underscores
179
+ * (`data_pipeline_query_records` → server `data_pipeline`, tool
180
+ * `query_records`). Mirrors the catalog access pattern used by
181
+ * {@link mapMastraToolsToMcpToolInfo}.
182
+ *
183
+ * Design debt: this walk runs per `tool-call` / `tool-result` /
184
+ * `tool-approval-request` event and reconstructs the tool→server linkage from
185
+ * the namespaced string. The cost is O(servers) on a typically-small map, but
186
+ * three call sites now share the same lookup, so the carry-on-the-record fix —
187
+ * attaching an `McpToolInfo` reference (with `serverName`) on the in-memory
188
+ * tool-call record from the moment Mastra namespaces the name — becomes
189
+ * proportionally more valuable. That fix touches `MastraEventAdapter`,
190
+ * `ToolCallInfo`, and the per-stream pipeline; out of scope for W-22693491.
191
+ * If a fourth per-event MCP-catalog lookup gets added, do the
192
+ * carry-on-the-record refactor first; if this walk shows up in profiles
193
+ * before that, build a `Map<namespacedToolName, { serverName, McpToolInfo }>`
194
+ * index keyed off discovery completion.
195
+ */
196
+ /**
197
+ * Tool ChatEvents that carry the optional `serverName` + `annotations`
198
+ * enrichment fields. Centralizing the predicate lets `createEventStream`
199
+ * narrow once and apply the lookup uniformly across the three variants.
200
+ */
201
+ function isEnrichableToolEvent(event) {
202
+ return event.type === 'tool-call' || event.type === 'tool-result' || event.type === 'tool-approval-request';
203
+ }
204
+ function lookupMcpToolMetadata(agentState, namespacedToolName) {
205
+ for (const [serverName, serverState] of agentState.mcpServers) {
206
+ if (!namespacedToolName.startsWith(`${serverName}_`))
207
+ continue;
208
+ const original = namespacedToolName.slice(serverName.length + 1);
209
+ const annotations = serverState.tools[original]
210
+ ?.mcp?.annotations;
211
+ return { serverName, ...(annotations ? { annotations } : {}) };
212
+ }
213
+ return undefined;
214
+ }
171
215
  /**
172
216
  * Projects the namespaced Mastra `ToolAction` map for one MCP server into the
173
217
  * SDK's harness-agnostic `McpToolInfo[]` shape.
@@ -452,6 +496,13 @@ export class MastraHarness {
452
496
  const localFs = new LocalFilesystem({ basePath: projectRoot, allowedPaths });
453
497
  const sandbox = new LocalSandbox({
454
498
  workingDirectory: projectRoot,
499
+ // Forward the parent process env so spawned commands see HOME,
500
+ // locale, etc. (LocalSandbox's default forwards only PATH, which
501
+ // breaks tools like `sf org list` that resolve config from HOME).
502
+ // TODO: filter sensitive vars (e.g. ANTHROPIC_*, AWS creds) here
503
+ // if the harness ever runs alongside secrets it shouldn't leak
504
+ // into spawned tool subprocesses.
505
+ env: process.env,
455
506
  });
456
507
  mastraWorkspace = new Workspace({
457
508
  filesystem: localFs,
@@ -978,7 +1029,7 @@ export class MastraHarness {
978
1029
  agentState.activeRuns.set(threadId, { runId: output.runId, fullStream: output.fullStream });
979
1030
  const adapter = new MastraEventAdapter(this.buses.getLogBus());
980
1031
  return {
981
- eventStream: this.createEventStream(output.fullStream, adapter),
1032
+ eventStream: this.createEventStream(output.fullStream, adapter, agentState),
982
1033
  textStream: this.readableStreamToAsyncGenerator(output.textStream),
983
1034
  };
984
1035
  }
@@ -993,11 +1044,23 @@ export class MastraHarness {
993
1044
  agentResourceId(agentId) {
994
1045
  return `AGENT_INSTANCE:${agentId}`;
995
1046
  }
996
- async *createEventStream(fullStream, adapter) {
1047
+ async *createEventStream(fullStream, adapter, agentState) {
997
1048
  for await (const chunk of this.readableStreamToAsyncGenerator(fullStream)) {
998
1049
  const event = adapter.toChatEvent(chunk);
999
1050
  if (!event)
1000
1051
  continue;
1052
+ if (isEnrichableToolEvent(event)) {
1053
+ const toolName = event.type === 'tool-approval-request' ? event.toolCall.toolName : event.toolName;
1054
+ const metadata = lookupMcpToolMetadata(agentState, toolName);
1055
+ if (metadata) {
1056
+ yield {
1057
+ ...event,
1058
+ serverName: metadata.serverName,
1059
+ ...(metadata.annotations ? { annotations: metadata.annotations } : {}),
1060
+ };
1061
+ continue;
1062
+ }
1063
+ }
1001
1064
  yield event;
1002
1065
  }
1003
1066
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/sfdx-agent-harness-mastra",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Mastra-backed AgentHarness implementation for @salesforce/sfdx-agent-sdk",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -34,14 +34,14 @@
34
34
  "@mastra/memory": "^1.19.0",
35
35
  "@mastra/schema-compat": "^1.2.10",
36
36
  "@salesforce/agentic-common": "0.4.0",
37
- "@salesforce/llm-gateway-sdk": "0.4.0"
37
+ "@salesforce/llm-gateway-sdk": "0.5.0"
38
38
  },
39
39
  "peerDependencies": {
40
- "@salesforce/sfdx-agent-sdk": "0.6.0"
40
+ "@salesforce/sfdx-agent-sdk": "0.8.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@eslint/js": "^10.0.1",
44
- "@salesforce/sfdx-agent-sdk": "0.6.0",
44
+ "@salesforce/sfdx-agent-sdk": "0.8.0",
45
45
  "@types/node": "^22.19.17",
46
46
  "@vitest/coverage-istanbul": "^4.1.7",
47
47
  "@vitest/eslint-plugin": "^1.6.17",