@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.
- package/CONDUCTOR.md +75 -0
- package/README.md +8 -5
- package/build/agent-capabilities/probe-context.js +13 -3
- package/build/agent-capabilities/probes.js +262 -11
- package/build/agent-capabilities/reporter.js +1 -0
- package/build/agents.generated.js +1 -1
- package/build/backend-warnings.js +44 -0
- package/build/claude-settings.js +129 -0
- package/build/commands.generated.js +5 -3
- package/build/conductor/bridge-api-client.js +7 -7
- package/build/conductor/cli.js +65 -12
- package/build/conductor/deny-enforcement-preflight.js +96 -0
- package/build/conductor/doctor.js +183 -2
- package/build/conductor/epic-reconcile.js +9 -1
- package/build/conductor/epic-runtime.js +403 -43
- package/build/conductor/epic-state.js +7 -0
- package/build/conductor/errors.js +115 -3
- package/build/conductor/event-accessors.js +28 -10
- package/build/conductor/local-merge.js +458 -95
- package/build/conductor/merge-ledger.js +6 -4
- package/build/conductor/pr-ci-producer.js +17 -2
- package/build/conductor/producer-ledger.js +1 -1
- package/build/conductor/store.js +161 -18
- package/build/conductor/supervisor-merge.js +32 -5
- package/build/conductor/taxonomy.js +8 -0
- package/build/conductor/tools.js +28 -6
- package/build/conductor/worker-ledger-cli.js +244 -0
- package/build/conductor-bin.js +1884 -6917
- package/build/doctor.js +8 -0
- package/build/estimate-epic.js +84 -0
- package/build/executor/cli.js +229 -0
- package/build/executor/credentials.js +65 -0
- package/build/executor/deps.js +117 -0
- package/build/executor/env.js +79 -0
- package/build/executor/heartbeat.js +59 -0
- package/build/executor/http-client.js +131 -0
- package/build/executor/index.js +10 -0
- package/build/executor/job-errors.js +55 -0
- package/build/executor/job-log-registry.js +110 -0
- package/build/executor/job-runner.js +822 -0
- package/build/executor/job-types.js +60 -0
- package/build/executor/merge-job.js +229 -0
- package/build/executor/observation.js +123 -0
- package/build/executor/permissions.js +79 -0
- package/build/executor/preflight.js +144 -0
- package/build/executor/process.js +81 -0
- package/build/executor/prompt-spec.js +235 -0
- package/build/executor/results.js +134 -0
- package/build/executor/resume-pre-spawn.js +179 -0
- package/build/executor/runner.js +98 -0
- package/build/executor/terminal-mutation.js +34 -0
- package/build/executor/test-clock.js +109 -0
- package/build/executor/types.js +18 -0
- package/build/executor/verdict-artifact.js +53 -0
- package/build/executor/viewer-tabs.js +78 -0
- package/build/executor/watch-cli.js +113 -0
- package/build/executor/worker-command.js +106 -0
- package/build/executor/worker-finalization.js +177 -0
- package/build/executor/worker-log.js +92 -0
- package/build/executor/worktree-gc.js +134 -0
- package/build/executor/worktree-inspection.js +86 -0
- package/build/executor/worktree.js +103 -0
- package/build/index.js +14534 -10416
- package/build/mcp-invoke.js +19 -3
- package/build/mcp-provisioning.js +31 -25
- package/build/mcp-registration-doctor.js +27 -7
- package/build/mcp-server-invocation.js +152 -0
- package/build/pipelines.generated.js +16 -20
- package/build/readme.generated.js +1 -1
- package/build/sfcc/client.js +192 -50
- package/build/sfcc/ocapi-write-faults.js +94 -0
- package/build/sfcc/permissions.js +7 -22
- package/build/sfcc/reads-site-preference.js +52 -19
- package/build/sfcc/register.js +9 -0
- package/build/sfcc/write-grants.js +80 -0
- package/build/sfcc/write-guard.js +39 -0
- package/build/sfcc/write-result.js +47 -0
- package/build/sfcc/write-tool-common.js +85 -0
- package/build/sfcc/writes-custom-object-def.js +141 -0
- package/build/sfcc/writes-object-attribute-payloads.js +97 -0
- package/build/sfcc/writes-site-preference-payloads.js +59 -0
- package/build/sfcc/writes-site-preference.js +96 -0
- package/build/sfcc/writes-system-object-payloads.js +213 -0
- package/build/sfcc/writes-system-object.js +348 -0
- package/build/sfcc/writes.js +66 -0
- package/build/start-tickets-conductor.js +25 -93
- package/build/start-tickets-prereqs.js +152 -1
- package/build/start-tickets.js +96 -158
- package/build/version.generated.js +1 -1
- package/build/visual-diff-worker.js +313 -0
- package/build/visual-diff.js +632 -0
- package/build/worktree-core.js +202 -0
- package/package.json +8 -4
- package/pipelines/idea-to-ticket.json +7 -0
- package/pipelines/review-ticket.json +5 -18
- package/public/css/main.min.css +1612 -107
- package/public/css/main.min.css.map +1 -1
- package/public/js/main.min.js +10267 -1
- package/public/js/main.min.js.map +1 -1
- 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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
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("
|
|
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("
|
|
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
|
|
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
|
|
94
|
+
const postBody = {
|
|
95
|
+
query: { match_all_query: {} },
|
|
96
|
+
};
|
|
90
97
|
if (start !== undefined)
|
|
91
|
-
|
|
98
|
+
postBody.start = start;
|
|
92
99
|
if (count !== undefined)
|
|
93
|
-
|
|
94
|
-
const
|
|
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"
|
|
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
|
|
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
|
|
145
|
-
"
|
|
146
|
-
"
|
|
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,
|
package/build/sfcc/register.js
CHANGED
|
@@ -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
|
+
}
|