@bridge_gpt/mcp-server 0.2.36 → 0.2.37
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 +44 -6
- package/build/base-url.js +79 -0
- package/build/bridge-api-urls.js +9 -0
- package/build/chain-orchestrator.js +93 -15
- package/build/claude-user-config-doctor.js +317 -0
- package/build/commands.generated.js +2 -1
- package/build/conductor/bridge-api-client.js +178 -4
- package/build/conductor-bin.js +1 -1
- package/build/conductor-bundle-artifacts.js +7 -6
- package/build/credential-store.js +205 -4
- package/build/direct-ticket-tools.js +70 -0
- package/build/doctor.js +239 -80
- package/build/executor/cli.js +51 -1
- package/build/executor/credentials.js +1 -7
- package/build/executor/deps.js +18 -1
- package/build/executor/env.js +51 -25
- package/build/executor/heartbeat.js +138 -17
- package/build/executor/http-client.js +49 -8
- package/build/executor/job-errors.js +4 -0
- package/build/executor/job-runner.js +422 -22
- package/build/executor/observation.js +130 -0
- package/build/executor/permissions.js +104 -8
- package/build/executor/preflight.js +32 -0
- package/build/executor/runner.js +8 -0
- package/build/executor/test-clock.js +67 -3
- package/build/executor/types.js +4 -1
- package/build/executor/worker-command.js +11 -3
- package/build/executor/worker-config-isolation.js +287 -0
- package/build/executor/worker-finalization.js +68 -14
- package/build/executor/worktree.js +46 -4
- package/build/index.js +614 -244
- package/build/init.js +363 -73
- package/build/install-bridge.js +568 -80
- package/build/launcher-config-inspection.js +351 -0
- package/build/mcp-invoke.js +49 -6
- package/build/mcp-provisioning.js +30 -7
- package/build/mcp-registration-doctor.js +14 -5
- package/build/notifications.js +553 -0
- package/build/pipeline-orchestrator.js +146 -4
- package/build/pipeline-utils.js +3 -0
- package/build/pipelines.generated.js +22 -9
- package/build/plan-execution-ledger.js +550 -0
- package/build/plan-phase-routing.js +272 -0
- package/build/plane/alembic-head.js +110 -0
- package/build/plane/build-freshness.js +167 -0
- package/build/plane/cli.js +480 -0
- package/build/plane/defaults.js +266 -0
- package/build/plane/manifest.js +377 -0
- package/build/plane/member-logs.js +147 -0
- package/build/plane/member-roster.js +147 -0
- package/build/plane/preflight.js +289 -0
- package/build/plane/shutdown.js +195 -0
- package/build/plane/status.js +125 -0
- package/build/plane/supervisor.js +569 -0
- package/build/plane/test-fakes.js +156 -0
- package/build/plane/types.js +75 -0
- package/build/readme.generated.js +1 -1
- package/build/run-unit-tests-launcher.js +2 -0
- package/build/setup-epic.js +662 -27
- package/build/sfcc/log-gate.js +38 -11
- package/build/sfcc/log-query.js +55 -15
- package/build/sfcc/ocapi-shape.js +51 -14
- package/build/sfcc/output.js +41 -11
- package/build/sfcc/permissions.js +24 -2
- package/build/sfcc/read-projection.js +181 -0
- package/build/sfcc/read-result.js +158 -0
- package/build/sfcc/reads-custom-object-def.js +29 -18
- package/build/sfcc/reads-site-preference.js +75 -29
- package/build/sfcc/reads-system-object.js +40 -34
- package/build/sfcc/sfcc-result.js +106 -0
- package/build/sfcc/tool-wrapper.js +56 -13
- package/build/sfcc/write-grants.js +45 -22
- package/build/sfcc/write-guard.js +21 -13
- package/build/sfcc/write-result.js +61 -14
- package/build/sfcc/write-tool-common.js +126 -32
- package/build/sfcc/writes-system-object.js +11 -50
- package/build/start-tickets-prereqs.js +129 -0
- package/build/start-tickets.js +17 -13
- package/build/ticket-backend-metadata.js +59 -0
- package/build/ticket-key-utils.js +92 -0
- package/build/tool-error-envelope.js +71 -0
- package/build/tool-surface-gating.js +72 -0
- package/build/update-status.js +102 -0
- package/build/upgrade-advice.js +47 -0
- package/build/upgrade-cli.js +417 -101
- package/build/version.generated.js +1 -1
- package/build/worktree-core.js +73 -0
- package/docs/CONDUCTOR.md +23 -8
- package/package.json +3 -3
- package/pipelines/implement-ticket.json +15 -5
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SFCC read boundary, layered on the universal SFCC result contract (BAPI-759,
|
|
3
|
+
* generalized by BAPI-766).
|
|
4
|
+
*
|
|
5
|
+
* This module owns the *read-specific* concerns: OCAPI fault-message extraction,
|
|
6
|
+
* the read-boundary error wrapper, and the fixed read messages. The envelope
|
|
7
|
+
* itself — normalization, serialization, and the `isError` predicate — lives in
|
|
8
|
+
* `sfcc-result.ts` and is shared with the write and WebDAV log surfaces, so
|
|
9
|
+
* there is exactly one implementation for the whole SFCC surface.
|
|
10
|
+
*
|
|
11
|
+
* Read handlers never construct an error envelope inline — they call
|
|
12
|
+
* `formatSfccReadFailure`, which is re-exported here as a thin delegate to the
|
|
13
|
+
* shared formatter.
|
|
14
|
+
*
|
|
15
|
+
* The unified guarantees (`isError: true` on every failure, and a body that
|
|
16
|
+
* parses as `{ error: { code, status, message, source, details } }`) are
|
|
17
|
+
* documented in `sfcc-result.ts`. An object-valued OCAPI body is preserved
|
|
18
|
+
* verbatim under `error.details` (`_v`, `fault.type`, `fault.message`,
|
|
19
|
+
* `fault.arguments` and any other upstream field), so upstream data is never
|
|
20
|
+
* discarded and never collides with the Bridge-owned envelope fields.
|
|
21
|
+
*/
|
|
22
|
+
import { z } from "zod";
|
|
23
|
+
import { formatSfccFailure } from "./sfcc-result.js";
|
|
24
|
+
export { formatSfccReadFailure } from "./sfcc-result.js";
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Fixed messages
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
/**
|
|
29
|
+
* The one message returned for an unexpected throw. Bridge-authored and fixed:
|
|
30
|
+
* the thrown value is never serialized, so a credential, token, request header,
|
|
31
|
+
* or stack frame carried by the exception cannot reach the caller.
|
|
32
|
+
*/
|
|
33
|
+
export const SFCC_INTERNAL_ERROR_MESSAGE = "An unexpected internal error occurred while handling this SFCC read. " +
|
|
34
|
+
"The underlying failure detail is withheld from this response to avoid " +
|
|
35
|
+
"leaking credentials or internal state.";
|
|
36
|
+
/** The one message returned when arguments fail schema validation at the read boundary. */
|
|
37
|
+
export const SFCC_VALIDATION_ERROR_MESSAGE = "Arguments failed schema validation before any OCAPI call was made. " +
|
|
38
|
+
"See error.details.issues for the offending fields.";
|
|
39
|
+
/** Bridge-authored fallback used when an OCAPI body carries no usable `fault.message`. */
|
|
40
|
+
export function ocapiFallbackMessage(status) {
|
|
41
|
+
return (`The SFCC OCAPI Data API returned HTTP ${status} for this read and supplied no ` +
|
|
42
|
+
`fault message. The upstream response body is preserved under error.details.`);
|
|
43
|
+
}
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// OCAPI read failures
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
/** True for a plain (non-array, non-null) object. */
|
|
48
|
+
function isPlainObject(value) {
|
|
49
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Format a non-OK OCAPI read response through the shared formatter.
|
|
53
|
+
*
|
|
54
|
+
* The message is the upstream `fault.message` when it is a non-empty string,
|
|
55
|
+
* otherwise a stable Bridge-authored fallback — raw response text is never
|
|
56
|
+
* promoted into `error.message`. The whole upstream body is preserved under
|
|
57
|
+
* `error.details`.
|
|
58
|
+
*
|
|
59
|
+
* `upstreamHttpStatus` exists for the case where Bridge reports a status other
|
|
60
|
+
* than the one HTTP returned. The OCAPI client does not currently normalize a
|
|
61
|
+
* read status (`ocapiGet`/`ocapiPost` surface `Response.status` verbatim), so
|
|
62
|
+
* in practice the two are equal and the field is omitted; the parameter keeps
|
|
63
|
+
* the seam honest if a normalization step is ever introduced.
|
|
64
|
+
*/
|
|
65
|
+
export function formatOcapiReadFailure(result, upstreamHttpStatus) {
|
|
66
|
+
return formatSfccFailure({
|
|
67
|
+
code: "OCAPI_FAULT",
|
|
68
|
+
status: result.status,
|
|
69
|
+
message: ocapiFaultMessage(result.body, result.status),
|
|
70
|
+
source: "ocapi",
|
|
71
|
+
details: result.body,
|
|
72
|
+
upstreamHttpStatus,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/** Pull a usable human-readable message out of an OCAPI fault body. */
|
|
76
|
+
function ocapiFaultMessage(body, status) {
|
|
77
|
+
if (isPlainObject(body) && isPlainObject(body.fault)) {
|
|
78
|
+
const faultMessage = body.fault.message;
|
|
79
|
+
if (typeof faultMessage === "string" && faultMessage.trim() !== "") {
|
|
80
|
+
return faultMessage;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return ocapiFallbackMessage(status);
|
|
84
|
+
}
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Read boundary
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
/**
|
|
89
|
+
* Recognize a Zod/schema failure without matching on the exception message.
|
|
90
|
+
*
|
|
91
|
+
* `instanceof` covers the normal case. The structural fallback covers a
|
|
92
|
+
* ZodError produced by a second copy of Zod (a duplicated dependency or a
|
|
93
|
+
* module-mocked import), where the prototype chain differs but the contract
|
|
94
|
+
* — a `ZodError` name plus an `issues` array — does not.
|
|
95
|
+
*/
|
|
96
|
+
function isSchemaFailure(err) {
|
|
97
|
+
if (err instanceof z.ZodError)
|
|
98
|
+
return true;
|
|
99
|
+
return (typeof err === "object" &&
|
|
100
|
+
err !== null &&
|
|
101
|
+
err.name === "ZodError" &&
|
|
102
|
+
Array.isArray(err.issues));
|
|
103
|
+
}
|
|
104
|
+
/** Reduce Zod issues to field path + code + message; nothing else is disclosed. */
|
|
105
|
+
function safeZodIssues(err) {
|
|
106
|
+
const issues = Array.isArray(err.issues) ? err.issues : [];
|
|
107
|
+
return issues.map((issue) => ({
|
|
108
|
+
path: Array.isArray(issue.path) ? issue.path.map(String).join(".") : "",
|
|
109
|
+
code: typeof issue.code === "string" ? issue.code : "invalid",
|
|
110
|
+
message: typeof issue.message === "string" ? issue.message : "Invalid input.",
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Wrap a read tool callback so no failure escapes as a raw throw.
|
|
115
|
+
*
|
|
116
|
+
* This is the outermost project-owned callback for every SFCC read tool. It
|
|
117
|
+
* converts:
|
|
118
|
+
*
|
|
119
|
+
* - a recognized Zod/schema failure → `VALIDATION_ERROR` (400, source `tool`)
|
|
120
|
+
* - any other thrown value → `INTERNAL_ERROR` (500, source `tool`)
|
|
121
|
+
*
|
|
122
|
+
* A result the inner handler *returns* — success or an already-formatted
|
|
123
|
+
* failure — is passed through untouched, so a gate rejection keeps its
|
|
124
|
+
* `NOT_CONFIGURED` code rather than being reclassified.
|
|
125
|
+
*
|
|
126
|
+
* Note on ordering: the MCP SDK validates `inputSchema` before invoking this
|
|
127
|
+
* callback and turns a rejection into its own `isError: true` result, so the
|
|
128
|
+
* transport path is already a tool result rather than a transport error. This
|
|
129
|
+
* boundary covers the paths the SDK does not: the handler's own `schema.parse`,
|
|
130
|
+
* in-process dispatch through `TOOL_HANDLERS` (which bypasses SDK validation
|
|
131
|
+
* entirely), and every unexpected throw from the gate, the OCAPI client, or the
|
|
132
|
+
* output seam.
|
|
133
|
+
*/
|
|
134
|
+
export function withSfccReadErrorBoundary(handler) {
|
|
135
|
+
return async (...args) => {
|
|
136
|
+
try {
|
|
137
|
+
return await handler(...args);
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
if (isSchemaFailure(err)) {
|
|
141
|
+
return formatSfccFailure({
|
|
142
|
+
code: "VALIDATION_ERROR",
|
|
143
|
+
status: 400,
|
|
144
|
+
message: SFCC_VALIDATION_ERROR_MESSAGE,
|
|
145
|
+
source: "tool",
|
|
146
|
+
details: { issues: safeZodIssues(err) },
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return formatSfccFailure({
|
|
150
|
+
code: "INTERNAL_ERROR",
|
|
151
|
+
status: 500,
|
|
152
|
+
message: SFCC_INTERNAL_ERROR_MESSAGE,
|
|
153
|
+
source: "tool",
|
|
154
|
+
details: {},
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -20,7 +20,9 @@ import path from "path";
|
|
|
20
20
|
import { z } from "zod";
|
|
21
21
|
import { ocapiGet, ocapiPost } from "./client.js";
|
|
22
22
|
import { withSfccGate } from "./tool-wrapper.js";
|
|
23
|
+
import { formatOcapiReadFailure, withSfccReadErrorBoundary } from "./read-result.js";
|
|
23
24
|
import { normalizeOcapiBody } from "./ocapi-shape.js";
|
|
25
|
+
import { applyProjectionToBody, projectionInputFor, projectionQueryEntries, } from "./read-projection.js";
|
|
24
26
|
import { truncateAndSaveIfNeeded } from "./output.js";
|
|
25
27
|
// ---------------------------------------------------------------------------
|
|
26
28
|
// Annotations (SPEC: read-only against developer sandboxes)
|
|
@@ -39,17 +41,19 @@ const customObjectAttributesGetInput = z.object({
|
|
|
39
41
|
.string()
|
|
40
42
|
.describe('Known custom object type identifier, e.g. a custom type id starting with "c_". ' +
|
|
41
43
|
"OCAPI cannot enumerate custom object types — the type must already be known."),
|
|
44
|
+
projection: projectionInputFor("attribute_collection"),
|
|
42
45
|
});
|
|
43
46
|
const customObjectAttributeSearchInput = z.object({
|
|
44
47
|
object_type: z
|
|
45
48
|
.string()
|
|
46
49
|
.describe('Known custom object type to search within, e.g. a custom type id starting with "c_". ' +
|
|
47
50
|
"OCAPI cannot enumerate custom object types — the type must already be known."),
|
|
48
|
-
query: z.union([z.string(), z.record(z.string(), z.any())]).describe("Search query.
|
|
49
|
-
"
|
|
51
|
+
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
|
+
"id and display_name; a structured OCAPI query object passes through unchanged."),
|
|
50
53
|
start: z.number().optional().describe("Zero-based offset for paging."),
|
|
51
54
|
count: z.number().optional().describe("Maximum number of results to return."),
|
|
52
55
|
sorts: z.array(z.any()).optional().describe("Array of OCAPI sort descriptors."),
|
|
56
|
+
projection: projectionInputFor("attribute_search"),
|
|
53
57
|
});
|
|
54
58
|
// ---------------------------------------------------------------------------
|
|
55
59
|
// Helpers
|
|
@@ -63,30 +67,34 @@ function safeType(objectType) {
|
|
|
63
67
|
function textResult(text) {
|
|
64
68
|
return { content: [{ type: "text", text }] };
|
|
65
69
|
}
|
|
66
|
-
async function saveAndReturn(text, dir, filename) {
|
|
67
|
-
const output = await truncateAndSaveIfNeeded(text, dir, filename);
|
|
70
|
+
async function saveAndReturn(text, dir, filename, page) {
|
|
71
|
+
const output = await truncateAndSaveIfNeeded(text, dir, filename, page);
|
|
68
72
|
return textResult(output);
|
|
69
73
|
}
|
|
70
74
|
// ---------------------------------------------------------------------------
|
|
71
75
|
// Handlers
|
|
72
76
|
// ---------------------------------------------------------------------------
|
|
73
77
|
function buildCustomObjectAttributesGetHandler(gateDeps, getDocsDir) {
|
|
74
|
-
return withSfccGate(gateDeps, async (args, credentials) => {
|
|
75
|
-
const { object_type } = customObjectAttributesGetInput.parse(args);
|
|
78
|
+
return withSfccReadErrorBoundary(withSfccGate(gateDeps, async (args, credentials) => {
|
|
79
|
+
const { object_type, projection } = customObjectAttributesGetInput.parse(args);
|
|
76
80
|
const encodedType = encodeURIComponent(object_type);
|
|
77
|
-
const
|
|
81
|
+
const queryEntries = projectionQueryEntries("attribute_collection", projection);
|
|
82
|
+
const queryStr = Object.keys(queryEntries).length > 0
|
|
83
|
+
? "?" + new URLSearchParams(queryEntries).toString()
|
|
84
|
+
: "";
|
|
85
|
+
const result = await ocapiGet(`/custom_object_definitions/${encodedType}/attribute_definitions${queryStr}`, credentials);
|
|
78
86
|
if (!result.ok) {
|
|
79
|
-
return
|
|
87
|
+
return formatOcapiReadFailure(result);
|
|
80
88
|
}
|
|
81
89
|
const normalized = normalizeOcapiBody(result.body);
|
|
82
90
|
const text = JSON.stringify(normalized, null, 2);
|
|
83
91
|
const dir = path.join(await getDocsDir(), "sfcc");
|
|
84
|
-
return saveAndReturn(text, dir, `custom-object-def-attributes-${safeType(object_type)}-${safeTimestamp()}.json
|
|
85
|
-
});
|
|
92
|
+
return saveAndReturn(text, dir, `custom-object-def-attributes-${safeType(object_type)}-${safeTimestamp()}.json`, normalized.page);
|
|
93
|
+
}));
|
|
86
94
|
}
|
|
87
95
|
function buildCustomObjectAttributeSearchHandler(gateDeps, getDocsDir) {
|
|
88
|
-
return withSfccGate(gateDeps, async (args, credentials) => {
|
|
89
|
-
const { object_type, query, start, count, sorts } = customObjectAttributeSearchInput.parse(args);
|
|
96
|
+
return withSfccReadErrorBoundary(withSfccGate(gateDeps, async (args, credentials) => {
|
|
97
|
+
const { object_type, query, start, count, sorts, projection } = customObjectAttributeSearchInput.parse(args);
|
|
90
98
|
const encodedType = encodeURIComponent(object_type);
|
|
91
99
|
// Coerce plain string queries into OCAPI text_query shape
|
|
92
100
|
const resolvedQuery = typeof query === "string"
|
|
@@ -99,15 +107,16 @@ function buildCustomObjectAttributeSearchHandler(gateDeps, getDocsDir) {
|
|
|
99
107
|
postBody.count = count;
|
|
100
108
|
if (sorts !== undefined)
|
|
101
109
|
postBody.sorts = sorts;
|
|
110
|
+
applyProjectionToBody(postBody, "attribute_search", projection);
|
|
102
111
|
const result = await ocapiPost(`/custom_object_definitions/${encodedType}/attribute_definition_search`, postBody, credentials);
|
|
103
112
|
if (!result.ok) {
|
|
104
|
-
return
|
|
113
|
+
return formatOcapiReadFailure(result);
|
|
105
114
|
}
|
|
106
115
|
const normalized = normalizeOcapiBody(result.body);
|
|
107
116
|
const text = JSON.stringify(normalized, null, 2);
|
|
108
117
|
const dir = path.join(await getDocsDir(), "sfcc");
|
|
109
|
-
return saveAndReturn(text, dir, `custom-object-def-search-${safeType(object_type)}-${safeTimestamp()}.json
|
|
110
|
-
});
|
|
118
|
+
return saveAndReturn(text, dir, `custom-object-def-search-${safeType(object_type)}-${safeTimestamp()}.json`, normalized.page);
|
|
119
|
+
}));
|
|
111
120
|
}
|
|
112
121
|
/**
|
|
113
122
|
* Register the two SFCC custom-object-definition introspection read tools.
|
|
@@ -119,16 +128,18 @@ export function registerSfccCustomObjectDefReadTools(registerTool, deps) {
|
|
|
119
128
|
registerTool("custom_object_definition_attributes_get", {
|
|
120
129
|
description: "Retrieve attribute definitions for a KNOWN custom object type via " +
|
|
121
130
|
"GET /custom_object_definitions/{type}/attribute_definitions. " +
|
|
131
|
+
"Returns each attribute's full schema by default. " +
|
|
122
132
|
"OCAPI cannot enumerate types — object_type is required. " +
|
|
123
|
-
"Type creation is v2 metadata-import only
|
|
133
|
+
"Type creation is v2 metadata-import only. Oversized payloads auto-saved locally.",
|
|
124
134
|
inputSchema: customObjectAttributesGetInput,
|
|
125
135
|
annotations: READ_ANNOTATIONS,
|
|
126
136
|
}, buildCustomObjectAttributesGetHandler(gateDeps, getDocsDir));
|
|
127
137
|
registerTool("custom_object_definition_attribute_search", {
|
|
128
138
|
description: "Search attribute definitions within a KNOWN custom object type via " +
|
|
129
139
|
"POST /custom_object_definitions/{type}/attribute_definition_search. " +
|
|
130
|
-
"
|
|
131
|
-
"
|
|
140
|
+
"Returns each attribute's full schema by default. " +
|
|
141
|
+
"OCAPI cannot enumerate types; object_type is required. " +
|
|
142
|
+
"Pass a string or OCAPI query. Type creation is v2 metadata-import only. " +
|
|
132
143
|
"Oversized results auto-saved locally.",
|
|
133
144
|
inputSchema: customObjectAttributeSearchInput,
|
|
134
145
|
annotations: READ_ANNOTATIONS,
|
|
@@ -9,6 +9,27 @@
|
|
|
9
9
|
* All three 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
11
|
*
|
|
12
|
+
* PREFERENCE VALUES ARE NOT AVAILABLE HERE (BAPI-767).
|
|
13
|
+
* `preference_search` returns hits shaped `{_type, id}` and nothing more.
|
|
14
|
+
* Verified live against a developer sandbox (OCAPI v25_6): supplying
|
|
15
|
+
* `?select=(**)` changes the response not at all — zero hits gain a `value`
|
|
16
|
+
* key. That is why these two tools take no `projection` input while
|
|
17
|
+
* `site_preference_group_list` does: a projection that cannot alter the
|
|
18
|
+
* response is a flag with no observable effect.
|
|
19
|
+
*
|
|
20
|
+
* The consequence is a genuine caller-facing limitation, stated in both tool
|
|
21
|
+
* descriptions rather than left to be discovered: an ids-only read cannot
|
|
22
|
+
* distinguish "preference not set" from "preference set to the empty string".
|
|
23
|
+
*
|
|
24
|
+
* It is also, for now, accidentally protective. A name-heuristic scan of all 19
|
|
25
|
+
* preference groups on the tested sandbox found secret-named preferences in six
|
|
26
|
+
* of them — including one literally named `client_id` — and none of them expose
|
|
27
|
+
* a value through this endpoint. No redaction seam is built here because there
|
|
28
|
+
* is no value-bearing path to redact. If OCAPI ever starts returning values,
|
|
29
|
+
* redaction must be added on the raw body BEFORE normalization, serialization,
|
|
30
|
+
* truncation, or save, so one placement covers both the inline response and the
|
|
31
|
+
* docs/tmp/sfcc/ save path.
|
|
32
|
+
*
|
|
12
33
|
* NOTE: SCAPI Preferences API (sfcc.preferences) is intentionally NOT used.
|
|
13
34
|
* This keeps the tool context on the OCAPI Data API, preserving the single
|
|
14
35
|
* AM-OAuth boundary established in T1. Do not "modernize" these tools onto
|
|
@@ -18,7 +39,9 @@ import path from "path";
|
|
|
18
39
|
import { z } from "zod";
|
|
19
40
|
import { ocapiGet, ocapiPost } from "./client.js";
|
|
20
41
|
import { withSfccGate } from "./tool-wrapper.js";
|
|
42
|
+
import { formatOcapiReadFailure, formatSfccReadFailure, withSfccReadErrorBoundary, } from "./read-result.js";
|
|
21
43
|
import { normalizeOcapiBody } from "./ocapi-shape.js";
|
|
44
|
+
import { projectionInputFor, projectionQueryEntries } from "./read-projection.js";
|
|
22
45
|
import { truncateAndSaveIfNeeded } from "./output.js";
|
|
23
46
|
// ---------------------------------------------------------------------------
|
|
24
47
|
// Annotations (SPEC: read-only against developer sandboxes)
|
|
@@ -53,6 +76,7 @@ const sitePreferenceSearchInput = z.object({
|
|
|
53
76
|
const sitePreferenceGroupListInput = z.object({
|
|
54
77
|
count: z.number().optional().describe("Maximum number of site preference groups to return."),
|
|
55
78
|
start: z.number().optional().describe("Zero-based offset for paging."),
|
|
79
|
+
projection: projectionInputFor("broad_list"),
|
|
56
80
|
});
|
|
57
81
|
// ---------------------------------------------------------------------------
|
|
58
82
|
// Helpers
|
|
@@ -66,18 +90,37 @@ function safeGroup(group) {
|
|
|
66
90
|
function textResult(text) {
|
|
67
91
|
return { content: [{ type: "text", text }] };
|
|
68
92
|
}
|
|
69
|
-
async function saveAndReturn(text, dir, filename) {
|
|
70
|
-
const output = await truncateAndSaveIfNeeded(text, dir, filename);
|
|
93
|
+
async function saveAndReturn(text, dir, filename, page) {
|
|
94
|
+
const output = await truncateAndSaveIfNeeded(text, dir, filename, page);
|
|
71
95
|
return textResult(output);
|
|
72
96
|
}
|
|
73
|
-
|
|
97
|
+
/**
|
|
98
|
+
* D-5: reject any non-sandbox instance context (safer than silently proceeding).
|
|
99
|
+
*
|
|
100
|
+
* The rejection decision is unchanged; BAPI-759 moved its representation to the
|
|
101
|
+
* shared formatter, so the guardrail fails at the protocol level like every
|
|
102
|
+
* other SFCC failure. Details carry non-sensitive guard metadata only — the
|
|
103
|
+
* rejected value is caller-supplied and enum-constrained.
|
|
104
|
+
*
|
|
105
|
+
* BAPI-766 reworded the message only. It used to open with "v1", which reads as
|
|
106
|
+
* an API version while OCAPI simultaneously reports `_v: "25.6"` in the same
|
|
107
|
+
* session; the constraint belongs to this Bridge tool, not to a version. The
|
|
108
|
+
* status, code, source, decision, and `details` payload are untouched.
|
|
109
|
+
*/
|
|
74
110
|
function rejectIfNotSandbox(instance) {
|
|
75
111
|
if (instance !== "sandbox") {
|
|
76
|
-
return
|
|
77
|
-
|
|
112
|
+
return formatSfccReadFailure({
|
|
113
|
+
code: "VALIDATION_ERROR",
|
|
78
114
|
status: 400,
|
|
79
|
-
message: `
|
|
80
|
-
|
|
115
|
+
message: `The Bridge site-preference tool only supports the 'sandbox' instance ` +
|
|
116
|
+
`context. Received: '${instance}'.`,
|
|
117
|
+
source: "tool",
|
|
118
|
+
details: {
|
|
119
|
+
guard: "sandbox_only",
|
|
120
|
+
received_instance: instance,
|
|
121
|
+
supported_instances: ["sandbox"],
|
|
122
|
+
},
|
|
123
|
+
});
|
|
81
124
|
}
|
|
82
125
|
return null;
|
|
83
126
|
}
|
|
@@ -85,7 +128,7 @@ function rejectIfNotSandbox(instance) {
|
|
|
85
128
|
// Handlers
|
|
86
129
|
// ---------------------------------------------------------------------------
|
|
87
130
|
export function buildSitePreferenceGetHandler(gateDeps, getDocsDir) {
|
|
88
|
-
return withSfccGate(gateDeps, async (args, credentials) => {
|
|
131
|
+
return withSfccReadErrorBoundary(withSfccGate(gateDeps, async (args, credentials) => {
|
|
89
132
|
const { group, instance, start, count } = sitePreferenceGetInput.parse(args);
|
|
90
133
|
const guard = rejectIfNotSandbox(instance);
|
|
91
134
|
if (guard)
|
|
@@ -100,16 +143,16 @@ export function buildSitePreferenceGetHandler(gateDeps, getDocsDir) {
|
|
|
100
143
|
postBody.count = count;
|
|
101
144
|
const result = await ocapiPost(`/site_preferences/preference_groups/${encodedGroup}/${instance}/preference_search`, postBody, credentials);
|
|
102
145
|
if (!result.ok) {
|
|
103
|
-
return
|
|
146
|
+
return formatOcapiReadFailure(result);
|
|
104
147
|
}
|
|
105
148
|
const normalized = normalizeOcapiBody(result.body);
|
|
106
149
|
const text = JSON.stringify(normalized, null, 2);
|
|
107
150
|
const dir = path.join(await getDocsDir(), "sfcc");
|
|
108
|
-
return saveAndReturn(text, dir, `site-preference-get-${safeGroup(group)}-${safeTimestamp()}.json
|
|
109
|
-
});
|
|
151
|
+
return saveAndReturn(text, dir, `site-preference-get-${safeGroup(group)}-${safeTimestamp()}.json`, normalized.page);
|
|
152
|
+
}));
|
|
110
153
|
}
|
|
111
154
|
export function buildSitePreferenceSearchHandler(gateDeps, getDocsDir) {
|
|
112
|
-
return withSfccGate(gateDeps, async (args, credentials) => {
|
|
155
|
+
return withSfccReadErrorBoundary(withSfccGate(gateDeps, async (args, credentials) => {
|
|
113
156
|
const { group, instance, query, start, count, sorts } = sitePreferenceSearchInput.parse(args);
|
|
114
157
|
const guard = rejectIfNotSandbox(instance);
|
|
115
158
|
if (guard)
|
|
@@ -128,18 +171,20 @@ export function buildSitePreferenceSearchHandler(gateDeps, getDocsDir) {
|
|
|
128
171
|
postBody.sorts = sorts;
|
|
129
172
|
const result = await ocapiPost(`/site_preferences/preference_groups/${encodedGroup}/${instance}/preference_search`, postBody, credentials);
|
|
130
173
|
if (!result.ok) {
|
|
131
|
-
return
|
|
174
|
+
return formatOcapiReadFailure(result);
|
|
132
175
|
}
|
|
133
176
|
const normalized = normalizeOcapiBody(result.body);
|
|
134
177
|
const text = JSON.stringify(normalized, null, 2);
|
|
135
178
|
const dir = path.join(await getDocsDir(), "sfcc");
|
|
136
|
-
return saveAndReturn(text, dir, `site-preference-search-${safeGroup(group)}-${safeTimestamp()}.json
|
|
137
|
-
});
|
|
179
|
+
return saveAndReturn(text, dir, `site-preference-search-${safeGroup(group)}-${safeTimestamp()}.json`, normalized.page);
|
|
180
|
+
}));
|
|
138
181
|
}
|
|
139
182
|
export function buildSitePreferenceGroupListHandler(gateDeps, getDocsDir) {
|
|
140
|
-
return withSfccGate(gateDeps, async (args, credentials) => {
|
|
141
|
-
const { count, start } = sitePreferenceGroupListInput.parse(args);
|
|
142
|
-
const queryParams = {
|
|
183
|
+
return withSfccReadErrorBoundary(withSfccGate(gateDeps, async (args, credentials) => {
|
|
184
|
+
const { count, start, projection } = sitePreferenceGroupListInput.parse(args);
|
|
185
|
+
const queryParams = {
|
|
186
|
+
...projectionQueryEntries("broad_list", projection),
|
|
187
|
+
};
|
|
143
188
|
if (count !== undefined)
|
|
144
189
|
queryParams.count = String(count);
|
|
145
190
|
if (start !== undefined)
|
|
@@ -149,13 +194,13 @@ export function buildSitePreferenceGroupListHandler(gateDeps, getDocsDir) {
|
|
|
149
194
|
: "";
|
|
150
195
|
const result = await ocapiGet(`/system_object_definitions/SitePreferences/attribute_groups${queryStr}`, credentials);
|
|
151
196
|
if (!result.ok) {
|
|
152
|
-
return
|
|
197
|
+
return formatOcapiReadFailure(result);
|
|
153
198
|
}
|
|
154
199
|
const normalized = normalizeOcapiBody(result.body);
|
|
155
200
|
const text = JSON.stringify(normalized, null, 2);
|
|
156
201
|
const dir = path.join(await getDocsDir(), "sfcc");
|
|
157
|
-
return saveAndReturn(text, dir, `site-preference-group-list-${safeTimestamp()}.json
|
|
158
|
-
});
|
|
202
|
+
return saveAndReturn(text, dir, `site-preference-group-list-${safeTimestamp()}.json`, normalized.page);
|
|
203
|
+
}));
|
|
159
204
|
}
|
|
160
205
|
/**
|
|
161
206
|
* Register the three SFCC site-preference introspection read tools.
|
|
@@ -173,18 +218,19 @@ export function registerSitePreferenceTools(registerTool, deps) {
|
|
|
173
218
|
annotations: READ_ANNOTATIONS,
|
|
174
219
|
}, buildSitePreferenceGroupListHandler(gateDeps, getDocsDir));
|
|
175
220
|
registerTool("site_preference_get", {
|
|
176
|
-
description: "
|
|
177
|
-
"
|
|
178
|
-
"
|
|
179
|
-
"Oversized payloads
|
|
221
|
+
description: "List preference IDENTIFIERS for a site preference group from the developer sandbox via a match-all " +
|
|
222
|
+
"POST /site_preferences/preference_groups/{group}/sandbox/preference_search. " +
|
|
223
|
+
"Returns ids only, NOT values — 'unset' and 'empty string' are indistinguishable. " +
|
|
224
|
+
"Read-only, sandbox-only. Optional start/count paging. Oversized payloads auto-saved locally.",
|
|
180
225
|
inputSchema: sitePreferenceGetInput,
|
|
181
226
|
annotations: READ_ANNOTATIONS,
|
|
182
227
|
}, buildSitePreferenceGetHandler(gateDeps, getDocsDir));
|
|
183
228
|
registerTool("site_preference_search", {
|
|
184
|
-
description: "Search
|
|
185
|
-
"
|
|
186
|
-
"
|
|
187
|
-
"
|
|
229
|
+
description: "Search preference IDENTIFIERS within a site preference group via " +
|
|
230
|
+
"POST /site_preferences/preference_groups/{group}/sandbox/preference_search. " +
|
|
231
|
+
"Returns ids only — this endpoint does not expose values. Read-only; v1 sandbox only. " +
|
|
232
|
+
"Pass a plain string for id-only text search or a structured OCAPI query. " +
|
|
233
|
+
"Oversized results auto-saved locally.",
|
|
188
234
|
inputSchema: sitePreferenceSearchInput,
|
|
189
235
|
annotations: READ_ANNOTATIONS,
|
|
190
236
|
}, buildSitePreferenceSearchHandler(gateDeps, getDocsDir));
|
|
@@ -13,7 +13,9 @@ import path from "path";
|
|
|
13
13
|
import { z } from "zod";
|
|
14
14
|
import { ocapiGet, ocapiPost } from "./client.js";
|
|
15
15
|
import { withSfccGate } from "./tool-wrapper.js";
|
|
16
|
+
import { formatOcapiReadFailure, withSfccReadErrorBoundary } from "./read-result.js";
|
|
16
17
|
import { normalizeOcapiBody } from "./ocapi-shape.js";
|
|
18
|
+
import { applyProjectionToBody, projectionInputFor, projectionQueryEntries, } from "./read-projection.js";
|
|
17
19
|
import { truncateAndSaveIfNeeded } from "./output.js";
|
|
18
20
|
// ---------------------------------------------------------------------------
|
|
19
21
|
// Annotations (SPEC: read-only against developer sandboxes)
|
|
@@ -30,19 +32,23 @@ const READ_ANNOTATIONS = {
|
|
|
30
32
|
const systemObjectListInput = z.object({
|
|
31
33
|
count: z.number().optional().describe("Maximum number of system object types to return."),
|
|
32
34
|
start: z.number().optional().describe("Zero-based offset for paging."),
|
|
35
|
+
projection: projectionInputFor("broad_list"),
|
|
33
36
|
});
|
|
37
|
+
// No `projection` field: GET /system_object_definitions/{type} already returns
|
|
38
|
+
// its complete document and ignores `select` (verified live — 13 keys with and
|
|
39
|
+
// without it). A flag here would have no observable effect, which is the defect
|
|
40
|
+
// BAPI-760 removed from this same tool. See read-projection.ts.
|
|
34
41
|
const systemObjectGetInput = z.object({
|
|
35
42
|
object_type: z.string().describe("System object type identifier, e.g. \"Product\" or \"Order\"."),
|
|
36
|
-
expand_attribute_definitions: z.boolean().optional().describe("When true, include the full attribute definition list in the response. " +
|
|
37
|
-
"May produce a large payload — auto-saved locally when oversized."),
|
|
38
43
|
});
|
|
39
44
|
const systemObjectAttributeSearchInput = z.object({
|
|
40
45
|
object_type: z.string().describe("System object type to search within, e.g. \"Order\"."),
|
|
41
|
-
query: z.union([z.string(), z.record(z.string(), z.any())]).describe("Search query.
|
|
42
|
-
"
|
|
46
|
+
query: z.union([z.string(), z.record(z.string(), z.any())]).describe("Search query. A plain string is a case-insensitive substring match across " +
|
|
47
|
+
"id and display_name; a structured OCAPI query object passes through unchanged."),
|
|
43
48
|
start: z.number().optional().describe("Zero-based offset for paging."),
|
|
44
49
|
count: z.number().optional().describe("Maximum number of results to return."),
|
|
45
50
|
sorts: z.array(z.any()).optional().describe("Array of OCAPI sort descriptors."),
|
|
51
|
+
projection: projectionInputFor("attribute_search"),
|
|
46
52
|
});
|
|
47
53
|
// ---------------------------------------------------------------------------
|
|
48
54
|
// Helpers
|
|
@@ -56,17 +62,19 @@ function safeType(objectType) {
|
|
|
56
62
|
function textResult(text) {
|
|
57
63
|
return { content: [{ type: "text", text }] };
|
|
58
64
|
}
|
|
59
|
-
async function saveAndReturn(text, dir, filename) {
|
|
60
|
-
const output = await truncateAndSaveIfNeeded(text, dir, filename);
|
|
65
|
+
async function saveAndReturn(text, dir, filename, page) {
|
|
66
|
+
const output = await truncateAndSaveIfNeeded(text, dir, filename, page);
|
|
61
67
|
return textResult(output);
|
|
62
68
|
}
|
|
63
69
|
// ---------------------------------------------------------------------------
|
|
64
70
|
// Handlers
|
|
65
71
|
// ---------------------------------------------------------------------------
|
|
66
72
|
function buildSystemObjectListHandler(gateDeps, getDocsDir) {
|
|
67
|
-
return withSfccGate(gateDeps, async (args, credentials) => {
|
|
68
|
-
const { count, start } = systemObjectListInput.parse(args);
|
|
69
|
-
const queryParams = {
|
|
73
|
+
return withSfccReadErrorBoundary(withSfccGate(gateDeps, async (args, credentials) => {
|
|
74
|
+
const { count, start, projection } = systemObjectListInput.parse(args);
|
|
75
|
+
const queryParams = {
|
|
76
|
+
...projectionQueryEntries("broad_list", projection),
|
|
77
|
+
};
|
|
70
78
|
if (count !== undefined)
|
|
71
79
|
queryParams.count = String(count);
|
|
72
80
|
if (start !== undefined)
|
|
@@ -76,34 +84,31 @@ function buildSystemObjectListHandler(gateDeps, getDocsDir) {
|
|
|
76
84
|
: "";
|
|
77
85
|
const result = await ocapiGet(`/system_object_definitions${queryStr}`, credentials);
|
|
78
86
|
if (!result.ok) {
|
|
79
|
-
return
|
|
87
|
+
return formatOcapiReadFailure(result);
|
|
80
88
|
}
|
|
81
89
|
const normalized = normalizeOcapiBody(result.body);
|
|
82
90
|
const text = JSON.stringify(normalized, null, 2);
|
|
83
91
|
const dir = path.join(await getDocsDir(), "sfcc");
|
|
84
|
-
return saveAndReturn(text, dir, `system-object-list-${safeTimestamp()}.json
|
|
85
|
-
});
|
|
92
|
+
return saveAndReturn(text, dir, `system-object-list-${safeTimestamp()}.json`, normalized.page);
|
|
93
|
+
}));
|
|
86
94
|
}
|
|
87
95
|
function buildSystemObjectGetHandler(gateDeps, getDocsDir) {
|
|
88
|
-
return withSfccGate(gateDeps, async (args, credentials) => {
|
|
89
|
-
const { object_type
|
|
96
|
+
return withSfccReadErrorBoundary(withSfccGate(gateDeps, async (args, credentials) => {
|
|
97
|
+
const { object_type } = systemObjectGetInput.parse(args);
|
|
90
98
|
const encodedType = encodeURIComponent(object_type);
|
|
91
|
-
const
|
|
92
|
-
? "?expand=attribute_definitions"
|
|
93
|
-
: "";
|
|
94
|
-
const result = await ocapiGet(`/system_object_definitions/${encodedType}${expandParam}`, credentials);
|
|
99
|
+
const result = await ocapiGet(`/system_object_definitions/${encodedType}`, credentials);
|
|
95
100
|
if (!result.ok) {
|
|
96
|
-
return
|
|
101
|
+
return formatOcapiReadFailure(result);
|
|
97
102
|
}
|
|
98
103
|
const normalized = normalizeOcapiBody(result.body);
|
|
99
104
|
const text = JSON.stringify(normalized, null, 2);
|
|
100
105
|
const dir = path.join(await getDocsDir(), "sfcc");
|
|
101
|
-
return saveAndReturn(text, dir, `system-object-get-${safeType(object_type)}-${safeTimestamp()}.json
|
|
102
|
-
});
|
|
106
|
+
return saveAndReturn(text, dir, `system-object-get-${safeType(object_type)}-${safeTimestamp()}.json`, normalized.page);
|
|
107
|
+
}));
|
|
103
108
|
}
|
|
104
109
|
function buildSystemObjectAttributeSearchHandler(gateDeps, getDocsDir) {
|
|
105
|
-
return withSfccGate(gateDeps, async (args, credentials) => {
|
|
106
|
-
const { object_type, query, start, count, sorts } = systemObjectAttributeSearchInput.parse(args);
|
|
110
|
+
return withSfccReadErrorBoundary(withSfccGate(gateDeps, async (args, credentials) => {
|
|
111
|
+
const { object_type, query, start, count, sorts, projection } = systemObjectAttributeSearchInput.parse(args);
|
|
107
112
|
const encodedType = encodeURIComponent(object_type);
|
|
108
113
|
// Coerce plain string queries into OCAPI text_query shape
|
|
109
114
|
const resolvedQuery = typeof query === "string"
|
|
@@ -116,15 +121,16 @@ function buildSystemObjectAttributeSearchHandler(gateDeps, getDocsDir) {
|
|
|
116
121
|
postBody.count = count;
|
|
117
122
|
if (sorts !== undefined)
|
|
118
123
|
postBody.sorts = sorts;
|
|
124
|
+
applyProjectionToBody(postBody, "attribute_search", projection);
|
|
119
125
|
const result = await ocapiPost(`/system_object_definitions/${encodedType}/attribute_definition_search`, postBody, credentials);
|
|
120
126
|
if (!result.ok) {
|
|
121
|
-
return
|
|
127
|
+
return formatOcapiReadFailure(result);
|
|
122
128
|
}
|
|
123
129
|
const normalized = normalizeOcapiBody(result.body);
|
|
124
130
|
const text = JSON.stringify(normalized, null, 2);
|
|
125
131
|
const dir = path.join(await getDocsDir(), "sfcc");
|
|
126
|
-
return saveAndReturn(text, dir, `system-object-search-${safeType(object_type)}-${safeTimestamp()}.json
|
|
127
|
-
});
|
|
132
|
+
return saveAndReturn(text, dir, `system-object-search-${safeType(object_type)}-${safeTimestamp()}.json`, normalized.page);
|
|
133
|
+
}));
|
|
128
134
|
}
|
|
129
135
|
/**
|
|
130
136
|
* Register the three SFCC system-object introspection read tools.
|
|
@@ -137,25 +143,25 @@ export function registerSystemObjectReadTools(registerTool, deps) {
|
|
|
137
143
|
description: "List all system object types from the developer sandbox via " +
|
|
138
144
|
"GET /system_object_definitions. Read-only. " +
|
|
139
145
|
"Accepts optional `count` and `start` for paging (OCAPI default pagination applies when omitted). " +
|
|
140
|
-
"
|
|
146
|
+
"Returns ids only by default; pass projection=full for all fields. " +
|
|
147
|
+
"Oversized outputs are auto-saved locally.",
|
|
141
148
|
inputSchema: systemObjectListInput,
|
|
142
149
|
annotations: READ_ANNOTATIONS,
|
|
143
150
|
}, buildSystemObjectListHandler(gateDeps, getDocsDir));
|
|
144
151
|
registerTool("system_object_get", {
|
|
145
|
-
description: "Retrieve a system object type from the developer sandbox. Read-only. " +
|
|
152
|
+
description: "Retrieve a system object type definition from the developer sandbox. Read-only. " +
|
|
146
153
|
"GET /system_object_definitions/{type}. " +
|
|
147
|
-
"Prefer system_object_attribute_search for targeted attribute lookups
|
|
148
|
-
"
|
|
149
|
-
"(expand_attribute_definitions=true). Oversized payloads are auto-saved locally.",
|
|
154
|
+
"Prefer system_object_attribute_search for targeted attribute lookups. " +
|
|
155
|
+
"Oversized payloads are auto-saved locally.",
|
|
150
156
|
inputSchema: systemObjectGetInput,
|
|
151
157
|
annotations: READ_ANNOTATIONS,
|
|
152
158
|
}, buildSystemObjectGetHandler(gateDeps, getDocsDir));
|
|
153
159
|
registerTool("system_object_attribute_search", {
|
|
154
160
|
description: "Search attribute definitions for a system object type. Read-only. " +
|
|
155
161
|
"POST /system_object_definitions/{type}/attribute_definition_search. " +
|
|
156
|
-
"
|
|
157
|
-
"Pass a plain string for text search or a structured OCAPI query
|
|
158
|
-
"Oversized results
|
|
162
|
+
"Use for targeted c_ attribute lookups; returns each attribute's full schema by default. " +
|
|
163
|
+
"Pass a plain string for text search or a structured OCAPI query. " +
|
|
164
|
+
"Oversized results auto-saved locally.",
|
|
159
165
|
inputSchema: systemObjectAttributeSearchInput,
|
|
160
166
|
annotations: READ_ANNOTATIONS,
|
|
161
167
|
}, buildSystemObjectAttributeSearchHandler(gateDeps, getDocsDir));
|