@bridge_gpt/mcp-server 0.2.37 → 0.2.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +193 -16
- package/build/agent-capabilities/probe-context.js +2 -1
- package/build/agent-launchers/claude-executor-adapter.js +392 -0
- package/build/agent-launchers/executor-adapter-inspection.js +163 -0
- package/build/agent-launchers/executor-adapter-registry.js +90 -0
- package/build/agent-launchers/executor-adapter.js +136 -0
- package/build/agent-registry.js +28 -0
- package/build/agents.generated.js +1 -1
- package/build/claude-login.js +85 -0
- package/build/claude-user-config-doctor.js +59 -33
- package/build/commands.generated.js +12 -11
- package/build/conduct-epic/bridge-client.js +345 -0
- package/build/conduct-epic/checkpoint-store.js +423 -0
- package/build/conduct-epic/cli.js +1732 -0
- package/build/conduct-epic/lock.js +302 -0
- package/build/conduct-epic/pr-state.js +197 -0
- package/build/conduct-epic/spawn.js +101 -0
- package/build/conductor/bridge-api-client.js +37 -2
- package/build/conductor/doctor.js +11 -1
- package/build/conductor/install-doctor.js +184 -10
- package/build/conductor-bin.js +7 -7
- package/build/credential-store.js +10 -4
- package/build/credentials-cli.js +34 -19
- package/build/docs.generated.js +1 -1
- package/build/doctor.js +579 -88
- package/build/executor/agent-identity.js +32 -0
- package/build/executor/cli.js +50 -39
- package/build/executor/deps.js +15 -1
- package/build/executor/env.js +56 -45
- package/build/executor/index.js +9 -1
- package/build/executor/install-preflight.js +138 -0
- package/build/executor/job-errors.js +200 -0
- package/build/executor/job-runner.js +619 -268
- package/build/executor/observation.js +165 -0
- package/build/executor/permissions.js +163 -36
- package/build/executor/platform.js +54 -0
- package/build/executor/preflight.js +175 -67
- package/build/executor/process.js +39 -7
- package/build/executor/runner.js +19 -0
- package/build/executor/service-lifecycle.js +269 -0
- package/build/executor/service-unit.js +121 -12
- package/build/executor/stale-artifacts.js +70 -0
- package/build/executor/test-clock.js +188 -24
- package/build/executor/worker-command.js +22 -58
- package/build/executor/worker-log.js +82 -0
- package/build/executor/worktree-lock.js +264 -0
- package/build/index.js +527 -357
- package/build/install-bridge-conductor.js +376 -38
- package/build/install-bridge.js +414 -114
- package/build/install-doctor.js +13 -0
- package/build/install-reexec.js +5 -3
- package/build/mcp-install-state.js +130 -0
- package/build/mcp-profile.js +11 -2
- package/build/mcp-provisioning.js +15 -0
- package/build/merge-pull-request.js +562 -0
- package/build/phase-result-artifacts.js +450 -0
- package/build/pipeline-orchestrator.js +4 -0
- package/build/pipeline-utils.js +16 -0
- package/build/pipelines.generated.js +7 -7
- package/build/plane/preflight.js +18 -14
- package/build/plane/supervisor.js +8 -1
- package/build/project-root.js +34 -0
- package/build/readme.generated.js +1 -1
- package/build/run-unit-tests-launcher.js +36 -9
- package/build/setup-epic.js +57 -4
- package/build/sfcc/ocapi-shape.js +23 -4
- package/build/sfcc/permissions.js +25 -6
- package/build/sfcc/read-body.js +92 -0
- package/build/sfcc/read-projection.js +6 -2
- package/build/sfcc/reads-custom-object-def.js +33 -21
- package/build/sfcc/reads-site-preference.js +20 -7
- package/build/sfcc/reads-system-object.js +11 -5
- package/build/sfcc/register.js +61 -23
- package/build/sfcc/registration-inventory.js +89 -0
- package/build/sfcc/setup-status.js +18 -34
- package/build/sfcc/tool-wrapper.js +294 -17
- package/build/sfcc/write-grants.js +33 -1
- package/build/sfcc/write-guard.js +41 -12
- package/build/sfcc/write-result.js +16 -7
- package/build/sfcc/writes-custom-object-def.js +12 -4
- package/build/sfcc/writes-site-preference.js +6 -1
- package/build/sfcc/writes-system-object.js +11 -2
- package/build/sfcc/writes.js +13 -8
- package/build/start-tickets-prereqs.js +25 -15
- package/build/start-tickets.js +123 -21
- package/build/version.generated.js +1 -1
- package/build/worktree-core.js +9 -3
- package/docs/install/mcp-tool-integrations.md +54 -9
- package/docs/install/sfcc-integration.md +71 -24
- package/package.json +3 -3
- package/build/executor/worker-config-isolation.js +0 -287
|
@@ -65,6 +65,7 @@ export const MODULE_MOCK_MANIFEST = [
|
|
|
65
65
|
"build/sfcc/reads-site-preference.test.js",
|
|
66
66
|
"build/sfcc/reads-system-object.test.js",
|
|
67
67
|
"build/sfcc/register.test.js",
|
|
68
|
+
"build/sfcc/registration-boundary.test.js",
|
|
68
69
|
"build/sfcc/setup-status.test.js",
|
|
69
70
|
"build/sfcc/tool-wrapper.test.js",
|
|
70
71
|
"build/sfcc/write-gate-rejection.test.js",
|
|
@@ -96,14 +97,24 @@ export const FORCE_EXIT_QUARANTINE = ["build/secret-safety.test.js"];
|
|
|
96
97
|
// normal unit suite.
|
|
97
98
|
const INTEGRATION_DIR_PREFIX = "build/integration/";
|
|
98
99
|
const INTEGRATION_DIR_ALLOWLIST = new Set(["build/integration/measure-tools.test.js"]);
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
|
|
100
|
+
// Compiled test files that live OUTSIDE build/integration/ but still belong to
|
|
101
|
+
// the separately-invoked `npm run test:integration` lane, so the unit lane must
|
|
102
|
+
// skip them. BAPI-683 originally introduced this set as
|
|
103
|
+
// KNOWN_UNREGISTERED_ORPHANS: two pre-existing files that were wired into no
|
|
104
|
+
// script at all, preserved as-is rather than silently picked up. BAPI-822
|
|
105
|
+
// retired that state — every entry here is now named explicitly by
|
|
106
|
+
// scripts.test:integration, and validateManifest below fails if an entry
|
|
107
|
+
// disappears from the build output.
|
|
108
|
+
//
|
|
109
|
+
// Only add a file here when it genuinely needs the integration lane (an
|
|
110
|
+
// MCP_INTEGRATION=1 gate, real subprocesses, or an optional native dependency
|
|
111
|
+
// like better-sqlite3). A plain compiled test outside build/integration/ is
|
|
112
|
+
// discovered into the unit lane automatically and needs no registration —
|
|
113
|
+
// tests/pytest/mcp/test_mcp_server_test_registration_multicomponent.py fails if
|
|
114
|
+
// a *.integration.test.ts ends up in neither lane.
|
|
115
|
+
export const INTEGRATION_LANE_ONLY = [
|
|
104
116
|
"build/conductor/conductor-runtime.integration.test.js",
|
|
105
|
-
|
|
106
|
-
]);
|
|
117
|
+
];
|
|
107
118
|
/** Recursively lists compiled `*.test.js` files under `<root>/<buildDir>`, sorted and package-relative. */
|
|
108
119
|
export function discoverTestFiles(options) {
|
|
109
120
|
const { root, buildDir = DEFAULT_BUILD_DIR, fsImpl = { readdirSync } } = options;
|
|
@@ -133,7 +144,7 @@ export function discoverTestFiles(options) {
|
|
|
133
144
|
results.sort();
|
|
134
145
|
return results;
|
|
135
146
|
}
|
|
136
|
-
/** Throws if the module-mock
|
|
147
|
+
/** Throws if the module-mock, force-exit-quarantine, or integration-lane manifest has drifted from the discovered build output. */
|
|
137
148
|
export function validateManifest(discovered) {
|
|
138
149
|
const discoveredSet = new Set(discovered);
|
|
139
150
|
const seen = new Set();
|
|
@@ -157,6 +168,21 @@ export function validateManifest(discovered) {
|
|
|
157
168
|
`(rebuild with 'npm run build', or remove it from FORCE_EXIT_QUARANTINE if the file was deleted/renamed)`);
|
|
158
169
|
}
|
|
159
170
|
}
|
|
171
|
+
// The integration-lane set is the one exclusion that removes a discovered
|
|
172
|
+
// file from every unit batch, so a stale entry is invisible by construction:
|
|
173
|
+
// the excluded file simply stops running and nothing reports it. Validating
|
|
174
|
+
// it against the build output turns that into a loud failure (BAPI-822).
|
|
175
|
+
for (const entry of INTEGRATION_LANE_ONLY) {
|
|
176
|
+
if (seen.has(entry)) {
|
|
177
|
+
throw new Error(`run-unit-tests: integration-lane entry duplicated or also present in another manifest: ${entry}`);
|
|
178
|
+
}
|
|
179
|
+
seen.add(entry);
|
|
180
|
+
if (!discoveredSet.has(entry)) {
|
|
181
|
+
throw new Error(`run-unit-tests: integration-lane entry not found in build output: ${entry} ` +
|
|
182
|
+
`(rebuild with 'npm run build', or remove it from INTEGRATION_LANE_ONLY — and from ` +
|
|
183
|
+
`scripts.test:integration — if the file was deleted/renamed)`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
160
186
|
}
|
|
161
187
|
/** Selects and validates the requested group's file list from the discovered inventory. */
|
|
162
188
|
export function selectTestGroup(mode, discovered) {
|
|
@@ -165,12 +191,13 @@ export function selectTestGroup(mode, discovered) {
|
|
|
165
191
|
}
|
|
166
192
|
validateManifest(discovered);
|
|
167
193
|
const moduleMockSet = new Set(MODULE_MOCK_MANIFEST);
|
|
194
|
+
const integrationLaneSet = new Set(INTEGRATION_LANE_ONLY);
|
|
168
195
|
const normal = discovered.filter((file) => {
|
|
169
196
|
if (moduleMockSet.has(file))
|
|
170
197
|
return false;
|
|
171
198
|
if (file.startsWith(INTEGRATION_DIR_PREFIX) && !INTEGRATION_DIR_ALLOWLIST.has(file))
|
|
172
199
|
return false;
|
|
173
|
-
if (
|
|
200
|
+
if (integrationLaneSet.has(file))
|
|
174
201
|
return false;
|
|
175
202
|
return true;
|
|
176
203
|
});
|
package/build/setup-epic.js
CHANGED
|
@@ -38,6 +38,10 @@ import os from "node:os";
|
|
|
38
38
|
import readline from "node:readline";
|
|
39
39
|
import { approveEpicPlan, createEpicRunWithDisposition, fetchEpicRunState, replaceEpicRunPolicy, resolveConductorBridgeApiAccess, storeEpicPlan, validateEpicPlan, ConductorBridgeApiError, } from "./conductor/bridge-api-client.js";
|
|
40
40
|
import { validateBranchName } from "./base-ref.js";
|
|
41
|
+
// BAPI-814/E1a: reused from the conduct-epic client on purpose. The override is
|
|
42
|
+
// ONE repository-wide row with ONE reader; adding a second wrapper under
|
|
43
|
+
// `conductor/` would give the v2 path its own copy to drift from.
|
|
44
|
+
import { getIndexBranch } from "./conduct-epic/bridge-client.js";
|
|
41
45
|
import { hashPlan } from "./conductor/plan.js";
|
|
42
46
|
/** Accepted `policy_json.review_policy.source` values (the `ReviewPolicy` surface). */
|
|
43
47
|
export const SETUP_EPIC_REVIEW_POLICY_SOURCES = [
|
|
@@ -45,6 +49,36 @@ export const SETUP_EPIC_REVIEW_POLICY_SOURCES = [
|
|
|
45
49
|
"native_review_decision",
|
|
46
50
|
"none",
|
|
47
51
|
];
|
|
52
|
+
/**
|
|
53
|
+
* The warning lines for an active repository-wide index-branch override, or an
|
|
54
|
+
* empty array when there is nothing to say (BAPI-814/E1a).
|
|
55
|
+
*
|
|
56
|
+
* Fail-open by construction. This is optional enrichment layered onto a command
|
|
57
|
+
* whose real job is creating an epic run, so an unreadable override must not
|
|
58
|
+
* become a preflight failure: an unavailable lookup produces one bounded,
|
|
59
|
+
* sanitized diagnostic and `setup-epic` continues. The client already returns a
|
|
60
|
+
* sanitized `error` string and never the raw service body, so nothing further
|
|
61
|
+
* needs stripping here.
|
|
62
|
+
*/
|
|
63
|
+
async function describeActiveIndexOverride(access, deps) {
|
|
64
|
+
const result = await getIndexBranch(access, deps.fetch);
|
|
65
|
+
if (!result.ok) {
|
|
66
|
+
return [
|
|
67
|
+
` [warn] The repository's indexed-branch override could not be read: ${result.error}. ` +
|
|
68
|
+
`Continuing — this is optional context, not a prerequisite.`,
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
const override = result.value.override;
|
|
72
|
+
if (override === null)
|
|
73
|
+
return [];
|
|
74
|
+
return [
|
|
75
|
+
` [warn] ${access.repoName} has an ACTIVE index-branch override: ` +
|
|
76
|
+
`'${override.override_branch}'. Repository parses currently index that branch, ` +
|
|
77
|
+
`and the base-branch config field this run may fall back on resolves through the ` +
|
|
78
|
+
`same override.`,
|
|
79
|
+
` Setup is CONTINUING — this is a warning, not a refusal.`,
|
|
80
|
+
];
|
|
81
|
+
}
|
|
48
82
|
/** Echoed single-line prompt on stderr (mirrors connect-github's helper). */
|
|
49
83
|
function defaultPromptLine(promptText) {
|
|
50
84
|
return new Promise((resolve) => {
|
|
@@ -102,9 +136,13 @@ export function getSetupEpicUsage() {
|
|
|
102
136
|
" --review-policy <src> PER-RUN review policy source, one of:",
|
|
103
137
|
` ${SETUP_EPIC_REVIEW_POLICY_SOURCES.join(", ")}.`,
|
|
104
138
|
" Composed into policy_json.review_policy on create.",
|
|
105
|
-
" This
|
|
106
|
-
"
|
|
107
|
-
"
|
|
139
|
+
" This flag sets the policy for THIS run only and never",
|
|
140
|
+
" changes a repository default. To persist one, pass",
|
|
141
|
+
" --review-policy to `install-bridge conductor`, which",
|
|
142
|
+
" stores it as the supervisor project default; that",
|
|
143
|
+
" default is stamped into a new run only when the run",
|
|
144
|
+
" sets no review policy of its own, so a value supplied",
|
|
145
|
+
" here always wins.",
|
|
108
146
|
" --json Emit a single JSON result object on stdout",
|
|
109
147
|
" -h, --help Show this help",
|
|
110
148
|
"",
|
|
@@ -981,6 +1019,21 @@ export async function runSetupEpicCli(argv, overrides = {}) {
|
|
|
981
1019
|
say(`Local hash: ${localHash}`);
|
|
982
1020
|
for (const w of warnings)
|
|
983
1021
|
say(` [warn] ${w}`);
|
|
1022
|
+
// --- Active index-branch override (BAPI-814/E1a) -------------------------
|
|
1023
|
+
// Non-mutating enrichment, placed once credentials and repository identity are
|
|
1024
|
+
// both known and before any run-state read or mutating request.
|
|
1025
|
+
//
|
|
1026
|
+
// WARNING ONLY, never a refusal. The override is repository-wide: while it is
|
|
1027
|
+
// active every parse indexes that branch, and the create-PR config-field
|
|
1028
|
+
// fallback and feature-branch provisioning read the same overridden
|
|
1029
|
+
// `config_code_repositories.base_branch`. A v2 operator setting up an epic in
|
|
1030
|
+
// that repository needs to know, but the condition is legitimate — it is
|
|
1031
|
+
// usually another epic mid-flight — so setup continues.
|
|
1032
|
+
//
|
|
1033
|
+
// Emitted in `--dry-run` too: the condition already exists in the repository
|
|
1034
|
+
// and is independent of whether this invocation writes anything.
|
|
1035
|
+
for (const line of await describeActiveIndexOverride(access, deps))
|
|
1036
|
+
say(line);
|
|
984
1037
|
// --- Feature branch selection (BAPI-655) --------------------------------
|
|
985
1038
|
// Resolved AFTER local/access context is known but BEFORE any run-state read
|
|
986
1039
|
// or mutating request, so malformed interactive input fails before network
|
|
@@ -1171,7 +1224,7 @@ export async function runSetupEpicCli(argv, overrides = {}) {
|
|
|
1171
1224
|
say(` - feature branch: ${featureBranch} (create from repository base branch; no request made in dry-run)`);
|
|
1172
1225
|
}
|
|
1173
1226
|
if (opts.reviewPolicy !== undefined) {
|
|
1174
|
-
say(` - review policy: ${opts.reviewPolicy} (per-run policy_json.review_policy;
|
|
1227
|
+
say(` - review policy: ${opts.reviewPolicy} (per-run policy_json.review_policy; sets no repository default — use \`install-bridge conductor --review-policy\` for that)`);
|
|
1175
1228
|
}
|
|
1176
1229
|
say(` - POST /jira/epic-runs/runs/${opts.epicKey}/plan (v${plan.plan_version})`);
|
|
1177
1230
|
say(` - POST /jira/epic-runs/runs/${opts.epicKey}/approve-plan (v${plan.plan_version})`);
|
|
@@ -22,6 +22,13 @@
|
|
|
22
22
|
* that legitimately live inside the returned resources: an OCAPI attribute
|
|
23
23
|
* search document carries its own `count` and `hits`, and those stay untouched
|
|
24
24
|
* inside `items`.
|
|
25
|
+
*
|
|
26
|
+
* `page` is built strictly from properties of the collection envelope itself,
|
|
27
|
+
* never from a field promoted out of an individual item (BAPI-769). A bare
|
|
28
|
+
* object response is the one shape where this matters concretely: `items` is
|
|
29
|
+
* always that single object, so its own internal `total`/`count` describes hits
|
|
30
|
+
* *inside* the document, not a count of `items`, and is never attached to
|
|
31
|
+
* `page.total` — see the bare-object branch of `normalizeOcapiBody` below.
|
|
25
32
|
*/
|
|
26
33
|
/**
|
|
27
34
|
* Single constructor for page metadata, shared by both normalizers.
|
|
@@ -79,6 +86,15 @@ export function normalizeOcapiPage(body) {
|
|
|
79
86
|
* an attribute-search document with its own `count` and `hits` — becomes the
|
|
80
87
|
* single item unchanged; its `count` is never promoted to collection metadata,
|
|
81
88
|
* and a missing `hits` is never synthesized.
|
|
89
|
+
*
|
|
90
|
+
* `page` describes only the normalized `items` array; it is never built from a
|
|
91
|
+
* field promoted out of an item's own body (BAPI-769). The bare-object branch
|
|
92
|
+
* always produces exactly one item — the whole document — so a `total` count
|
|
93
|
+
* OCAPI reported about the document's own internal contents (e.g. a search
|
|
94
|
+
* document's zero-hit `total: 0`) is never attached to `page.total`, where it
|
|
95
|
+
* would misread as "0 of 1 returned item" for a wrapper whose `returned` is
|
|
96
|
+
* always 1. `page.total` is therefore absent on every bare-object response;
|
|
97
|
+
* `has_more` is unaffected — it still reflects the bare object's own `next`.
|
|
82
98
|
*/
|
|
83
99
|
export function normalizeOcapiBody(body) {
|
|
84
100
|
// Bare array
|
|
@@ -93,11 +109,14 @@ export function normalizeOcapiBody(body) {
|
|
|
93
109
|
const items = obj.data;
|
|
94
110
|
return { items, page: buildPageMeta(items, obj.total, obj.next) };
|
|
95
111
|
}
|
|
96
|
-
// Bare object (single resource, no wrapping envelope).
|
|
97
|
-
//
|
|
98
|
-
//
|
|
112
|
+
// Bare object (single resource, no wrapping envelope). `items` is always
|
|
113
|
+
// the single wrapper document, so `returned` is always 1 — a `total` (and,
|
|
114
|
+
// for a search document, `count`/`hits`) reported INSIDE that document is
|
|
115
|
+
// never promoted to `page.total`: it describes hits within the document,
|
|
116
|
+
// not a count of `items`. `page.total` is therefore always absent here
|
|
117
|
+
// (BAPI-769); `next` is still honored for `has_more`, which is unaffected.
|
|
99
118
|
const items = [body];
|
|
100
|
-
return { items, page: buildPageMeta(items,
|
|
119
|
+
return { items, page: buildPageMeta(items, undefined, obj.next) };
|
|
101
120
|
}
|
|
102
121
|
// Non-object (null, string, number, etc.)
|
|
103
122
|
return { items: [], page: buildPageMeta([], undefined, null) };
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Issues a harmless GET /system_object_definitions probe. On 200: reports the
|
|
5
5
|
* detected OCAPI version. On 401/403: prints the exact OCAPI Settings JSON to
|
|
6
|
-
* paste in Business Manager, split into read
|
|
7
|
-
*
|
|
6
|
+
* paste in Business Manager, split into read/search grants (for the `sfcc` read
|
|
7
|
+
* tools) and mutation grants (for the nine shipped `sfcc-write` tools).
|
|
8
8
|
*/
|
|
9
9
|
import { ocapiGet } from "./client.js";
|
|
10
10
|
import { normalizeOcapiPage } from "./ocapi-shape.js";
|
|
@@ -14,7 +14,7 @@ import { formatOcapiWriteGrantJson } from "./write-grants.js";
|
|
|
14
14
|
// OCAPI Settings JSON template
|
|
15
15
|
// Operators paste this in: Administration > Site Development > Open Commerce API Settings → Data API tab.
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
17
|
-
const
|
|
17
|
+
const OCAPI_SETTINGS_READ_SEARCH = (ocapiVersion) => JSON.stringify({
|
|
18
18
|
_v: ocapiVersion,
|
|
19
19
|
clients: [
|
|
20
20
|
{
|
|
@@ -138,12 +138,31 @@ function formatProbeCount(normalized) {
|
|
|
138
138
|
}
|
|
139
139
|
return `${returned} items returned on this page (OCAPI did not provide the instance total)`;
|
|
140
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Render the two grant blocks an operator pastes in Business Manager.
|
|
143
|
+
*
|
|
144
|
+
* Both labels were wrong before BAPI-817, in opposite directions:
|
|
145
|
+
*
|
|
146
|
+
* - The first block was labelled **"READ-ONLY GRANTS"** while actually
|
|
147
|
+
* granting `methods: ["get","post"]` with `write_attributes: "(**)"` across
|
|
148
|
+
* three resource families. An operator pasted a grant they had been told was
|
|
149
|
+
* read-only. It is renamed to read/search, which is what it is — the `post`
|
|
150
|
+
* is OCAPI's search convention (`*_search` endpoints are POSTs), not a
|
|
151
|
+
* mutation, but calling that "read-only" is still a claim about a grant the
|
|
152
|
+
* operator cannot verify without reading OCAPI's own semantics.
|
|
153
|
+
* - The second was labelled **"v2 — forward-looking"** for capabilities that
|
|
154
|
+
* had already shipped. All nine write tools exist; the grant is required
|
|
155
|
+
* today by anyone enabling `sfcc-write`, not speculative future work.
|
|
156
|
+
*
|
|
157
|
+
* A label that understates a grant is worse than no label, because the operator
|
|
158
|
+
* stops reading the JSON once the heading has told them it is safe.
|
|
159
|
+
*/
|
|
141
160
|
function ocapiSettingsInstructions(ocapiVersion) {
|
|
142
161
|
return (`To grant OCAPI access, paste the JSON below in Business Manager:\n` +
|
|
143
162
|
` Administration > Site Development > Open Commerce API Settings → Data API tab\n\n` +
|
|
144
|
-
`--- READ
|
|
145
|
-
`${
|
|
146
|
-
`---
|
|
163
|
+
`--- READ/SEARCH TOOL GRANTS (required for the sfcc read tools) ---\n` +
|
|
164
|
+
`${OCAPI_SETTINGS_READ_SEARCH(ocapiVersion)}\n\n` +
|
|
165
|
+
`--- MUTATION GRANTS (required for the nine sfcc-write tools) ---\n` +
|
|
147
166
|
`${OCAPI_SETTINGS_WRITE_IMPORT(ocapiVersion)}\n\n` +
|
|
148
167
|
`Replace <YOUR_CLIENT_ID> with the client_id from your dw.json.`);
|
|
149
168
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SFCC read-body redaction seam (BAPI-769).
|
|
3
|
+
*
|
|
4
|
+
* `projection: "full"` (BAPI-767) makes several OCAPI Data API endpoints return
|
|
5
|
+
* complete attribute-definition documents — `system_object_attribute_search`,
|
|
6
|
+
* `custom_object_definition_attribute_*`, and `system_object_definitions/{type}/
|
|
7
|
+
* attribute_definitions/{id}`. Live sandbox verification (OCAPI Data API v25_6)
|
|
8
|
+
* showed one of those 27 keys is not schema metadata about the attribute: it is
|
|
9
|
+
* the attribute's own runtime VALUE. `default_value` can carry a real
|
|
10
|
+
* configuration value — an attribute named `llmAvailableModelsJson` can carry
|
|
11
|
+
* actual model-routing config as its default — and that value must never reach
|
|
12
|
+
* an MCP caller, in the inline response, the `docs/tmp/sfcc/` save, or a
|
|
13
|
+
* successful attribute-definition write echo.
|
|
14
|
+
*
|
|
15
|
+
* This module owns exactly that: withholding one named structured field. It
|
|
16
|
+
* deliberately does NOT port `src/python/sfcc_monitor/adapters/redaction.py`'s
|
|
17
|
+
* regex/entropy engine — there is nothing to detect heuristically, only one key
|
|
18
|
+
* to withhold — and it deliberately does NOT redact inside `ocapi-shape.ts`.
|
|
19
|
+
* `normalizeOcapiBody`'s documented invariant is verbatim passthrough; folding
|
|
20
|
+
* redaction into it would make that no longer true. `readSfccBody` composes the
|
|
21
|
+
* two steps explicitly instead: redact first, normalize second.
|
|
22
|
+
*
|
|
23
|
+
* Attribute default values are intentionally unavailable through this MCP
|
|
24
|
+
* surface. Business Manager (Administration → Site Development → an object
|
|
25
|
+
* type's attribute definition) is the supported path to read one.
|
|
26
|
+
*/
|
|
27
|
+
import { normalizeOcapiBody } from "./ocapi-shape.js";
|
|
28
|
+
/**
|
|
29
|
+
* Marker substituted for a withheld `default_value` node.
|
|
30
|
+
*
|
|
31
|
+
* Deliberately distinct from the log-query system's own `[REDACTED]` marker
|
|
32
|
+
* (`sfcc/log-query.ts` and friends) — the two redaction seams are unrelated:
|
|
33
|
+
* this one withholds one named structured field before normalization, the log
|
|
34
|
+
* seam performs regex/entropy scrubbing of log text. Conflating the markers
|
|
35
|
+
* would make a caller unable to tell which seam produced a given redaction.
|
|
36
|
+
*/
|
|
37
|
+
export const BRIDGE_REDACTED_MARKER = "[REDACTED_BY_BRIDGE]";
|
|
38
|
+
/** True for a plain (non-array, non-null) object. */
|
|
39
|
+
function isPlainObject(value) {
|
|
40
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Recursively withhold every `default_value` key found anywhere in `body`.
|
|
44
|
+
*
|
|
45
|
+
* Whenever an object carries an own `default_value` key, that key's ENTIRE
|
|
46
|
+
* value — including the `object_attribute_value_definition` wrapper OCAPI
|
|
47
|
+
* shapes it as (`{ _type, id, position, value }`) — is replaced with
|
|
48
|
+
* `BRIDGE_REDACTED_MARKER`. The key itself is always kept; only its value
|
|
49
|
+
* changes. Recursion continues into every sibling and into the rest of the same
|
|
50
|
+
* object/array, so a document that embeds another attribute-definition-shaped
|
|
51
|
+
* object deeper in its tree is still covered — the live key survey found only
|
|
52
|
+
* one `default_value` per document, but the walker stays general rather than
|
|
53
|
+
* hard-coding that shape.
|
|
54
|
+
*
|
|
55
|
+
* Side-effect-free: `body` is never mutated. A fresh tree is built and
|
|
56
|
+
* returned, so the caller's original object is unchanged after the call. A
|
|
57
|
+
* `default_value` key that was never present on the source is never
|
|
58
|
+
* synthesized — absence stays absence.
|
|
59
|
+
*/
|
|
60
|
+
export function redactSfccBody(body) {
|
|
61
|
+
if (Array.isArray(body)) {
|
|
62
|
+
return body.map((item) => redactSfccBody(item));
|
|
63
|
+
}
|
|
64
|
+
if (isPlainObject(body)) {
|
|
65
|
+
const result = {};
|
|
66
|
+
for (const [key, value] of Object.entries(body)) {
|
|
67
|
+
// Object.entries only yields own enumerable properties, so reaching this
|
|
68
|
+
// branch already means `body` owns `default_value` — no key is ever added
|
|
69
|
+
// where the source never had one.
|
|
70
|
+
if (key === "default_value") {
|
|
71
|
+
result[key] = BRIDGE_REDACTED_MARKER;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
result[key] = redactSfccBody(value);
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
return body;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Redact, then normalize, a raw OCAPI response body.
|
|
82
|
+
*
|
|
83
|
+
* This is the read-path replacement for calling `normalizeOcapiBody` directly.
|
|
84
|
+
* Redaction happens BEFORE normalization, serialization, truncation, or save —
|
|
85
|
+
* one placement covers the inline MCP response and the `docs/tmp/sfcc/` save
|
|
86
|
+
* path identically, because both are derived from this same normalized result.
|
|
87
|
+
* `normalizeOcapiBody` itself remains a non-redacting verbatim shaper; it is
|
|
88
|
+
* `readSfccBody` that owns the redaction decision.
|
|
89
|
+
*/
|
|
90
|
+
export function readSfccBody(body) {
|
|
91
|
+
return normalizeOcapiBody(redactSfccBody(body));
|
|
92
|
+
}
|
|
@@ -81,12 +81,16 @@ const PROJECTION_POLICY = {
|
|
|
81
81
|
attribute_search: {
|
|
82
82
|
defaultProjection: "full",
|
|
83
83
|
placement: "body",
|
|
84
|
-
inputDescription: "Detail level. Default: full
|
|
84
|
+
inputDescription: "Detail level. Default: full — OCAPI's complete metadata projection " +
|
|
85
|
+
"(value_type, mandatory, localizable, searchable, site_specific); " +
|
|
86
|
+
"default_value is withheld. lean returns ids only.",
|
|
85
87
|
},
|
|
86
88
|
attribute_collection: {
|
|
87
89
|
defaultProjection: "full",
|
|
88
90
|
placement: "query",
|
|
89
|
-
inputDescription: "Detail level. Default: full
|
|
91
|
+
inputDescription: "Detail level. Default: full — OCAPI's complete metadata projection " +
|
|
92
|
+
"(value_type, mandatory, localizable, searchable, site_specific); " +
|
|
93
|
+
"default_value is withheld. lean returns ids only.",
|
|
90
94
|
},
|
|
91
95
|
broad_list: {
|
|
92
96
|
defaultProjection: "lean",
|
|
@@ -11,17 +11,31 @@
|
|
|
11
11
|
* OCAPI's CustomObjectDefinitions resource only exposes operations scoped to
|
|
12
12
|
* a known object_type — there is no top-level list and no bare get-by-type
|
|
13
13
|
* (unlike SystemObjectDefinitions). A caller-supplied object_type is always
|
|
14
|
-
* required; OCAPI cannot enumerate custom object
|
|
14
|
+
* required; OCAPI cannot enumerate custom object type IDs directly.
|
|
15
|
+
*
|
|
16
|
+
* That is narrower than "wholly undiscoverable," and the tools below say so:
|
|
17
|
+
* `system_object_list` at `projection: "full"` returns a distinct
|
|
18
|
+
* `display_name` and `attribute_definition_count` per custom type, from which a
|
|
19
|
+
* candidate ID can be derived (e.g. strip spaces from "Product Quality Result"
|
|
20
|
+
* → "ProductQualityResult") and confirmed by checking that this module's own
|
|
21
|
+
* `custom_object_definition_attributes_get` returns an attribute count matching
|
|
22
|
+
* that row's `attribute_definition_count`. This adds no new tool and no new
|
|
23
|
+
* projection value — it is existing capability, stated accurately.
|
|
15
24
|
*
|
|
16
25
|
* Note: custom object TYPE creation/update is NOT available via OCAPI REST.
|
|
17
26
|
* It is a v2 metadata-import capability (metadata XML → WebDAV → import job).
|
|
27
|
+
*
|
|
28
|
+
* Both tools can return complete attribute-definition documents at
|
|
29
|
+
* `projection: "full"` (BAPI-767). Bridge withholds each attribute's
|
|
30
|
+
* `default_value` from that response (BAPI-769, `read-body.ts`) — every other
|
|
31
|
+
* schema field (type, flags, labels) is unaffected.
|
|
18
32
|
*/
|
|
19
33
|
import path from "path";
|
|
20
34
|
import { z } from "zod";
|
|
21
35
|
import { ocapiGet, ocapiPost } from "./client.js";
|
|
22
36
|
import { withSfccGate } from "./tool-wrapper.js";
|
|
23
37
|
import { formatOcapiReadFailure, withSfccReadErrorBoundary } from "./read-result.js";
|
|
24
|
-
import {
|
|
38
|
+
import { readSfccBody } from "./read-body.js";
|
|
25
39
|
import { applyProjectionToBody, projectionInputFor, projectionQueryEntries, } from "./read-projection.js";
|
|
26
40
|
import { truncateAndSaveIfNeeded } from "./output.js";
|
|
27
41
|
// ---------------------------------------------------------------------------
|
|
@@ -36,18 +50,17 @@ const READ_ANNOTATIONS = {
|
|
|
36
50
|
// ---------------------------------------------------------------------------
|
|
37
51
|
// Input schemas
|
|
38
52
|
// ---------------------------------------------------------------------------
|
|
53
|
+
const OBJECT_TYPE_DISCOVERY_HINT = 'Known custom object type id (e.g. starting with "c_"). OCAPI cannot enumerate ' +
|
|
54
|
+
"type IDs directly, but you can discover a candidate via system_object_list at " +
|
|
55
|
+
"projection=\"full\" (distinct display_name + attribute_definition_count per " +
|
|
56
|
+
"custom type) and confirm it by checking that this tool's returned attribute " +
|
|
57
|
+
"count matches that row's attribute_definition_count.";
|
|
39
58
|
const customObjectAttributesGetInput = z.object({
|
|
40
|
-
object_type: z
|
|
41
|
-
.string()
|
|
42
|
-
.describe('Known custom object type identifier, e.g. a custom type id starting with "c_". ' +
|
|
43
|
-
"OCAPI cannot enumerate custom object types — the type must already be known."),
|
|
59
|
+
object_type: z.string().describe(OBJECT_TYPE_DISCOVERY_HINT),
|
|
44
60
|
projection: projectionInputFor("attribute_collection"),
|
|
45
61
|
});
|
|
46
62
|
const customObjectAttributeSearchInput = z.object({
|
|
47
|
-
object_type: z
|
|
48
|
-
.string()
|
|
49
|
-
.describe('Known custom object type to search within, e.g. a custom type id starting with "c_". ' +
|
|
50
|
-
"OCAPI cannot enumerate custom object types — the type must already be known."),
|
|
63
|
+
object_type: z.string().describe(OBJECT_TYPE_DISCOVERY_HINT),
|
|
51
64
|
query: z.union([z.string(), z.record(z.string(), z.any())]).describe("Search query. A plain string is a case-insensitive substring match across " +
|
|
52
65
|
"id and display_name; a structured OCAPI query object passes through unchanged."),
|
|
53
66
|
start: z.number().optional().describe("Zero-based offset for paging."),
|
|
@@ -86,7 +99,7 @@ function buildCustomObjectAttributesGetHandler(gateDeps, getDocsDir) {
|
|
|
86
99
|
if (!result.ok) {
|
|
87
100
|
return formatOcapiReadFailure(result);
|
|
88
101
|
}
|
|
89
|
-
const normalized =
|
|
102
|
+
const normalized = readSfccBody(result.body);
|
|
90
103
|
const text = JSON.stringify(normalized, null, 2);
|
|
91
104
|
const dir = path.join(await getDocsDir(), "sfcc");
|
|
92
105
|
return saveAndReturn(text, dir, `custom-object-def-attributes-${safeType(object_type)}-${safeTimestamp()}.json`, normalized.page);
|
|
@@ -112,7 +125,7 @@ function buildCustomObjectAttributeSearchHandler(gateDeps, getDocsDir) {
|
|
|
112
125
|
if (!result.ok) {
|
|
113
126
|
return formatOcapiReadFailure(result);
|
|
114
127
|
}
|
|
115
|
-
const normalized =
|
|
128
|
+
const normalized = readSfccBody(result.body);
|
|
116
129
|
const text = JSON.stringify(normalized, null, 2);
|
|
117
130
|
const dir = path.join(await getDocsDir(), "sfcc");
|
|
118
131
|
return saveAndReturn(text, dir, `custom-object-def-search-${safeType(object_type)}-${safeTimestamp()}.json`, normalized.page);
|
|
@@ -127,20 +140,19 @@ export function registerSfccCustomObjectDefReadTools(registerTool, deps) {
|
|
|
127
140
|
const { gateDeps, getDocsDir } = deps;
|
|
128
141
|
registerTool("custom_object_definition_attributes_get", {
|
|
129
142
|
description: "Retrieve attribute definitions for a KNOWN custom object type via " +
|
|
130
|
-
"GET /custom_object_definitions/{type}/attribute_definitions
|
|
131
|
-
"
|
|
132
|
-
"
|
|
133
|
-
"Type creation is v2 metadata-import only.
|
|
143
|
+
"GET /custom_object_definitions/{type}/attribute_definitions " +
|
|
144
|
+
"(default_value withheld). OCAPI cannot enumerate type IDs directly; " +
|
|
145
|
+
"derive one via system_object_list (projection=full, attribute_definition_count), " +
|
|
146
|
+
"confirm here. Type creation is v2 metadata-import only.",
|
|
134
147
|
inputSchema: customObjectAttributesGetInput,
|
|
135
148
|
annotations: READ_ANNOTATIONS,
|
|
136
149
|
}, buildCustomObjectAttributesGetHandler(gateDeps, getDocsDir));
|
|
137
150
|
registerTool("custom_object_definition_attribute_search", {
|
|
138
151
|
description: "Search attribute definitions within a KNOWN custom object type via " +
|
|
139
|
-
"POST /custom_object_definitions/{type}/attribute_definition_search
|
|
140
|
-
"
|
|
141
|
-
"
|
|
142
|
-
"
|
|
143
|
-
"Oversized results auto-saved locally.",
|
|
152
|
+
"POST /custom_object_definitions/{type}/attribute_definition_search " +
|
|
153
|
+
"(default_value withheld). OCAPI cannot enumerate type IDs directly; " +
|
|
154
|
+
"derive one via system_object_list (projection=full, attribute_definition_count), " +
|
|
155
|
+
"confirm here. Type creation is v2 metadata-import only.",
|
|
144
156
|
inputSchema: customObjectAttributeSearchInput,
|
|
145
157
|
annotations: READ_ANNOTATIONS,
|
|
146
158
|
}, buildCustomObjectAttributeSearchHandler(gateDeps, getDocsDir));
|
|
@@ -24,11 +24,18 @@
|
|
|
24
24
|
* It is also, for now, accidentally protective. A name-heuristic scan of all 19
|
|
25
25
|
* preference groups on the tested sandbox found secret-named preferences in six
|
|
26
26
|
* of them — including one literally named `client_id` — and none of them expose
|
|
27
|
-
* a value through this endpoint.
|
|
28
|
-
* is no value-bearing path to redact. If OCAPI ever starts returning values,
|
|
27
|
+
* a value through this endpoint. If OCAPI ever starts returning values here,
|
|
29
28
|
* redaction must be added on the raw body BEFORE normalization, serialization,
|
|
30
29
|
* truncation, or save, so one placement covers both the inline response and the
|
|
31
|
-
* docs/tmp/sfcc/ save path
|
|
30
|
+
* docs/tmp/sfcc/ save path — exactly the `readSfccBody` seam these three
|
|
31
|
+
* handlers already route through (BAPI-769).
|
|
32
|
+
*
|
|
33
|
+
* That value-bearing route already exists elsewhere: `SitePreferences`
|
|
34
|
+
* attribute-definition reads via `system_object_attribute_search` (with
|
|
35
|
+
* `object_type: "SitePreferences"`) can return each preference's
|
|
36
|
+
* `default_value`, and `readSfccBody` withholds it there today. This module's
|
|
37
|
+
* three preference-value tools remain ids-only for the reason stated above —
|
|
38
|
+
* OCAPI's `preference_search` itself never returns a value, `select` or not.
|
|
32
39
|
*
|
|
33
40
|
* NOTE: SCAPI Preferences API (sfcc.preferences) is intentionally NOT used.
|
|
34
41
|
* This keeps the tool context on the OCAPI Data API, preserving the single
|
|
@@ -40,7 +47,7 @@ import { z } from "zod";
|
|
|
40
47
|
import { ocapiGet, ocapiPost } from "./client.js";
|
|
41
48
|
import { withSfccGate } from "./tool-wrapper.js";
|
|
42
49
|
import { formatOcapiReadFailure, formatSfccReadFailure, withSfccReadErrorBoundary, } from "./read-result.js";
|
|
43
|
-
import {
|
|
50
|
+
import { readSfccBody } from "./read-body.js";
|
|
44
51
|
import { projectionInputFor, projectionQueryEntries } from "./read-projection.js";
|
|
45
52
|
import { truncateAndSaveIfNeeded } from "./output.js";
|
|
46
53
|
// ---------------------------------------------------------------------------
|
|
@@ -106,6 +113,12 @@ async function saveAndReturn(text, dir, filename, page) {
|
|
|
106
113
|
* an API version while OCAPI simultaneously reports `_v: "25.6"` in the same
|
|
107
114
|
* session; the constraint belongs to this Bridge tool, not to a version. The
|
|
108
115
|
* status, code, source, decision, and `details` payload are untouched.
|
|
116
|
+
*
|
|
117
|
+
* ⚠️ BAPI-817: like `write-guard.ts#rejectIfNotSandboxForWrite`, this checks a
|
|
118
|
+
* caller-supplied OCAPI instance-CONTEXT argument, NOT the host that will be
|
|
119
|
+
* contacted. It is secondary. The instance actually reached is decided by the
|
|
120
|
+
* resolved-hostname boundary in `tool-wrapper.ts`, which runs before this and
|
|
121
|
+
* does not consult `instance`.
|
|
109
122
|
*/
|
|
110
123
|
function rejectIfNotSandbox(instance) {
|
|
111
124
|
if (instance !== "sandbox") {
|
|
@@ -145,7 +158,7 @@ export function buildSitePreferenceGetHandler(gateDeps, getDocsDir) {
|
|
|
145
158
|
if (!result.ok) {
|
|
146
159
|
return formatOcapiReadFailure(result);
|
|
147
160
|
}
|
|
148
|
-
const normalized =
|
|
161
|
+
const normalized = readSfccBody(result.body);
|
|
149
162
|
const text = JSON.stringify(normalized, null, 2);
|
|
150
163
|
const dir = path.join(await getDocsDir(), "sfcc");
|
|
151
164
|
return saveAndReturn(text, dir, `site-preference-get-${safeGroup(group)}-${safeTimestamp()}.json`, normalized.page);
|
|
@@ -173,7 +186,7 @@ export function buildSitePreferenceSearchHandler(gateDeps, getDocsDir) {
|
|
|
173
186
|
if (!result.ok) {
|
|
174
187
|
return formatOcapiReadFailure(result);
|
|
175
188
|
}
|
|
176
|
-
const normalized =
|
|
189
|
+
const normalized = readSfccBody(result.body);
|
|
177
190
|
const text = JSON.stringify(normalized, null, 2);
|
|
178
191
|
const dir = path.join(await getDocsDir(), "sfcc");
|
|
179
192
|
return saveAndReturn(text, dir, `site-preference-search-${safeGroup(group)}-${safeTimestamp()}.json`, normalized.page);
|
|
@@ -196,7 +209,7 @@ export function buildSitePreferenceGroupListHandler(gateDeps, getDocsDir) {
|
|
|
196
209
|
if (!result.ok) {
|
|
197
210
|
return formatOcapiReadFailure(result);
|
|
198
211
|
}
|
|
199
|
-
const normalized =
|
|
212
|
+
const normalized = readSfccBody(result.body);
|
|
200
213
|
const text = JSON.stringify(normalized, null, 2);
|
|
201
214
|
const dir = path.join(await getDocsDir(), "sfcc");
|
|
202
215
|
return saveAndReturn(text, dir, `site-preference-group-list-${safeTimestamp()}.json`, normalized.page);
|
|
@@ -8,13 +8,18 @@
|
|
|
8
8
|
*
|
|
9
9
|
* All tools are read-only, run behind the T1 call-time gate, and route
|
|
10
10
|
* oversized payloads through the sfcc/output.ts truncate-and-save seam.
|
|
11
|
+
*
|
|
12
|
+
* `system_object_attribute_search` can return complete attribute-definition
|
|
13
|
+
* documents at `projection: "full"` (BAPI-767). Bridge withholds each
|
|
14
|
+
* attribute's `default_value` from that response (BAPI-769, `read-body.ts`) —
|
|
15
|
+
* every other schema field (type, flags, labels) is unaffected.
|
|
11
16
|
*/
|
|
12
17
|
import path from "path";
|
|
13
18
|
import { z } from "zod";
|
|
14
19
|
import { ocapiGet, ocapiPost } from "./client.js";
|
|
15
20
|
import { withSfccGate } from "./tool-wrapper.js";
|
|
16
21
|
import { formatOcapiReadFailure, withSfccReadErrorBoundary } from "./read-result.js";
|
|
17
|
-
import {
|
|
22
|
+
import { readSfccBody } from "./read-body.js";
|
|
18
23
|
import { applyProjectionToBody, projectionInputFor, projectionQueryEntries, } from "./read-projection.js";
|
|
19
24
|
import { truncateAndSaveIfNeeded } from "./output.js";
|
|
20
25
|
// ---------------------------------------------------------------------------
|
|
@@ -86,7 +91,7 @@ function buildSystemObjectListHandler(gateDeps, getDocsDir) {
|
|
|
86
91
|
if (!result.ok) {
|
|
87
92
|
return formatOcapiReadFailure(result);
|
|
88
93
|
}
|
|
89
|
-
const normalized =
|
|
94
|
+
const normalized = readSfccBody(result.body);
|
|
90
95
|
const text = JSON.stringify(normalized, null, 2);
|
|
91
96
|
const dir = path.join(await getDocsDir(), "sfcc");
|
|
92
97
|
return saveAndReturn(text, dir, `system-object-list-${safeTimestamp()}.json`, normalized.page);
|
|
@@ -100,7 +105,7 @@ function buildSystemObjectGetHandler(gateDeps, getDocsDir) {
|
|
|
100
105
|
if (!result.ok) {
|
|
101
106
|
return formatOcapiReadFailure(result);
|
|
102
107
|
}
|
|
103
|
-
const normalized =
|
|
108
|
+
const normalized = readSfccBody(result.body);
|
|
104
109
|
const text = JSON.stringify(normalized, null, 2);
|
|
105
110
|
const dir = path.join(await getDocsDir(), "sfcc");
|
|
106
111
|
return saveAndReturn(text, dir, `system-object-get-${safeType(object_type)}-${safeTimestamp()}.json`, normalized.page);
|
|
@@ -126,7 +131,7 @@ function buildSystemObjectAttributeSearchHandler(gateDeps, getDocsDir) {
|
|
|
126
131
|
if (!result.ok) {
|
|
127
132
|
return formatOcapiReadFailure(result);
|
|
128
133
|
}
|
|
129
|
-
const normalized =
|
|
134
|
+
const normalized = readSfccBody(result.body);
|
|
130
135
|
const text = JSON.stringify(normalized, null, 2);
|
|
131
136
|
const dir = path.join(await getDocsDir(), "sfcc");
|
|
132
137
|
return saveAndReturn(text, dir, `system-object-search-${safeType(object_type)}-${safeTimestamp()}.json`, normalized.page);
|
|
@@ -159,7 +164,8 @@ export function registerSystemObjectReadTools(registerTool, deps) {
|
|
|
159
164
|
registerTool("system_object_attribute_search", {
|
|
160
165
|
description: "Search attribute definitions for a system object type. Read-only. " +
|
|
161
166
|
"POST /system_object_definitions/{type}/attribute_definition_search. " +
|
|
162
|
-
"Use for targeted c_ attribute lookups; returns each attribute's
|
|
167
|
+
"Use for targeted c_ attribute lookups; returns each attribute's schema " +
|
|
168
|
+
"by default (default_value withheld). " +
|
|
163
169
|
"Pass a plain string for text search or a structured OCAPI query. " +
|
|
164
170
|
"Oversized results auto-saved locally.",
|
|
165
171
|
inputSchema: systemObjectAttributeSearchInput,
|