@bridge_gpt/mcp-server 0.2.18 → 0.2.20

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 (100) hide show
  1. package/CONDUCTOR.md +75 -0
  2. package/README.md +8 -5
  3. package/build/agent-capabilities/probe-context.js +13 -3
  4. package/build/agent-capabilities/probes.js +262 -11
  5. package/build/agent-capabilities/reporter.js +1 -0
  6. package/build/agents.generated.js +1 -1
  7. package/build/backend-warnings.js +44 -0
  8. package/build/claude-settings.js +129 -0
  9. package/build/commands.generated.js +5 -3
  10. package/build/conductor/bridge-api-client.js +7 -7
  11. package/build/conductor/cli.js +65 -12
  12. package/build/conductor/deny-enforcement-preflight.js +96 -0
  13. package/build/conductor/doctor.js +183 -2
  14. package/build/conductor/epic-reconcile.js +9 -1
  15. package/build/conductor/epic-runtime.js +403 -43
  16. package/build/conductor/epic-state.js +7 -0
  17. package/build/conductor/errors.js +115 -3
  18. package/build/conductor/event-accessors.js +28 -10
  19. package/build/conductor/local-merge.js +458 -95
  20. package/build/conductor/merge-ledger.js +6 -4
  21. package/build/conductor/pr-ci-producer.js +17 -2
  22. package/build/conductor/producer-ledger.js +1 -1
  23. package/build/conductor/store.js +161 -18
  24. package/build/conductor/supervisor-merge.js +32 -5
  25. package/build/conductor/taxonomy.js +8 -0
  26. package/build/conductor/tools.js +28 -6
  27. package/build/conductor/worker-ledger-cli.js +244 -0
  28. package/build/conductor-bin.js +1884 -6917
  29. package/build/doctor.js +8 -0
  30. package/build/estimate-epic.js +84 -0
  31. package/build/executor/cli.js +229 -0
  32. package/build/executor/credentials.js +65 -0
  33. package/build/executor/deps.js +117 -0
  34. package/build/executor/env.js +79 -0
  35. package/build/executor/heartbeat.js +59 -0
  36. package/build/executor/http-client.js +131 -0
  37. package/build/executor/index.js +10 -0
  38. package/build/executor/job-errors.js +55 -0
  39. package/build/executor/job-log-registry.js +110 -0
  40. package/build/executor/job-runner.js +822 -0
  41. package/build/executor/job-types.js +60 -0
  42. package/build/executor/merge-job.js +229 -0
  43. package/build/executor/observation.js +123 -0
  44. package/build/executor/permissions.js +79 -0
  45. package/build/executor/preflight.js +144 -0
  46. package/build/executor/process.js +81 -0
  47. package/build/executor/prompt-spec.js +235 -0
  48. package/build/executor/results.js +134 -0
  49. package/build/executor/resume-pre-spawn.js +179 -0
  50. package/build/executor/runner.js +98 -0
  51. package/build/executor/terminal-mutation.js +34 -0
  52. package/build/executor/test-clock.js +109 -0
  53. package/build/executor/types.js +18 -0
  54. package/build/executor/verdict-artifact.js +53 -0
  55. package/build/executor/viewer-tabs.js +78 -0
  56. package/build/executor/watch-cli.js +113 -0
  57. package/build/executor/worker-command.js +106 -0
  58. package/build/executor/worker-finalization.js +177 -0
  59. package/build/executor/worker-log.js +92 -0
  60. package/build/executor/worktree-gc.js +134 -0
  61. package/build/executor/worktree-inspection.js +86 -0
  62. package/build/executor/worktree.js +103 -0
  63. package/build/index.js +14534 -10416
  64. package/build/mcp-invoke.js +19 -3
  65. package/build/mcp-provisioning.js +31 -25
  66. package/build/mcp-registration-doctor.js +27 -7
  67. package/build/mcp-server-invocation.js +152 -0
  68. package/build/pipelines.generated.js +16 -20
  69. package/build/readme.generated.js +1 -1
  70. package/build/sfcc/client.js +192 -50
  71. package/build/sfcc/ocapi-write-faults.js +94 -0
  72. package/build/sfcc/permissions.js +7 -22
  73. package/build/sfcc/reads-site-preference.js +52 -19
  74. package/build/sfcc/register.js +9 -0
  75. package/build/sfcc/write-grants.js +80 -0
  76. package/build/sfcc/write-guard.js +39 -0
  77. package/build/sfcc/write-result.js +47 -0
  78. package/build/sfcc/write-tool-common.js +85 -0
  79. package/build/sfcc/writes-custom-object-def.js +141 -0
  80. package/build/sfcc/writes-object-attribute-payloads.js +97 -0
  81. package/build/sfcc/writes-site-preference-payloads.js +59 -0
  82. package/build/sfcc/writes-site-preference.js +96 -0
  83. package/build/sfcc/writes-system-object-payloads.js +213 -0
  84. package/build/sfcc/writes-system-object.js +348 -0
  85. package/build/sfcc/writes.js +66 -0
  86. package/build/start-tickets-conductor.js +25 -93
  87. package/build/start-tickets-prereqs.js +152 -1
  88. package/build/start-tickets.js +96 -158
  89. package/build/version.generated.js +1 -1
  90. package/build/visual-diff-worker.js +313 -0
  91. package/build/visual-diff.js +632 -0
  92. package/build/worktree-core.js +202 -0
  93. package/package.json +8 -4
  94. package/pipelines/idea-to-ticket.json +7 -0
  95. package/pipelines/review-ticket.json +5 -18
  96. package/public/css/main.min.css +1612 -107
  97. package/public/css/main.min.css.map +1 -1
  98. package/public/js/main.min.js +10267 -1
  99. package/public/js/main.min.js.map +1 -1
  100. package/smoke-test/SMOKE-TEST.md +2 -1
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Shared OCAPI write fault-mapping primitives (BAPI-582 foundation).
3
+ *
4
+ * Centralizes the status/fault distinctions required by SFCC write plumbing so
5
+ * transport (client.ts) and future write tools do not each collapse write
6
+ * failures into a generic error. The known fault set below is the subset the
7
+ * ticket requires write handlers to be able to distinguish; unknown
8
+ * status/fault combinations are preserved verbatim rather than being coerced
9
+ * into one of the known types.
10
+ */
11
+ // ---------------------------------------------------------------------------
12
+ // Known (status, faultType) map
13
+ // ---------------------------------------------------------------------------
14
+ /** The exact (status → fault type) pairs the ticket requires callers to distinguish. */
15
+ const KNOWN_WRITE_FAULTS = {
16
+ 400: "MalformedKeyParameterException",
17
+ 404: "AttributeDefinitionNotFoundException",
18
+ 409: "IfMatchRequiredException",
19
+ 412: "InvalidIfMatchException",
20
+ };
21
+ // ---------------------------------------------------------------------------
22
+ // Fault extraction
23
+ // ---------------------------------------------------------------------------
24
+ /**
25
+ * Extract the OCAPI fault type from a response body.
26
+ *
27
+ * Handles the common SFCC shapes:
28
+ * - `{ fault: { type: "..." } }`
29
+ * - `{ type: "..." }`
30
+ * - `{ fault: "..." }` (string-valued fault)
31
+ *
32
+ * Returns `undefined` when no usable fault type string is present.
33
+ */
34
+ export function extractOcapiFaultType(body) {
35
+ if (body === null || typeof body !== "object")
36
+ return undefined;
37
+ const record = body;
38
+ // `{ fault: { type } }` or `{ fault: "..." }`
39
+ const fault = record.fault;
40
+ if (typeof fault === "string" && fault.length > 0)
41
+ return fault;
42
+ if (fault !== null && typeof fault === "object") {
43
+ const faultType = fault.type;
44
+ if (typeof faultType === "string" && faultType.length > 0)
45
+ return faultType;
46
+ }
47
+ // `{ type: "..." }`
48
+ const type = record.type;
49
+ if (typeof type === "string" && type.length > 0)
50
+ return type;
51
+ return undefined;
52
+ }
53
+ // ---------------------------------------------------------------------------
54
+ // Fault mapping
55
+ // ---------------------------------------------------------------------------
56
+ /**
57
+ * Map an OCAPI write response `(status, body)` into a structured fault.
58
+ *
59
+ * A fault is `known` only when BOTH the status and the extracted fault type
60
+ * match one of the ticket-required pairs. For everything else the numeric
61
+ * status and any extracted fault type are preserved without pretending the
62
+ * failure is one of the known faults.
63
+ */
64
+ export function mapOcapiWriteFault(status, body) {
65
+ const faultType = extractOcapiFaultType(body);
66
+ const expected = KNOWN_WRITE_FAULTS[status];
67
+ const known = expected !== undefined && faultType === expected;
68
+ return {
69
+ status,
70
+ faultType,
71
+ known,
72
+ errorCode: known ? expected : "OCAPI_WRITE_FAULT",
73
+ };
74
+ }
75
+ // ---------------------------------------------------------------------------
76
+ // Synthetic client-side bodies
77
+ // ---------------------------------------------------------------------------
78
+ /**
79
+ * Build an OCAPI-shaped synthetic body for the client-side missing-ETag case.
80
+ *
81
+ * Used by `ocapiPatch` when the GET round trip returned no ETag: rather than
82
+ * issuing a PATCH that SFCC would reject, the client synthesizes a
83
+ * `409 IfMatchRequiredException` body so callers see the same shape they would
84
+ * for a server-originated If-Match failure.
85
+ */
86
+ export function buildSyntheticIfMatchRequiredBody(path) {
87
+ return {
88
+ fault: {
89
+ type: "IfMatchRequiredException",
90
+ message: `PATCH ${path} requires an ETag (If-Match) captured from the GET round trip, ` +
91
+ `but the GET response returned no ETag header. Cannot safely issue a conditional PATCH.`,
92
+ },
93
+ };
94
+ }
@@ -9,6 +9,7 @@
9
9
  import { ocapiGet } from "./client.js";
10
10
  import { normalizeOcapiPage } from "./ocapi-shape.js";
11
11
  import { DEFAULT_OCAPI_VERSION } from "./config.js";
12
+ import { formatOcapiWriteGrantJson } from "./write-grants.js";
12
13
  // ---------------------------------------------------------------------------
13
14
  // OCAPI Settings JSON template
14
15
  // Operators paste this in: Administration > Site Development > Open Commerce API Settings → Data API tab.
@@ -47,28 +48,12 @@ const OCAPI_SETTINGS_READ_ONLY = (ocapiVersion) => JSON.stringify({
47
48
  },
48
49
  ],
49
50
  }, null, 2);
50
- const OCAPI_SETTINGS_WRITE_IMPORT = (ocapiVersion) => JSON.stringify({
51
- _v: ocapiVersion,
52
- clients: [
53
- {
54
- client_id: "<YOUR_CLIENT_ID>",
55
- resources: [
56
- {
57
- resource_id: "/system_object_definitions",
58
- methods: ["get", "put", "patch", "delete"],
59
- read_attributes: "(**)",
60
- write_attributes: "(**)",
61
- },
62
- {
63
- resource_id: "/system_object_definitions/**",
64
- methods: ["get", "put", "patch", "delete"],
65
- read_attributes: "(**)",
66
- write_attributes: "(**)",
67
- },
68
- ],
69
- },
70
- ],
71
- }, null, 2);
51
+ // Write/import grants are sourced from the shared write-grants module so the
52
+ // JSON printed here is byte-identical to what future write handlers echo on a
53
+ // 403. This now covers all required write resource families:
54
+ // /system_object_definitions, /system_object_definitions/**,
55
+ // /custom_object_definitions/**, and /site_preferences/**.
56
+ const OCAPI_SETTINGS_WRITE_IMPORT = (ocapiVersion) => formatOcapiWriteGrantJson(ocapiVersion);
72
57
  // ---------------------------------------------------------------------------
73
58
  // Tool implementation
74
59
  // ---------------------------------------------------------------------------
@@ -2,10 +2,11 @@
2
2
  * SFCC site-preference introspection read tools (BAPI-403, T6 — optional).
3
3
  *
4
4
  * Implements:
5
- * site_preference_get — GET /site_preferences/preference_groups/{group}/{instance}
6
- * site_preference_search — POST /site_preferences/preference_groups/{group}/{instance}/preference_search
5
+ * site_preference_group_list — GET /system_object_definitions/SitePreferences/attribute_groups
6
+ * site_preference_get match-all POST /site_preferences/preference_groups/{group}/{instance}/preference_search
7
+ * site_preference_search — POST /site_preferences/preference_groups/{group}/{instance}/preference_search
7
8
  *
8
- * Both tools are read-only, run behind the T1 call-time gate, and route
9
+ * All three tools are read-only, run behind the T1 call-time gate, and route
9
10
  * oversized payloads through the sfcc/output.ts truncate-and-save seam.
10
11
  *
11
12
  * NOTE: SCAPI Preferences API (sfcc.preferences) is intentionally NOT used.
@@ -35,20 +36,24 @@ const INSTANCE_ENUM = z.enum(["staging", "development", "sandbox", "production"]
35
36
  const INSTANCE_DESCRIBE = "OCAPI instance context. v1 supports the 'sandbox' context only; " +
36
37
  "any other value is rejected with a validation error. Defaults to 'sandbox'.";
37
38
  const sitePreferenceGetInput = z.object({
38
- group: z.string().describe("Preference group ID, e.g. 'Account' or 'General'."),
39
+ group: z.string().describe("Custom site preference group ID, e.g. 'LLMIntegration'."),
39
40
  instance: INSTANCE_ENUM.optional().default("sandbox").describe(INSTANCE_DESCRIBE),
40
41
  start: z.number().optional().describe("Zero-based offset for paging."),
41
42
  count: z.number().optional().describe("Maximum number of preferences to return."),
42
43
  });
43
44
  const sitePreferenceSearchInput = z.object({
44
- group: z.string().describe("Preference group ID to search within."),
45
+ group: z.string().describe("Custom site preference group ID to search within, e.g. 'LLMIntegration'."),
45
46
  instance: INSTANCE_ENUM.optional().default("sandbox").describe(INSTANCE_DESCRIBE),
46
- query: z.union([z.string(), z.record(z.string(), z.any())]).describe("Search query. Pass a plain string for text search across preference ids and values, " +
47
+ query: z.union([z.string(), z.record(z.string(), z.any())]).describe("Search query. Pass a plain string for text search across preference ids, " +
47
48
  "or a structured OCAPI query object (term_query, filtered_query, etc.)."),
48
49
  start: z.number().optional().describe("Zero-based offset for paging."),
49
50
  count: z.number().optional().describe("Maximum number of results to return."),
50
51
  sorts: z.array(z.any()).optional().describe("Array of OCAPI sort descriptors."),
51
52
  });
53
+ const sitePreferenceGroupListInput = z.object({
54
+ count: z.number().optional().describe("Maximum number of site preference groups to return."),
55
+ start: z.number().optional().describe("Zero-based offset for paging."),
56
+ });
52
57
  // ---------------------------------------------------------------------------
53
58
  // Helpers
54
59
  // ---------------------------------------------------------------------------
@@ -86,15 +91,14 @@ export function buildSitePreferenceGetHandler(gateDeps, getDocsDir) {
86
91
  if (guard)
87
92
  return guard;
88
93
  const encodedGroup = encodeURIComponent(group);
89
- const queryParams = {};
94
+ const postBody = {
95
+ query: { match_all_query: {} },
96
+ };
90
97
  if (start !== undefined)
91
- queryParams.start = String(start);
98
+ postBody.start = start;
92
99
  if (count !== undefined)
93
- queryParams.count = String(count);
94
- const queryStr = Object.keys(queryParams).length > 0
95
- ? "?" + new URLSearchParams(queryParams).toString()
96
- : "";
97
- const result = await ocapiGet(`/site_preferences/preference_groups/${encodedGroup}/${instance}${queryStr}`, credentials);
100
+ postBody.count = count;
101
+ const result = await ocapiPost(`/site_preferences/preference_groups/${encodedGroup}/${instance}/preference_search`, postBody, credentials);
98
102
  if (!result.ok) {
99
103
  return textResult(JSON.stringify({ error: "OCAPI error", status: result.status, body: result.body }, null, 2));
100
104
  }
@@ -113,7 +117,7 @@ export function buildSitePreferenceSearchHandler(gateDeps, getDocsDir) {
113
117
  const encodedGroup = encodeURIComponent(group);
114
118
  // Coerce plain string queries into OCAPI text_query shape
115
119
  const resolvedQuery = typeof query === "string"
116
- ? { text_query: { fields: ["id", "value"], search_phrase: query } }
120
+ ? { text_query: { fields: ["id"], search_phrase: query } }
117
121
  : query;
118
122
  const postBody = { query: resolvedQuery };
119
123
  if (start !== undefined)
@@ -132,25 +136,54 @@ export function buildSitePreferenceSearchHandler(gateDeps, getDocsDir) {
132
136
  return saveAndReturn(text, dir, `site-preference-search-${safeGroup(group)}-${safeTimestamp()}.json`);
133
137
  });
134
138
  }
139
+ export function buildSitePreferenceGroupListHandler(gateDeps, getDocsDir) {
140
+ return withSfccGate(gateDeps, async (args, credentials) => {
141
+ const { count, start } = sitePreferenceGroupListInput.parse(args);
142
+ const queryParams = {};
143
+ if (count !== undefined)
144
+ queryParams.count = String(count);
145
+ if (start !== undefined)
146
+ queryParams.start = String(start);
147
+ const queryStr = Object.keys(queryParams).length > 0
148
+ ? "?" + new URLSearchParams(queryParams).toString()
149
+ : "";
150
+ const result = await ocapiGet(`/system_object_definitions/SitePreferences/attribute_groups${queryStr}`, credentials);
151
+ if (!result.ok) {
152
+ return textResult(JSON.stringify({ error: "OCAPI error", status: result.status, body: result.body }, null, 2));
153
+ }
154
+ const normalized = normalizeOcapiBody(result.body);
155
+ const text = JSON.stringify(normalized, null, 2);
156
+ const dir = path.join(await getDocsDir(), "sfcc");
157
+ return saveAndReturn(text, dir, `site-preference-group-list-${safeTimestamp()}.json`);
158
+ });
159
+ }
135
160
  /**
136
- * Register the two SFCC site-preference introspection read tools.
161
+ * Register the three SFCC site-preference introspection read tools.
137
162
  *
138
163
  * Called from `registerSfccTools` — no direct `index.ts` edits needed.
139
164
  */
140
165
  export function registerSitePreferenceTools(registerTool, deps) {
141
166
  const { gateDeps, getDocsDir } = deps;
167
+ registerTool("site_preference_group_list", {
168
+ description: "List custom site preference group ids from the developer sandbox via " +
169
+ "GET /system_object_definitions/SitePreferences/attribute_groups. Read-only; " +
170
+ "use this to discover group ids before site_preference_get or site_preference_search. " +
171
+ "Accepts optional `count` and `start` for paging. Oversized outputs are auto-saved locally.",
172
+ inputSchema: sitePreferenceGroupListInput,
173
+ annotations: READ_ANNOTATIONS,
174
+ }, buildSitePreferenceGroupListHandler(gateDeps, getDocsDir));
142
175
  registerTool("site_preference_get", {
143
176
  description: "Read effective preferences for a site preference group from the developer sandbox " +
144
- "via GET /site_preferences/preference_groups/{group}/sandbox. Read-only; v1 supports " +
145
- "sandboxes only (non-sandbox instances are rejected). Accepts optional `start` and " +
146
- "`count` for paging. Oversized payloads are auto-saved locally and previewed inline.",
177
+ "via a match-all POST /site_preferences/preference_groups/{group}/sandbox/preference_search. " +
178
+ "Read-only, sandbox-only. Accepts optional `start`/`count` paging in the POST body. " +
179
+ "Oversized payloads are auto-saved locally.",
147
180
  inputSchema: sitePreferenceGetInput,
148
181
  annotations: READ_ANNOTATIONS,
149
182
  }, buildSitePreferenceGetHandler(gateDeps, getDocsDir));
150
183
  registerTool("site_preference_search", {
151
184
  description: "Search/filter preferences within a site preference group from the developer sandbox " +
152
185
  "via POST /site_preferences/preference_groups/{group}/sandbox/preference_search. " +
153
- "Read-only; v1 sandbox only. Pass a plain string for text search or a structured " +
186
+ "Read-only; v1 sandbox only. Pass a plain string for id-only text search or a structured " +
154
187
  "OCAPI query object. Oversized results are auto-saved locally.",
155
188
  inputSchema: sitePreferenceSearchInput,
156
189
  annotations: READ_ANNOTATIONS,
@@ -13,6 +13,7 @@ import { checkPermissionsTool } from "./permissions.js";
13
13
  import { registerSystemObjectReadTools } from "./reads-system-object.js";
14
14
  import { registerSfccCustomObjectDefReadTools } from "./reads-custom-object-def.js";
15
15
  import { registerSitePreferenceTools } from "./reads-site-preference.js";
16
+ import { registerSfccWriteTools } from "./writes.js";
16
17
  // ---------------------------------------------------------------------------
17
18
  // Registration
18
19
  // ---------------------------------------------------------------------------
@@ -69,5 +70,13 @@ export function registerSfccTools(registerTool, deps) {
69
70
  gateDeps,
70
71
  getDocsDir: deps.getDocsDir,
71
72
  });
73
+ // SFCC write registration seam (BAPI-582). No write tools ship yet — future
74
+ // write-surface tickets register here. Gated behind the same includeReadTools
75
+ // branch so writes become default-on with reads whenever the sfcc profile
76
+ // group is active.
77
+ registerSfccWriteTools(registerTool, {
78
+ gateDeps,
79
+ getDocsDir: deps.getDocsDir,
80
+ });
72
81
  }
73
82
  }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Shared OCAPI write-grant JSON helpers (BAPI-582 foundation).
3
+ *
4
+ * This module is the single source of truth for the OCAPI write-grant JSON that
5
+ * is both echoed by future write handlers on a 403 and printed by the
6
+ * `check_permissions` diagnostic. Keeping one definition guarantees the
7
+ * remediation JSON an operator sees is identical no matter which surface it
8
+ * comes from.
9
+ */
10
+ // ---------------------------------------------------------------------------
11
+ // Write resource families
12
+ // ---------------------------------------------------------------------------
13
+ /**
14
+ * The OCAPI write resource families required by the SFCC write surface.
15
+ * Defined once here so the grant JSON and any future validation share the list.
16
+ */
17
+ export const OCAPI_WRITE_RESOURCE_IDS = [
18
+ "/system_object_definitions",
19
+ "/system_object_definitions/**",
20
+ "/custom_object_definitions/**",
21
+ "/site_preferences/**",
22
+ ];
23
+ // ---------------------------------------------------------------------------
24
+ // Grant JSON builders
25
+ // ---------------------------------------------------------------------------
26
+ /**
27
+ * Build the OCAPI Settings write-grant object for the given version.
28
+ *
29
+ * The `clientIdPlaceholder` stays as `<YOUR_CLIENT_ID>` by default so the output
30
+ * is safe to print and paste — the real resolved client_id is never substituted.
31
+ */
32
+ export function buildOcapiWriteGrantSettings(ocapiVersion, clientIdPlaceholder = "<YOUR_CLIENT_ID>") {
33
+ return {
34
+ _v: ocapiVersion,
35
+ clients: [
36
+ {
37
+ client_id: clientIdPlaceholder,
38
+ resources: OCAPI_WRITE_RESOURCE_IDS.map((resource_id) => ({
39
+ resource_id,
40
+ methods: ["get", "put", "patch", "delete"],
41
+ read_attributes: "(**)",
42
+ write_attributes: "(**)",
43
+ })),
44
+ },
45
+ ],
46
+ };
47
+ }
48
+ /** Pretty-printed JSON string of the write-grant settings. */
49
+ export function formatOcapiWriteGrantJson(ocapiVersion, clientIdPlaceholder = "<YOUR_CLIENT_ID>") {
50
+ return JSON.stringify(buildOcapiWriteGrantSettings(ocapiVersion, clientIdPlaceholder), null, 2);
51
+ }
52
+ // ---------------------------------------------------------------------------
53
+ // 403 write-grant UX
54
+ // ---------------------------------------------------------------------------
55
+ /**
56
+ * Build the human-facing 403 remediation text for a failed write operation.
57
+ * Includes the 403 status, the failed operation/path, and the exact write-grant
58
+ * JSON operators must paste in Business Manager.
59
+ */
60
+ export function buildOcapiWriteGrant403Text(params) {
61
+ const { operation, path, ocapiVersion, body } = params;
62
+ const bodyLine = body === undefined
63
+ ? ""
64
+ : `\nResponse body:\n${JSON.stringify(body, null, 2)}\n`;
65
+ return (`HTTP 403: OCAPI write access denied for ${operation} ${path}.\n` +
66
+ bodyLine +
67
+ `\nTo grant write access, paste the JSON below in Business Manager:\n` +
68
+ ` Administration > Site Development > Open Commerce API Settings → Data API tab\n\n` +
69
+ `${formatOcapiWriteGrantJson(ocapiVersion)}\n\n` +
70
+ `Replace <YOUR_CLIENT_ID> with the client_id from your dw.json.`);
71
+ }
72
+ /**
73
+ * MCP-shaped wrapper around `buildOcapiWriteGrant403Text` so future write
74
+ * handlers can return the grant UX without re-implementing content wrapping.
75
+ */
76
+ export function writeGrantForbiddenResult(params) {
77
+ return {
78
+ content: [{ type: "text", text: buildOcapiWriteGrant403Text(params) }],
79
+ };
80
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Reusable sandbox-only write guard (BAPI-582 foundation).
3
+ *
4
+ * Future SFCC write handlers MUST call `rejectIfNotSandboxForWrite`
5
+ * immediately after input parsing and BEFORE any payload construction or OCAPI
6
+ * mutation. A non-null return value is
7
+ * the pre-formatted MCP rejection to return directly; a null return means the
8
+ * effective instance is a sandbox and the handler may proceed.
9
+ */
10
+ // ---------------------------------------------------------------------------
11
+ // Helpers
12
+ // ---------------------------------------------------------------------------
13
+ /** Wrap text in the standard MCP text-result shape. */
14
+ function textResult(text) {
15
+ return { content: [{ type: "text", text }] };
16
+ }
17
+ // ---------------------------------------------------------------------------
18
+ // Guard
19
+ // ---------------------------------------------------------------------------
20
+ /**
21
+ * Reject any write targeting a non-sandbox instance.
22
+ *
23
+ * SFCC write tools are sandbox-only in this foundation. The effective instance
24
+ * defaults to `"sandbox"` when `undefined` (some schemas default the value
25
+ * elsewhere), so an omitted instance is allowed. Any other value returns a
26
+ * structured JSON validation error naming the rejected instance; a sandbox
27
+ * returns `null` so the caller proceeds.
28
+ */
29
+ export function rejectIfNotSandboxForWrite(instance) {
30
+ const effective = instance === undefined ? "sandbox" : instance;
31
+ if (effective === "sandbox")
32
+ return null;
33
+ return textResult(JSON.stringify({
34
+ error: "VALIDATION_ERROR",
35
+ status: 400,
36
+ message: `SFCC write tools are sandbox-only. Refusing to write against instance ` +
37
+ `'${effective}'. Re-run the write against a developer sandbox instance.`,
38
+ }));
39
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Shared write-result formatter (BAPI-582 foundation).
3
+ *
4
+ * This is the single formatting path for all future SFCC write tools. Handlers
5
+ * pass the raw `OcapiGetResult` from `ocapiPut`/`ocapiPatch` here rather than
6
+ * inventing their own 403/fault/success response logic, guaranteeing a
7
+ * consistent surface across every write tool.
8
+ */
9
+ import { writeGrantForbiddenResult } from "./write-grants.js";
10
+ import { DEFAULT_OCAPI_VERSION } from "./config.js";
11
+ /** Wrap text in the standard MCP text-result shape. */
12
+ function textResult(text) {
13
+ return { content: [{ type: "text", text }] };
14
+ }
15
+ // ---------------------------------------------------------------------------
16
+ // Formatter
17
+ // ---------------------------------------------------------------------------
18
+ /**
19
+ * Format an OCAPI write result into the standard MCP tool response.
20
+ *
21
+ * - `403` → the paste-ready write-grant remediation (via write-grants.ts).
22
+ * - success → JSON with `status`, `outcome`, and `body`.
23
+ * - other failure → JSON with `error: "OCAPI_WRITE_ERROR"`, `status`, `fault`, `body`.
24
+ */
25
+ export function formatOcapiWriteToolResult(result, operation, path, ocapiVersion = DEFAULT_OCAPI_VERSION) {
26
+ if (result.status === 403) {
27
+ return writeGrantForbiddenResult({
28
+ operation,
29
+ path,
30
+ ocapiVersion,
31
+ body: result.body,
32
+ });
33
+ }
34
+ if (result.ok) {
35
+ return textResult(JSON.stringify({
36
+ status: result.status,
37
+ outcome: result.outcome,
38
+ body: result.body,
39
+ }));
40
+ }
41
+ return textResult(JSON.stringify({
42
+ error: "OCAPI_WRITE_ERROR",
43
+ status: result.status,
44
+ fault: result.fault,
45
+ body: result.body,
46
+ }));
47
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Shared helpers for the SFCC write MCP tools (BAPI-584).
3
+ *
4
+ * The two write-surface modules (`writes-custom-object-def.ts` and
5
+ * `writes-site-preference.ts`) both need the same small set of primitives:
6
+ * the destructive tool annotations, the MCP text-result shape, a consistent
7
+ * set of safe pre-transport error envelopes (validation / unexpected), and a
8
+ * single path-segment encoder. Centralizing them here guarantees every write
9
+ * tool returns an identical envelope shape and never leaks a stack trace or a
10
+ * raw thrown value to the caller.
11
+ */
12
+ import { z } from "zod";
13
+ // ---------------------------------------------------------------------------
14
+ // Annotations
15
+ // ---------------------------------------------------------------------------
16
+ /**
17
+ * Annotations shared by every SFCC write tool. Writes are mutating and NOT
18
+ * read-only or idempotent; `openWorldHint` mirrors the read tools (OCAPI is an
19
+ * external system).
20
+ */
21
+ export const WRITE_ANNOTATIONS = {
22
+ readOnlyHint: false,
23
+ destructiveHint: true,
24
+ idempotentHint: false,
25
+ openWorldHint: true,
26
+ };
27
+ /** Wrap text in the standard MCP text-result shape. */
28
+ export function textResult(text) {
29
+ return { content: [{ type: "text", text }] };
30
+ }
31
+ // ---------------------------------------------------------------------------
32
+ // Pre-transport error envelopes
33
+ // ---------------------------------------------------------------------------
34
+ /**
35
+ * Convert a Zod validation failure into a safe `VALIDATION_ERROR` envelope.
36
+ * The flattened issues are surfaced so callers can see which field failed,
37
+ * without exposing any thrown-error internals.
38
+ */
39
+ export function zodValidationEnvelope(err) {
40
+ return textResult(JSON.stringify({
41
+ error: "VALIDATION_ERROR",
42
+ status: 400,
43
+ message: "Input failed schema validation before any OCAPI call.",
44
+ issues: err.issues.map((issue) => ({
45
+ path: issue.path.join("."),
46
+ message: issue.message,
47
+ })),
48
+ }, null, 2));
49
+ }
50
+ /**
51
+ * A plain (non-Zod) validation rejection, e.g. a body/URL field mismatch that
52
+ * the schema alone cannot express. Same envelope shape as the Zod variant.
53
+ */
54
+ export function validationEnvelope(message) {
55
+ return textResult(JSON.stringify({ error: "VALIDATION_ERROR", status: 400, message }, null, 2));
56
+ }
57
+ /**
58
+ * Safe fallback envelope for an unexpected thrown error inside a write handler
59
+ * (typically a transport-layer throw). Never includes a stack trace or the raw
60
+ * thrown value.
61
+ */
62
+ export function unexpectedEnvelope() {
63
+ return textResult(JSON.stringify({
64
+ error: "INTERNAL_ERROR",
65
+ status: 500,
66
+ message: "Unexpected SFCC write tool failure.",
67
+ }, null, 2));
68
+ }
69
+ /**
70
+ * Map a pre-transport error to its envelope: Zod validation errors become a
71
+ * `VALIDATION_ERROR`, everything else becomes the generic `INTERNAL_ERROR`.
72
+ * Used for the `.parse()` boundary before any OCAPI mutation.
73
+ */
74
+ export function preTransportErrorEnvelope(err) {
75
+ if (err instanceof z.ZodError)
76
+ return zodValidationEnvelope(err);
77
+ return unexpectedEnvelope();
78
+ }
79
+ // ---------------------------------------------------------------------------
80
+ // Path encoding
81
+ // ---------------------------------------------------------------------------
82
+ /** URL-encode a single OCAPI path segment. */
83
+ export function encodedSegment(segment) {
84
+ return encodeURIComponent(segment);
85
+ }