@salesforce/b2c-dx-mcp 3.0.1 → 3.1.1

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.
Files changed (69) hide show
  1. package/content/guidance/b2c-cli/b2c-config/SKILL.md +18 -1
  2. package/content/guidance/b2c-cli/b2c-docs/SKILL.md +8 -3
  3. package/content/guidance/b2c-cli/b2c-ecdn/SKILL.md +4 -4
  4. package/content/guidance/b2c-cli/b2c-ecdn/references/ADVANCED.md +21 -6
  5. package/content/guidance/b2c-cli/b2c-ecdn/references/SECURITY.md +4 -4
  6. package/content/guidance/b2c-cli/b2c-job/SKILL.md +2 -0
  7. package/content/guidance/b2c-cli/b2c-job/references/EXPORT.md +9 -3
  8. package/content/guidance/b2c-cli/b2c-job/references/IMPORT.md +12 -0
  9. package/content/guidance/b2c-cli/b2c-mrt/SKILL.md +5 -4
  10. package/content/guidance/b2c-cli/b2c-mrt/references/BUNDLE-COMMANDS.md +21 -3
  11. package/content/guidance/b2c-cli/b2c-mrt/references/ENVIRONMENT-COMMANDS.md +23 -0
  12. package/content/guidance/b2c-cli/b2c-webdav/SKILL.md +17 -0
  13. package/content/guidance/index.json +22 -22
  14. package/content/guidance/mcp/scapi/SKILL.md +46 -14
  15. package/dist/commands/mcp.js +1 -1
  16. package/dist/guidance.js +0 -2
  17. package/dist/registry.js +1 -1
  18. package/dist/server-context.d.ts +2 -0
  19. package/dist/server-context.js +4 -0
  20. package/dist/server.d.ts +2 -4
  21. package/dist/server.js +16 -3
  22. package/dist/services.d.ts +7 -5
  23. package/dist/services.js +19 -8
  24. package/dist/tools/adapter.js +1 -1
  25. package/dist/tools/scapi/execution-registry.d.ts +62 -0
  26. package/dist/tools/scapi/execution-registry.js +303 -0
  27. package/dist/tools/scapi/index.d.ts +2 -1
  28. package/dist/tools/scapi/index.js +2 -2
  29. package/dist/tools/scapi/scapi-code.d.ts +2 -1
  30. package/dist/tools/scapi/scapi-code.js +71 -34
  31. package/dist/utils/types.d.ts +10 -3
  32. package/node_modules/@modelcontextprotocol/server/dist/createMcpHandler-CLhGwQTn.d.mts +2 -1
  33. package/node_modules/@modelcontextprotocol/server/dist/createMcpHandler-dBHMsxwf.d.cts +2 -1
  34. package/node_modules/@modelcontextprotocol/server/dist/mcp-D7GmuPnv.cjs +1 -1
  35. package/node_modules/@modelcontextprotocol/server/dist/mcp-DXXb3Vv3.mjs +1 -1
  36. package/node_modules/@modelcontextprotocol/server/dist/src-CX2iR2pK.mjs +2 -0
  37. package/node_modules/@modelcontextprotocol/server/dist/src-STyD_Vvf.cjs +2 -0
  38. package/node_modules/@salesforce/b2c-agent-plugins/CHANGELOG.md +30 -0
  39. package/node_modules/@salesforce/b2c-agent-plugins/README.md +4 -2
  40. package/node_modules/@salesforce/b2c-agent-plugins/b2c/.codex-plugin/plugin.json +1 -1
  41. package/node_modules/@salesforce/b2c-agent-plugins/b2c/plugin.json +1 -1
  42. package/node_modules/@salesforce/b2c-agent-plugins/b2c-cli/.codex-plugin/plugin.json +1 -1
  43. package/node_modules/@salesforce/b2c-agent-plugins/b2c-cli/plugin.json +1 -1
  44. package/node_modules/@salesforce/b2c-agent-plugins/b2c-cli/skills/b2c-config/SKILL.md +18 -1
  45. package/node_modules/@salesforce/b2c-agent-plugins/b2c-cli/skills/b2c-docs/SKILL.md +8 -3
  46. package/node_modules/@salesforce/b2c-agent-plugins/b2c-cli/skills/b2c-ecdn/SKILL.md +4 -4
  47. package/node_modules/@salesforce/b2c-agent-plugins/b2c-cli/skills/b2c-ecdn/references/ADVANCED.md +21 -6
  48. package/node_modules/@salesforce/b2c-agent-plugins/b2c-cli/skills/b2c-ecdn/references/SECURITY.md +4 -4
  49. package/node_modules/@salesforce/b2c-agent-plugins/b2c-cli/skills/b2c-job/SKILL.md +2 -0
  50. package/node_modules/@salesforce/b2c-agent-plugins/b2c-cli/skills/b2c-job/references/EXPORT.md +9 -3
  51. package/node_modules/@salesforce/b2c-agent-plugins/b2c-cli/skills/b2c-job/references/IMPORT.md +12 -0
  52. package/node_modules/@salesforce/b2c-agent-plugins/b2c-cli/skills/b2c-mrt/SKILL.md +5 -4
  53. package/node_modules/@salesforce/b2c-agent-plugins/b2c-cli/skills/b2c-mrt/references/BUNDLE-COMMANDS.md +21 -3
  54. package/node_modules/@salesforce/b2c-agent-plugins/b2c-cli/skills/b2c-mrt/references/ENVIRONMENT-COMMANDS.md +23 -0
  55. package/node_modules/@salesforce/b2c-agent-plugins/b2c-cli/skills/b2c-webdav/SKILL.md +17 -0
  56. package/node_modules/@salesforce/b2c-agent-plugins/b2c-ops/.codex-plugin/plugin.json +1 -1
  57. package/node_modules/@salesforce/b2c-agent-plugins/b2c-ops/plugin.json +1 -1
  58. package/node_modules/@salesforce/b2c-agent-plugins/b2c-python-sdk/.codex-plugin/plugin.json +41 -0
  59. package/node_modules/@salesforce/b2c-agent-plugins/b2c-python-sdk/README.md +35 -0
  60. package/node_modules/@salesforce/b2c-agent-plugins/b2c-python-sdk/assets/logo.svg +31 -0
  61. package/node_modules/@salesforce/b2c-agent-plugins/b2c-python-sdk/plugin.json +45 -0
  62. package/node_modules/@salesforce/b2c-agent-plugins/b2c-python-sdk/skills/using-b2c-tooling-sdk/SKILL.md +226 -0
  63. package/node_modules/@salesforce/b2c-agent-plugins/b2c-python-sdk/skills/using-b2c-tooling-sdk/references/api-catalog.md +154 -0
  64. package/node_modules/@salesforce/b2c-agent-plugins/package.json +1 -1
  65. package/node_modules/@salesforce/b2c-agent-plugins/plugins.json +3 -0
  66. package/node_modules/@salesforce/b2c-agent-plugins/storefront-next/.codex-plugin/plugin.json +1 -1
  67. package/node_modules/@salesforce/b2c-agent-plugins/storefront-next/plugin.json +1 -1
  68. package/oclif.manifest.json +2 -2
  69. package/package.json +3 -3
@@ -0,0 +1,303 @@
1
+ /*
2
+ * Copyright (c) 2025, Salesforce, Inc.
3
+ * SPDX-License-Identifier: Apache-2
4
+ * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
5
+ */
6
+ import { createHash, createHmac, randomBytes, randomUUID } from 'node:crypto';
7
+ import { inputRequired } from '@modelcontextprotocol/server';
8
+ function deferred() {
9
+ let resolve;
10
+ let reject;
11
+ const promise = new Promise((yes, no) => {
12
+ resolve = yes;
13
+ reject = no;
14
+ });
15
+ return { promise, resolve, reject };
16
+ }
17
+ function fingerprint(args) {
18
+ const canonical = (value) => {
19
+ if (Array.isArray(value))
20
+ return value.map((item) => canonical(item));
21
+ if (value && typeof value === 'object')
22
+ return Object.fromEntries(Object.entries(value)
23
+ .sort(([a], [b]) => a.localeCompare(b))
24
+ .map(([k, v]) => [k, canonical(v)]));
25
+ return value;
26
+ };
27
+ return createHash('sha256')
28
+ .update(JSON.stringify(canonical(args)))
29
+ .digest('hex');
30
+ }
31
+ function errorResult(message) {
32
+ return { isError: true, content: [{ type: 'text', text: JSON.stringify({ error: message }) }] };
33
+ }
34
+ function preview(value) {
35
+ const json = JSON.stringify(value, (key, item) => (/password|secret|token|authorization|credential/i.test(key) ? '[redacted]' : item), 2) ?? 'none';
36
+ const excerpt = json.split('\n').slice(0, 5).join('\n');
37
+ if (excerpt.length === json.length && json.length <= 400)
38
+ return json;
39
+ const notice = ' ... [truncated; approval covers full request]';
40
+ return excerpt.slice(0, 400 - notice.length).trimEnd() + notice;
41
+ }
42
+ /** One worker survives protocol round trips; none of its code is replayed. */
43
+ export class ScapiExecution {
44
+ digest;
45
+ supportsElicitation;
46
+ mintState;
47
+ control;
48
+ controller = new AbortController();
49
+ done;
50
+ id = randomUUID();
51
+ operations = [];
52
+ result;
53
+ rounds = new Map();
54
+ cancellation;
55
+ cancellationListeners = new Map();
56
+ dispatched = new WeakSet();
57
+ event = deferred();
58
+ pending;
59
+ queue = Promise.resolve();
60
+ constructor(digest, supportsElicitation, mintState) {
61
+ this.digest = digest;
62
+ this.supportsElicitation = supportsElicitation;
63
+ this.mintState = mintState;
64
+ }
65
+ cancel(reason = 'SCAPI_EXECUTION_CANCELLED: explicitly cancelled.') {
66
+ if (this.result || this.cancellation)
67
+ return;
68
+ this.cancellation = reason;
69
+ const error = new Error(reason);
70
+ this.controller.abort(error);
71
+ this.pending?.decision.reject(error);
72
+ }
73
+ async confirm(request) {
74
+ this.controller.signal.throwIfAborted();
75
+ const record = this.operations.at(-1);
76
+ if (record)
77
+ record.status = 'awaiting_confirmation';
78
+ if (!this.supportsElicitation) {
79
+ this.cancel('SCAPI_CONFIRMATION_UNSUPPORTED: Confirmation required; this client does not support form elicitation.');
80
+ this.controller.signal.throwIfAborted();
81
+ }
82
+ const roundNumber = this.rounds.size + 1;
83
+ const state = this.mintState(roundNumber);
84
+ const key = `approve_${roundNumber}`;
85
+ const path = new URL(request.url).pathname;
86
+ const organizationId = path.match(/\/organizations\/([^/]+)/)?.[1];
87
+ const earlierWrites = this.operations.filter((item) => item.kind === 'request' && !['GET', 'HEAD', 'OPTIONS'].includes(item.method ?? '') && this.dispatched.has(item)).length;
88
+ const message = [
89
+ `Approve SCAPI operation: ${request.operationId}`,
90
+ ...(organizationId ? [`Organization: ${organizationId}`] : []),
91
+ `${request.method} ${path}`,
92
+ ...(Object.keys(request.query).length > 0 ? [`Query:\n${preview(request.query)}`] : []),
93
+ ...(request.body === undefined ? [] : [`Body:\n${preview(request.body)}`]),
94
+ 'Safety Mode requires approval. Declining stops this execution.',
95
+ earlierWrites ? `Earlier write requests sent: ${earlierWrites}; no rollback.` : 'No earlier writes.',
96
+ `Execution: ${this.id}`,
97
+ ].join('\n');
98
+ const prompt = {
99
+ ...inputRequired({
100
+ requestState: state,
101
+ inputRequests: {
102
+ [key]: inputRequired.elicit({
103
+ message,
104
+ requestedSchema: {
105
+ type: 'object',
106
+ properties: { approve: { type: 'boolean', title: 'Approve this request only', default: false } },
107
+ required: ['approve'],
108
+ },
109
+ }),
110
+ },
111
+ }),
112
+ // Keep the local tool result shape; the SDK dispatches by resultType before content.
113
+ content: [],
114
+ _meta: {
115
+ executionId: this.id,
116
+ status: 'awaiting_confirmation',
117
+ operations: structuredClone(this.operations),
118
+ },
119
+ };
120
+ const round = { state, key, prompt, decision: deferred() };
121
+ this.pending = round;
122
+ this.rounds.set(state, round);
123
+ this.control?.pauseTimeout();
124
+ this.event.resolve(prompt);
125
+ try {
126
+ await round.decision.promise;
127
+ this.controller.signal.throwIfAborted();
128
+ if (record)
129
+ record.status = 'in_flight';
130
+ }
131
+ finally {
132
+ this.pending = undefined;
133
+ this.control?.resumeTimeout();
134
+ }
135
+ }
136
+ finish(result) {
137
+ this.controller.abort(new Error('SCAPI_EXECUTION_FINISHED'));
138
+ for (const [signal, listener] of this.cancellationListeners)
139
+ signal.removeEventListener('abort', listener);
140
+ this.cancellationListeners.clear();
141
+ this.pending?.decision.reject(new Error('SCAPI_EXECUTION_FINISHED'));
142
+ for (const record of this.operations) {
143
+ if (record.status === 'awaiting_confirmation')
144
+ record.status = 'not_sent';
145
+ else if (record.status === 'in_flight')
146
+ record.status = this.dispatched.has(record) ? 'unknown' : 'not_sent';
147
+ }
148
+ const output = this.cancellation ? { ...result, isError: true } : result;
149
+ const first = output.content[0];
150
+ const data = first?.type === 'text' ? JSON.parse(first.text) : {};
151
+ if (this.cancellation) {
152
+ delete data.result;
153
+ data.error = this.cancellation + ' Check earlier writes before retrying; no rollback is performed.';
154
+ }
155
+ this.result = {
156
+ ...output,
157
+ content: [
158
+ {
159
+ type: 'text',
160
+ text: JSON.stringify({
161
+ ...data,
162
+ executionId: this.id,
163
+ status: this.cancellation ? 'cancelled' : output.isError ? 'failed' : 'completed',
164
+ operations: this.operations,
165
+ }),
166
+ },
167
+ ],
168
+ };
169
+ this.event.resolve(this.result);
170
+ }
171
+ markDispatched() {
172
+ const record = this.operations.at(-1);
173
+ if (record)
174
+ this.dispatched.add(record);
175
+ }
176
+ async retry(state, context) {
177
+ const round = this.rounds.get(state);
178
+ if (!round)
179
+ throw new Error('SCAPI_CONTINUATION_INVALID: unknown or altered continuation.');
180
+ if (this.cancellation)
181
+ await this.done;
182
+ if (this.result)
183
+ return this.result;
184
+ if (round.responded)
185
+ return this.wait(context);
186
+ if (round !== this.pending)
187
+ throw new Error('SCAPI_CONTINUATION_STALE: approval round is no longer pending.');
188
+ const response = context.inputResponses?.[round.key];
189
+ if (!response)
190
+ return this.wait(context);
191
+ this.event = deferred();
192
+ round.responded = true;
193
+ const result = this.wait(context);
194
+ if (!context.supportsElicitation || response.action !== 'accept' || response.content?.approve !== true) {
195
+ this.cancel('SCAPI_APPROVAL_DECLINED: execution cancelled without sending the pending request.');
196
+ }
197
+ else {
198
+ round.decision.resolve();
199
+ }
200
+ return result;
201
+ }
202
+ async runCall(kind, input, operation) {
203
+ const previous = this.queue;
204
+ const call = previous.then(async () => {
205
+ this.controller.signal.throwIfAborted();
206
+ const args = input;
207
+ const record = {
208
+ kind,
209
+ ...(typeof args?.method === 'string' ? { method: args.method.toUpperCase() } : {}),
210
+ ...(typeof args?.path === 'string' ? { path: args.path.slice(0, 500) } : {}),
211
+ status: 'in_flight',
212
+ };
213
+ this.operations.push(record);
214
+ try {
215
+ const result = await operation();
216
+ record.status = 'completed';
217
+ if (result && typeof result === 'object' && 'status' in result && typeof result.status === 'number')
218
+ record.httpStatus = result.status;
219
+ return result;
220
+ }
221
+ catch (error) {
222
+ record.status = kind === 'auth' ? 'failed' : this.dispatched.has(record) ? 'unknown' : 'not_sent';
223
+ throw error;
224
+ }
225
+ });
226
+ this.queue = call.catch(() => { });
227
+ return call;
228
+ }
229
+ async wait(context) {
230
+ const signal = context?.signal;
231
+ if (signal && !this.cancellationListeners.has(signal)) {
232
+ const cancel = () => this.cancel('SCAPI_EXECUTION_CANCELLED: client cancelled the request.');
233
+ this.cancellationListeners.set(signal, cancel);
234
+ signal.addEventListener('abort', cancel, { once: true });
235
+ if (signal.aborted)
236
+ cancel();
237
+ }
238
+ try {
239
+ return this.result ?? (await this.event.promise);
240
+ }
241
+ finally {
242
+ if (signal && (!context?.keepCancellation || this.result)) {
243
+ signal.removeEventListener('abort', this.cancellationListeners.get(signal));
244
+ this.cancellationListeners.delete(signal);
245
+ }
246
+ }
247
+ }
248
+ }
249
+ /** Stdio-server-owned continuations. Terminal records are bounded; tokens never start new work. */
250
+ export class ScapiExecutionRegistry {
251
+ options;
252
+ executions = new Map();
253
+ secret = randomBytes(32);
254
+ constructor(options = {}) {
255
+ this.options = options;
256
+ }
257
+ async cancel(id) {
258
+ const execution = this.executions.get(id);
259
+ if (!execution)
260
+ throw new Error('SCAPI_EXECUTION_NOT_FOUND: execution is unavailable in this server session.');
261
+ execution.cancel();
262
+ await execution.done;
263
+ return execution.result;
264
+ }
265
+ async destroyAll() {
266
+ for (const execution of this.executions.values())
267
+ execution.cancel('SCAPI_EXECUTION_CANCELLED: server shutting down.');
268
+ await Promise.allSettled([...this.executions.values()].map((execution) => execution.done));
269
+ this.executions.clear();
270
+ }
271
+ async retry(args, context) {
272
+ const state = context.requestState;
273
+ if (typeof state !== 'string')
274
+ throw new Error('SCAPI_CONTINUATION_INVALID: expected server-issued requestState.');
275
+ const execution = this.executions.get(state.split('.')[0]);
276
+ // Exact server-side lookup validates the MAC and binds state to this server, round and original arguments.
277
+ if (!execution || !execution.rounds.has(state))
278
+ throw new Error('SCAPI_CONTINUATION_INVALID: execution unavailable, server restarted, or state was altered.');
279
+ if (execution.digest !== fingerprint(args))
280
+ throw new Error('SCAPI_CONTINUATION_MISMATCH: original code, input and target arguments must be unchanged. Start new code with a fresh call.');
281
+ return execution.retry(state, context);
282
+ }
283
+ async start(args, context, run) {
284
+ if ([...this.executions.values()].filter((execution) => !execution.result).length >= (this.options.maxActive ?? 4))
285
+ throw new Error('SCAPI_EXECUTION_LIMIT: cancel an existing execution or wait for it to finish.');
286
+ const digest = fingerprint(args);
287
+ const execution = new ScapiExecution(digest, context?.supportsElicitation === true, (round) => {
288
+ const prefix = `${execution.id}.${round}`;
289
+ return `${prefix}.${createHmac('sha256', this.secret).update(`${prefix}.${digest}`).digest('base64url')}`;
290
+ });
291
+ this.executions.set(execution.id, execution);
292
+ execution.done = Promise.resolve()
293
+ .then(() => run(execution))
294
+ .then((result) => execution.finish(result), (error) => execution.finish(errorResult(error instanceof Error ? error.message : String(error))))
295
+ .then(() => {
296
+ const terminal = [...this.executions.values()].filter((item) => item.result);
297
+ for (const item of terminal.slice(0, Math.max(0, terminal.length - 50)))
298
+ this.executions.delete(item.id);
299
+ });
300
+ return execution.wait(context);
301
+ }
302
+ }
303
+ //# sourceMappingURL=execution-registry.js.map
@@ -8,10 +8,11 @@
8
8
  */
9
9
  import type { McpTool } from '../../utils/index.js';
10
10
  import type { Services } from '../../services.js';
11
+ import type { ScapiExecutionRegistry } from './execution-registry.js';
11
12
  /**
12
13
  * Creates all tools for the SCAPI toolset.
13
14
  *
14
15
  * @param loadServices - Function that loads configuration and returns Services instance
15
16
  * @returns Array of MCP tools
16
17
  */
17
- export declare function createScapiTools(loadServices: () => Promise<Services> | Services): McpTool[];
18
+ export declare function createScapiTools(loadServices: () => Promise<Services> | Services, executions?: ScapiExecutionRegistry): McpTool[];
@@ -13,9 +13,9 @@ import { createScapiCodeTools } from './scapi-code.js';
13
13
  * @param loadServices - Function that loads configuration and returns Services instance
14
14
  * @returns Array of MCP tools
15
15
  */
16
- export function createScapiTools(loadServices) {
16
+ export function createScapiTools(loadServices, executions) {
17
17
  return [
18
- ...createScapiCodeTools(loadServices),
18
+ ...createScapiCodeTools(loadServices, undefined, executions),
19
19
  createMetricsGetTool(loadServices),
20
20
  createScapiCustomApisStatusTool(loadServices),
21
21
  createScapiSchemasListTool(loadServices),
@@ -1,3 +1,4 @@
1
1
  import type { McpTool } from '../../utils/types.js';
2
2
  import type { ServicesLoader } from '../adapter.js';
3
- export declare function createScapiCodeTools(loadServices: ServicesLoader, snippetDirectory?: string): McpTool[];
3
+ import { ScapiExecutionRegistry } from './execution-registry.js';
4
+ export declare function createScapiCodeTools(loadServices: ServicesLoader, snippetDirectory?: string, registry?: ScapiExecutionRegistry): McpTool[];
@@ -4,7 +4,6 @@
4
4
  * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
5
5
  */
6
6
  import { z } from 'zod';
7
- import { randomUUID } from 'node:crypto';
8
7
  import { loadScapiSchemas, runScapiCode, createScapiRequest, createScapiAuth, loadScapiSnippets, saveScapiSnippet, } from '@salesforce/b2c-tooling-sdk/scapi';
9
8
  import { toOrganizationId } from '@salesforce/b2c-tooling-sdk/clients';
10
9
  import { getB2CConfigDirectory } from '@salesforce/b2c-tooling-sdk/config';
@@ -12,6 +11,7 @@ import { resolveEffectiveSafetyConfig, loadGlobalSafetyConfig } from '@salesforc
12
11
  import { createProjectContextInputSchema } from '../project-context.js';
13
12
  import { jsonResult, attachResolution } from '../adapter.js';
14
13
  import { MCP_SKILL_REFERENCES } from '../../skill-references.js';
14
+ import { ScapiExecutionRegistry } from './execution-registry.js';
15
15
  const code = z
16
16
  .string()
17
17
  .min(1)
@@ -60,7 +60,8 @@ async () => {
60
60
  const executeDescription = `Read, create, update, or delete Commerce records through SCAPI Admin APIs when no dedicated tool fits. Discover standard endpoints with scapi_search; for custom APIs, fetch the live contract through scapi.request() in the same program first. Requests authenticate automatically. JSON requests; no Shopper execution or binary transfers.
61
61
  Read skill://mcp/scapi/SKILL.md first.
62
62
 
63
- Reuse workflows with await codemode.run(name, input); describe before first use. Snippets share this execution's limits and configuration. async (input) receives the tool's input. executionId enables scapi_snippet_save; inspect the outcome before saving.
63
+ Reuse workflows with await codemode.run(name, input); describe first. async (input) receives input. Completed executionId enables scapi_snippet_save.
64
+ Safety confirmation uses MCP elicitation. Protocol retries resume retained code; never replay it. Approval has no server deadline. Decline or cancel terminates execution without rollback. action:cancel needs executionId only (plus skillRead).
64
65
 
65
66
  Available in your code:
66
67
  declare const organizationId: string | undefined; // resolved tenant
@@ -70,7 +71,7 @@ declare const scapi: {
70
71
  Promise<{status: number; ok: boolean; data: any; diagnostic?: {code: string; message: string}}>;
71
72
  };
72
73
 
73
- Compose known dependent requests in one async arrow function; await requests and pass intermediate results directly. Responses can be huge: filter/map/slice in code; return counts, selected rows, and verification fields. Preserve failures and diagnostics. HTTP failures return ok:false; transport/auth/safety failures throw. SDK safety governs scapi.request. fetch/WebSocket are disabled. Optional auth.accountManager()/auth.slas() export tokens for external clients; see skill. Check writes before retrying.
74
+ Compose dependent requests; await each. Responses can be huge: filter/map/slice; return counts, selected rows and verification. Preserve failures. HTTP failures return ok:false; transport/auth/safety failures throw. SDK safety governs scapi.request. fetch/WebSocket are disabled. Optional auth.accountManager()/auth.slas() export tokens for external clients; see skill. Check writes before retrying.
74
75
 
75
76
  Example: inspect a campaign's promotions
76
77
  async () => codemode.run('builtin/campaign-promotions', {campaignId: 'selected-campaign', limit: 4})`;
@@ -92,7 +93,7 @@ function failure(error, resolution) {
92
93
  isError: true,
93
94
  };
94
95
  }
95
- export function createScapiCodeTools(loadServices, snippetDirectory) {
96
+ export function createScapiCodeTools(loadServices, snippetDirectory, registry = new ScapiExecutionRegistry()) {
96
97
  // Retain only source for the last 50 completed executions, until this server ends.
97
98
  const executions = new Map();
98
99
  const searchInput = {
@@ -106,7 +107,16 @@ export function createScapiCodeTools(loadServices, snippetDirectory) {
106
107
  };
107
108
  const executeInput = {
108
109
  ...createProjectContextInputSchema('configuration'),
109
- code,
110
+ action: z
111
+ .enum(['execute', 'cancel'])
112
+ .optional()
113
+ .describe('Defaults to execute. Cancel terminates retained work; no rollback.'),
114
+ executionId: z
115
+ .string()
116
+ .uuid()
117
+ .optional()
118
+ .describe('Required for cancel; forbidden for execute. Protocol continuation uses requestState, not this field.'),
119
+ code: code.optional(),
110
120
  skillRead,
111
121
  input: z.unknown().optional().describe('JSON input for async (input); kept separate from saved source.'),
112
122
  };
@@ -164,6 +174,20 @@ export function createScapiCodeTools(loadServices, snippetDirectory) {
164
174
  try {
165
175
  const input = z.object(executeInput).strict().parse(args);
166
176
  requireScapiSkill(input.skillRead);
177
+ if (input.action === 'cancel') {
178
+ if (!input.executionId ||
179
+ Object.keys(args).some((key) => !['action', 'executionId', 'skillRead'].includes(key)) ||
180
+ context?.requestState !== undefined ||
181
+ context?.inputResponses !== undefined)
182
+ throw new Error('SCAPI_CANCEL_ARGUMENT_INVALID: cancel requires executionId and skillRead only; omit code, input, project overrides and protocol continuation.');
183
+ return await registry.cancel(input.executionId);
184
+ }
185
+ if (!input.code || input.executionId !== undefined)
186
+ throw new Error('SCAPI_EXECUTE_ARGUMENT_INVALID: execute requires code; omit executionId.');
187
+ if (context?.requestState !== undefined)
188
+ return await registry.retry(input, context);
189
+ if (context?.inputResponses !== undefined)
190
+ throw new Error('SCAPI_CONTINUATION_INVALID: inputResponses require server-issued requestState.');
167
191
  const services = await loadServices(input);
168
192
  resolution = services.getResolution();
169
193
  const config = services.getResolvedConfig();
@@ -172,36 +196,49 @@ export function createScapiCodeTools(loadServices, snippetDirectory) {
172
196
  name,
173
197
  services.getEnvironmentVariable(name),
174
198
  ]));
175
- let managedRequest;
176
- const request = async (options, signal) => {
177
- if (!shortCode || !tenantId)
178
- throw new Error('SCAPI requires configured shortCode and tenantId. Use config_inspect.');
179
- managedRequest ??= createScapiRequest({
180
- shortCode,
181
- tenantId,
182
- siteId,
183
- auth: () => config.createOAuth(),
184
- documents: loadScapiSchemas(),
185
- safety: resolveEffectiveSafetyConfig(config.values.safety, loadGlobalSafetyConfig(getB2CConfigDirectory(), safetyEnvironment, resolution?.projectDirectory?.path), safetyEnvironment),
186
- });
187
- return managedRequest(options, signal);
188
- };
189
- const result = await runScapiCode({
190
- code: input.code,
191
- request,
192
- auth: createScapiAuth(config.values, resolution.projectDirectory?.path),
193
- organizationId: tenantId ? toOrganizationId(tenantId) : undefined,
194
- siteId,
195
- cwd: resolution.projectDirectory?.path,
196
- signal: context?.signal,
197
- input: input.input,
198
- snippets: loadScapiSnippets(snippetDirectory),
199
+ return await registry.start(input, context, async (execution) => {
200
+ let managedRequest;
201
+ const request = async (options, signal) => {
202
+ if (!shortCode || !tenantId)
203
+ throw new Error('SCAPI requires configured shortCode and tenantId. Use config_inspect.');
204
+ managedRequest ??= createScapiRequest({
205
+ shortCode,
206
+ tenantId,
207
+ siteId,
208
+ auth: () => config.createOAuth(),
209
+ documents: loadScapiSchemas(),
210
+ confirm: (request) => execution.confirm(request),
211
+ onDispatch: () => execution.markDispatched(),
212
+ safety: resolveEffectiveSafetyConfig(config.values.safety, loadGlobalSafetyConfig(getB2CConfigDirectory(), safetyEnvironment, resolution?.projectDirectory?.path), safetyEnvironment),
213
+ });
214
+ return managedRequest(options, signal);
215
+ };
216
+ const auth = createScapiAuth(config.values, resolution?.projectDirectory?.path);
217
+ try {
218
+ const result = await runScapiCode({
219
+ code: input.code,
220
+ request: (options, signal) => execution.runCall('request', options, () => request(options, signal)),
221
+ auth: (operation, options, signal) => execution.runCall('auth', undefined, () => auth(operation, options, signal)),
222
+ organizationId: tenantId ? toOrganizationId(tenantId) : undefined,
223
+ siteId,
224
+ cwd: resolution?.projectDirectory?.path,
225
+ signal: execution.controller.signal,
226
+ onControl(control) {
227
+ execution.control = control;
228
+ },
229
+ input: input.input,
230
+ snippets: loadScapiSnippets(snippetDirectory),
231
+ });
232
+ const executionId = execution.id;
233
+ executions.set(executionId, input.code);
234
+ if (executions.size > 50)
235
+ executions.delete(executions.keys().next().value);
236
+ return codeResult({ result, executionId }, resolution);
237
+ }
238
+ catch (error) {
239
+ return failure(error, resolution);
240
+ }
199
241
  });
200
- const executionId = randomUUID();
201
- executions.set(executionId, input.code);
202
- if (executions.size > 50)
203
- executions.delete(executions.keys().next().value);
204
- return codeResult({ result, executionId }, resolution);
205
242
  }
206
243
  catch (error) {
207
244
  return failure(error, resolution);
@@ -6,6 +6,15 @@ import type { Toolset } from './constants.js';
6
6
  * Matches the MCP SDK's CallToolResult type.
7
7
  */
8
8
  export type ToolResult = CallToolResult;
9
+ /** Protocol input stays separate from model-supplied tool arguments. */
10
+ export interface ToolContext {
11
+ signal?: AbortSignal;
12
+ requestState?: unknown;
13
+ inputResponses?: Record<string, unknown>;
14
+ supportsElicitation?: boolean;
15
+ /** Legacy elicitation keeps the originating request open between handler entries. */
16
+ keepCancellation?: boolean;
17
+ }
9
18
  /** Effects of advertised operations; annotations are derived at registration. */
10
19
  export interface ToolEffects {
11
20
  effect: 'destructive' | 'read' | 'write';
@@ -35,9 +44,7 @@ export interface McpToolConfig<T extends ZodRawShape = ZodRawShape> extends Tool
35
44
  */
36
45
  export interface McpTool<T extends ZodRawShape = ZodRawShape> extends McpToolConfig<T> {
37
46
  /** Handler function that executes the tool */
38
- handler: (args: z.infer<z.ZodObject<T>>, context?: {
39
- signal?: AbortSignal;
40
- }) => Promise<ToolResult>;
47
+ handler: (args: z.infer<z.ZodObject<T>>, context?: ToolContext) => Promise<ToolResult>;
41
48
  }
42
49
  /**
43
50
  * Startup flags passed to the MCP server.
@@ -2001,6 +2001,7 @@ type RequestOptions = {
2001
2001
  /**
2002
2002
  * A timeout (in milliseconds) for this request. If exceeded, an {@linkcode SdkError} with code {@linkcode SdkErrorCode.RequestTimeout} will be raised from {@linkcode Protocol.request | request()}.
2003
2003
  *
2004
+ * Zero disables the timeout; explicit cancellation remains available.
2004
2005
  * If not specified, {@linkcode DEFAULT_REQUEST_TIMEOUT_MSEC} will be used as the timeout.
2005
2006
  */
2006
2007
  timeout?: number;
@@ -2819,7 +2820,7 @@ type ServerOptions = ProtocolOptions & {
2819
2820
  /**
2820
2821
  * Per-leg timeout (ms) for the shim's embedded server→client
2821
2822
  * requests, sent with `resetTimeoutOnProgress: true`. Human-paced —
2822
- * deliberately far above the 60s protocol default.
2823
+ * deliberately far above the 60s protocol default. Zero disables the timeout.
2823
2824
  * @default 600_000
2824
2825
  */
2825
2826
  roundTimeoutMs?: number;
@@ -2003,6 +2003,7 @@ type RequestOptions = {
2003
2003
  /**
2004
2004
  * A timeout (in milliseconds) for this request. If exceeded, an {@linkcode SdkError} with code {@linkcode SdkErrorCode.RequestTimeout} will be raised from {@linkcode Protocol.request | request()}.
2005
2005
  *
2006
+ * Zero disables the timeout; explicit cancellation remains available.
2006
2007
  * If not specified, {@linkcode DEFAULT_REQUEST_TIMEOUT_MSEC} will be used as the timeout.
2007
2008
  */
2008
2009
  timeout?: number;
@@ -2821,7 +2822,7 @@ type ServerOptions = ProtocolOptions & {
2821
2822
  /**
2822
2823
  * Per-leg timeout (ms) for the shim's embedded server→client
2823
2824
  * requests, sent with `resetTimeoutOnProgress: true`. Human-paced —
2824
- * deliberately far above the 60s protocol default.
2825
+ * deliberately far above the 60s protocol default. Zero disables the timeout.
2825
2826
  * @default 600_000
2826
2827
  */
2827
2828
  roundTimeoutMs?: number;
@@ -488,7 +488,7 @@ const DEFAULT_LEGACY_SHIM_ROUND_TIMEOUT_MS = 6e5;
488
488
  /** Resolves and validates `ServerOptions.inputRequired`, failing loudly at construction time. */
489
489
  function resolveLegacyShimOptions(options) {
490
490
  if (options?.maxRounds !== void 0 && (!Number.isInteger(options.maxRounds) || options.maxRounds < 1)) throw new RangeError(`inputRequired.maxRounds must be a positive integer (got ${options.maxRounds})`);
491
- if (options?.roundTimeoutMs !== void 0 && (!Number.isFinite(options.roundTimeoutMs) || options.roundTimeoutMs <= 0)) throw new RangeError(`inputRequired.roundTimeoutMs must be a positive number (got ${options.roundTimeoutMs})`);
491
+ if (options?.roundTimeoutMs !== void 0 && (!Number.isFinite(options.roundTimeoutMs) || options.roundTimeoutMs < 0)) throw new RangeError(`inputRequired.roundTimeoutMs must be a non-negative number (got ${options.roundTimeoutMs})`);
492
492
  return {
493
493
  maxRounds: options?.maxRounds ?? DEFAULT_LEGACY_SHIM_MAX_ROUNDS,
494
494
  roundTimeoutMs: options?.roundTimeoutMs ?? DEFAULT_LEGACY_SHIM_ROUND_TIMEOUT_MS,
@@ -486,7 +486,7 @@ const DEFAULT_LEGACY_SHIM_ROUND_TIMEOUT_MS = 6e5;
486
486
  /** Resolves and validates `ServerOptions.inputRequired`, failing loudly at construction time. */
487
487
  function resolveLegacyShimOptions(options) {
488
488
  if (options?.maxRounds !== void 0 && (!Number.isInteger(options.maxRounds) || options.maxRounds < 1)) throw new RangeError(`inputRequired.maxRounds must be a positive integer (got ${options.maxRounds})`);
489
- if (options?.roundTimeoutMs !== void 0 && (!Number.isFinite(options.roundTimeoutMs) || options.roundTimeoutMs <= 0)) throw new RangeError(`inputRequired.roundTimeoutMs must be a positive number (got ${options.roundTimeoutMs})`);
489
+ if (options?.roundTimeoutMs !== void 0 && (!Number.isFinite(options.roundTimeoutMs) || options.roundTimeoutMs < 0)) throw new RangeError(`inputRequired.roundTimeoutMs must be a non-negative number (got ${options.roundTimeoutMs})`);
490
490
  return {
491
491
  maxRounds: options?.maxRounds ?? DEFAULT_LEGACY_SHIM_MAX_ROUNDS,
492
492
  roundTimeoutMs: options?.roundTimeoutMs ?? DEFAULT_LEGACY_SHIM_ROUND_TIMEOUT_MS,
@@ -6251,6 +6251,8 @@ var Protocol = class {
6251
6251
  this._requestHandlerAbortControllers.get(notification.params.requestId)?.abort(notification.params.reason);
6252
6252
  }
6253
6253
  _setupTimeout(messageId, timeout, maxTotalTimeout, onTimeout, resetTimeoutOnProgress = false) {
6254
+ // Zero disables the deadline for human-paced elicitation; cancellation remains active.
6255
+ if (timeout === 0) return;
6254
6256
  this._timeoutInfo.set(messageId, {
6255
6257
  timeoutId: setTimeout(onTimeout, timeout),
6256
6258
  startTime: Date.now(),
@@ -6252,6 +6252,8 @@ var Protocol = class {
6252
6252
  this._requestHandlerAbortControllers.get(notification.params.requestId)?.abort(notification.params.reason);
6253
6253
  }
6254
6254
  _setupTimeout(messageId, timeout, maxTotalTimeout, onTimeout, resetTimeoutOnProgress = false) {
6255
+ // Zero disables the deadline for human-paced elicitation; cancellation remains active.
6256
+ if (timeout === 0) return;
6255
6257
  this._timeoutInfo.set(messageId, {
6256
6258
  timeoutId: setTimeout(onTimeout, timeout),
6257
6259
  startTime: Date.now(),
@@ -1,5 +1,35 @@
1
1
  # @salesforce/b2c-agent-plugins
2
2
 
3
+ ## 1.10.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#713](https://github.com/SalesforceCommerceCloud/b2c-developer-tooling/pull/713) [`b9972c9`](https://github.com/SalesforceCommerceCloud/b2c-developer-tooling/commit/b9972c9b9db69b2b2b7c2229250d9aa9b4c665e3) - Added Salesforce Help Knowledge Articles for B2C Commerce — troubleshooting, known issues, and how-to answers (administration: 135, developer support: 92, merchandising: 89, operations and security: 29, performance: 20, Composable Storefront: 16) — as a new `help-kb` docs category, available in the `b2c docs` CLI and MCP docs tools. (Thanks [@clavery](https://github.com/clavery)!)
8
+
9
+ - [#711](https://github.com/SalesforceCommerceCloud/b2c-developer-tooling/pull/711) [`c6ed31d`](https://github.com/SalesforceCommerceCloud/b2c-developer-tooling/commit/c6ed31d3f71553e1fb921c7e4106fb81086afb7d) - Updated the eCDN skill to cover the new code upload certificate workflow and fixed outdated flags in its certificate, security, speed, firewall, rate limit, and Page Shield examples. (Thanks [@clavery](https://github.com/clavery)!)
10
+
11
+ ## 1.10.0
12
+
13
+ ### Minor Changes
14
+
15
+ - [#667](https://github.com/SalesforceCommerceCloud/b2c-developer-tooling/pull/667) [`f9110ac`](https://github.com/SalesforceCommerceCloud/b2c-developer-tooling/commit/f9110ace279f3f3290ceaac9104e6436d71d2688) - Add a `b2c-python-sdk` agent-skills plugin for consuming the Python `salesforce-b2c-tooling-sdk` from scripts and notebooks. Install it from the plugin marketplace (`b2c-python-sdk`) or with `b2c setup skills b2c-python-sdk`. The skill covers install/import, async-vs-sync usage, choosing an auth mechanism, resolving config from `dw.json`, the operations-vs-clients error contract, and SLAS shopper tokens, with a full symbol catalog reference. (Thanks [@priandsf](https://github.com/priandsf)!)
16
+
17
+ ### Patch Changes
18
+
19
+ - [#690](https://github.com/SalesforceCommerceCloud/b2c-developer-tooling/pull/690) [`df4f24c`](https://github.com/SalesforceCommerceCloud/b2c-developer-tooling/commit/df4f24c963c050facea179013fc69db129ade5ea) - Make the `b2c mrt env var` family (`list` / `set` / `push` / `delete`) backend-aware, so environment variables can be read and written over the SCAPI Storefront Environments API (OAuth, scopes `sfcc.storefront.environments` for reads and `sfcc.storefront.environments.rw` for writes) instead of the legacy MRT Cloud API (per-user API key). Select the backend with `--mrt-backend` (`auto` | `legacy` | `scapi`, also `MRT_BACKEND` / `mrtBackend` in `dw.json`): `auto` prefers SCAPI when `--short-code`/`--tenant-id` and client-credentials or JWT Bearer auth are configured — otherwise legacy — and falls back to legacy on safe pre-execution errors; `scapi` never silently falls back. Over SCAPI, `set` and `delete` apply a merge-PATCH (only the keys you pass change; `delete` sends the key with a `null` value), and `push` pins every write to the backend its initial read resolved to, so a single `push` never crosses backends. The legacy path is unchanged. (Thanks [@kieran-sf](https://github.com/kieran-sf)!)
20
+
21
+ Under `--json`, `env var list` returns the serving backend's native shape (legacy `{count, variables}` vs the SCAPI environment-variables map) — the human-readable table is normalized across backends, but `--json` is not, so pin `legacy` or `scapi` when a script needs a stable shape. All four commands now emit only their result object on stdout under `--json` (progress text is suppressed); `push --json` is non-interactive and requires `--yes` when there are changes to apply.
22
+
23
+ - [#667](https://github.com/SalesforceCommerceCloud/b2c-developer-tooling/pull/667) [`f9110ac`](https://github.com/SalesforceCommerceCloud/b2c-developer-tooling/commit/f9110ace279f3f3290ceaac9104e6436d71d2688) - Hardened the Python SDK's `sync` facade: calling it from inside an already-running event loop (e.g. a Jupyter cell) now warns instead of silently blocking forever with no explanation, mixing direct `await` use and `sync` use of the same object now raises an actionable error instead of a confusing cross-loop `RuntimeError`, and syncified objects now preserve identity/equality with their async counterparts (`sync_obj == async_obj`, stable `is`/hashing across repeated calls). Also fixed a PKCE code example in the `b2c-python-sdk` skill that referenced a nonexistent `AuthCredentials` constructor. (Thanks [@priandsf](https://github.com/priandsf)!)
24
+
25
+ - [#705](https://github.com/SalesforceCommerceCloud/b2c-developer-tooling/pull/705) [`66c599d`](https://github.com/SalesforceCommerceCloud/b2c-developer-tooling/commit/66c599da0664563f3aaeb529a749b9d5f8f0bfc6) - Show a readable effective safety policy and rule count in `b2c setup inspect`, including instance, global-file, and environment sources. Use `--verbose` for the full ordered ruleset; `--json` includes the complete structured configuration. (Thanks [@clavery](https://github.com/clavery)!)
26
+
27
+ - [#686](https://github.com/SalesforceCommerceCloud/b2c-developer-tooling/pull/686) [`643d0c0`](https://github.com/SalesforceCommerceCloud/b2c-developer-tooling/commit/643d0c055eb2101bb5bbce24d5bf5a9701610220) - Export one composable storefront by name and optionally wait for Storefront Next post-import setup after a successful site archive import, surfacing import data errors if setup never starts. Export configuration types and the IDE selector now also include the latest platform data units. (Thanks [@clavery](https://github.com/clavery)!)
28
+
29
+ - [#689](https://github.com/SalesforceCommerceCloud/b2c-developer-tooling/pull/689) [`880d25a`](https://github.com/SalesforceCommerceCloud/b2c-developer-tooling/commit/880d25a42a841e41202dab3d428a3433a7350ebd) - Reuse Storefront Next B2C Commerce, SLAS, site, and MRT environment variables as configuration fallbacks while preserving toolkit-specific overrides and normalizing full organization IDs. (Thanks [@clavery](https://github.com/clavery)!)
30
+
31
+ - [#706](https://github.com/SalesforceCommerceCloud/b2c-developer-tooling/pull/706) [`3fe3a10`](https://github.com/SalesforceCommerceCloud/b2c-developer-tooling/commit/3fe3a10f6ea0a1e7c1e8824d387efac45b7e6e7f) - Support `--root=dynamic` across WebDAV commands to manage site-specific files, including Velocity templates, independently of code deployments. Include the site ID as the first segment of the remote path, for example `b2c webdav ls --root=dynamic MySite/`. (Thanks [@clavery](https://github.com/clavery)!)
32
+
3
33
  ## 1.9.0
4
34
 
5
35
  ### Minor Changes