@bridge_gpt/mcp-server 0.2.10 → 0.2.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +66 -4
  2. package/build/commands.generated.js +6 -6
  3. package/build/conductor/bridge-api-client.js +2 -1
  4. package/build/conductor/cli.js +16 -16
  5. package/build/conductor/doctor.js +79 -2
  6. package/build/conductor/epic-reconcile.js +213 -16
  7. package/build/conductor/epic-runtime.js +215 -60
  8. package/build/conductor/epic-state.js +105 -16
  9. package/build/conductor/errors.js +12 -0
  10. package/build/conductor/git-ci-types.js +10 -0
  11. package/build/conductor/git-producer.js +4 -4
  12. package/build/conductor/local-merge.js +212 -0
  13. package/build/conductor/merge-ledger.js +7 -7
  14. package/build/conductor/pr-ci-producer.js +18 -8
  15. package/build/conductor/pr-review-producer.js +2 -2
  16. package/build/conductor/producer-ledger.js +5 -5
  17. package/build/conductor/spec-review-producer.js +88 -0
  18. package/build/conductor/store.js +100 -25
  19. package/build/conductor/supervisor-ledger.js +2 -2
  20. package/build/conductor/supervisor-merge.js +5 -5
  21. package/build/conductor/supervisor-message-relay.js +1 -1
  22. package/build/conductor/supervisor-runtime.js +10 -10
  23. package/build/conductor/taxonomy.js +9 -0
  24. package/build/conductor/tools.js +5 -5
  25. package/build/conductor-bin.js +12689 -19
  26. package/build/conductor-claude-hook-bin.js +167 -17
  27. package/build/decision-page-schema.js +26 -0
  28. package/build/doctor.js +203 -0
  29. package/build/index.js +25084 -3632
  30. package/build/init.js +57 -0
  31. package/build/install-bridge.js +80 -0
  32. package/build/mcp-profile.js +33 -30
  33. package/build/pipelines.generated.js +70 -48
  34. package/build/readme.generated.js +1 -1
  35. package/build/sfcc/client.js +151 -0
  36. package/build/sfcc/config.js +39 -0
  37. package/build/sfcc/credentials.js +136 -0
  38. package/build/sfcc/ocapi-shape.js +77 -0
  39. package/build/sfcc/output.js +39 -0
  40. package/build/sfcc/permissions.js +136 -0
  41. package/build/sfcc/reads-custom-object-def.js +119 -0
  42. package/build/sfcc/reads-site-preference.js +158 -0
  43. package/build/sfcc/reads-system-object.js +162 -0
  44. package/build/sfcc/register.js +73 -0
  45. package/build/sfcc/setup-status.js +114 -0
  46. package/build/sfcc/tool-wrapper.js +70 -0
  47. package/build/start-tickets-conductor.js +9 -1
  48. package/build/start-tickets.js +47 -4
  49. package/build/version.generated.js +1 -1
  50. package/package.json +7 -4
  51. package/pipelines/check-ci-ticket.json +2 -2
  52. package/pipelines/implement-ticket.json +2 -2
  53. package/pipelines/learn-repository.json +84 -42
  54. package/smoke-test/SMOKE-TEST.md +11 -17
@@ -0,0 +1,77 @@
1
+ /**
2
+ * OCAPI Data API paging-envelope normalizer.
3
+ *
4
+ * The SFCC OCAPI Data API wraps collection responses in a standard paging
5
+ * envelope. This module provides a typed extractor for the `data` array so
6
+ * sub-task read tools share one normalizer rather than each re-deriving the
7
+ * shape.
8
+ */
9
+ /**
10
+ * Parse a raw OCAPI response body into a normalized envelope result.
11
+ *
12
+ * Returns `null` when `body` is not a valid OCAPI paging envelope (missing
13
+ * `data` array or `count` field), allowing callers to surface a structured
14
+ * error instead of crashing.
15
+ */
16
+ export function normalizeOcapiPage(body) {
17
+ if (body === null ||
18
+ typeof body !== "object" ||
19
+ !Array.isArray(body.data) ||
20
+ typeof body.count !== "number") {
21
+ return null;
22
+ }
23
+ const envelope = body;
24
+ return {
25
+ items: envelope.data,
26
+ count: envelope.count,
27
+ total: envelope.total,
28
+ hasMore: envelope.next != null,
29
+ };
30
+ }
31
+ /**
32
+ * Permissive OCAPI response normalizer used by sub-task read tools.
33
+ *
34
+ * Handles all envelope forms observed in the wild:
35
+ * - Standard wrapped envelope: { count, data, total?, start?, next? }
36
+ * - Version-tagged envelope: { _v, data, ... } (count may be absent)
37
+ * - Bare array response: [item, item, ...]
38
+ * - Bare object response: { id: "..." } (no data wrapper)
39
+ * - Any other value: returns empty items rather than throwing
40
+ *
41
+ * Unlike `normalizeOcapiPage`, this function never returns null and is
42
+ * forward-compatible with future OCAPI envelope shapes.
43
+ */
44
+ export function normalizeOcapiBody(body) {
45
+ // Bare array
46
+ if (Array.isArray(body)) {
47
+ return { items: body, count: body.length, total: undefined, hasMore: false };
48
+ }
49
+ // Object forms
50
+ if (body !== null && typeof body === "object") {
51
+ const obj = body;
52
+ // Wrapped envelope: has a data array (with or without explicit count)
53
+ if (Array.isArray(obj.data)) {
54
+ const items = obj.data;
55
+ return {
56
+ items,
57
+ count: typeof obj.count === "number" ? obj.count : items.length,
58
+ total: typeof obj.total === "number" ? obj.total : undefined,
59
+ hasMore: obj.next != null,
60
+ };
61
+ }
62
+ // Bare object (single resource, no wrapping envelope)
63
+ return { items: [body], count: 1, total: undefined, hasMore: false };
64
+ }
65
+ // Non-object (null, string, number, etc.)
66
+ return { items: [], count: 0, total: undefined, hasMore: false };
67
+ }
68
+ /**
69
+ * SPEC §4.5 `value_type` mapper.
70
+ *
71
+ * Surfaces the raw REST-JSON underscored enum string unchanged (e.g.,
72
+ * "set_of_string", "enum_of_int"). Unknown / future values are passed through
73
+ * without dropping them — forward-compatible by design.
74
+ */
75
+ export function mapValueType(value) {
76
+ return value;
77
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * SFCC output helpers — thin wrappers around the host index.ts seams.
3
+ *
4
+ * Re-exports the truncation + docs-path utilities through a stable sfcc/
5
+ * interface so sub-task read tools import from here instead of reaching into
6
+ * the host module directly. This boundary makes a future extraction cheap.
7
+ */
8
+ import path from "path";
9
+ import { mkdir, writeFile } from "fs/promises";
10
+ /** Maximum characters returned inline; content beyond this is saved locally. */
11
+ export const SFCC_MAX_INLINE = 50_000;
12
+ /** Truncation note appended to truncated payloads. */
13
+ function truncationNote(savedPath) {
14
+ return `\n\n[Response truncated — full payload saved to ${savedPath}]`;
15
+ }
16
+ /**
17
+ * If `text` exceeds `SFCC_MAX_INLINE`, save it to `dir/filename` and return a
18
+ * truncated version with a local-path note. Otherwise returns `text` unchanged.
19
+ *
20
+ * Write failures are non-fatal: the full (un-truncated) text is returned with
21
+ * a warning rather than throwing.
22
+ */
23
+ export async function truncateAndSaveIfNeeded(text, dir, filename, deps = {}) {
24
+ if (text.length <= SFCC_MAX_INLINE) {
25
+ return text;
26
+ }
27
+ const mk = deps.mkdir ?? mkdir;
28
+ const wf = deps.writeFile ?? writeFile;
29
+ const filePath = path.join(dir, filename);
30
+ try {
31
+ await mk(dir, { recursive: true });
32
+ await wf(filePath, text, "utf-8");
33
+ }
34
+ catch (err) {
35
+ return (text +
36
+ `\n\nWarning: response was NOT truncated because local save failed: ${err instanceof Error ? err.message : String(err)}`);
37
+ }
38
+ return text.slice(0, SFCC_MAX_INLINE) + truncationNote(filePath);
39
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * check_permissions — SFCC OCAPI access probe.
3
+ *
4
+ * Issues a harmless GET /system_object_definitions probe. On 200: reports the
5
+ * detected OCAPI version. On 401/403: prints the exact OCAPI Settings JSON to
6
+ * paste in Business Manager, split into read-only (v1) vs. write/import (v2)
7
+ * grant blocks.
8
+ */
9
+ import { ocapiGet } from "./client.js";
10
+ import { normalizeOcapiPage } from "./ocapi-shape.js";
11
+ import { DEFAULT_OCAPI_VERSION } from "./config.js";
12
+ // ---------------------------------------------------------------------------
13
+ // OCAPI Settings JSON template
14
+ // Operators paste this in: Administration > Site Development > Open Commerce API Settings → Data API tab.
15
+ // ---------------------------------------------------------------------------
16
+ const OCAPI_SETTINGS_READ_ONLY = (ocapiVersion) => JSON.stringify({
17
+ _v: ocapiVersion,
18
+ clients: [
19
+ {
20
+ client_id: "<YOUR_CLIENT_ID>",
21
+ resources: [
22
+ {
23
+ resource_id: "/system_object_definitions",
24
+ methods: ["get"],
25
+ read_attributes: "(**)",
26
+ write_attributes: "(**)",
27
+ },
28
+ {
29
+ resource_id: "/system_object_definitions/**",
30
+ methods: ["get"],
31
+ read_attributes: "(**)",
32
+ write_attributes: "(**)",
33
+ },
34
+ ],
35
+ },
36
+ ],
37
+ }, null, 2);
38
+ const OCAPI_SETTINGS_WRITE_IMPORT = (ocapiVersion) => JSON.stringify({
39
+ _v: ocapiVersion,
40
+ clients: [
41
+ {
42
+ client_id: "<YOUR_CLIENT_ID>",
43
+ resources: [
44
+ {
45
+ resource_id: "/system_object_definitions",
46
+ methods: ["get", "put", "patch", "delete"],
47
+ read_attributes: "(**)",
48
+ write_attributes: "(**)",
49
+ },
50
+ {
51
+ resource_id: "/system_object_definitions/**",
52
+ methods: ["get", "put", "patch", "delete"],
53
+ read_attributes: "(**)",
54
+ write_attributes: "(**)",
55
+ },
56
+ ],
57
+ },
58
+ ],
59
+ }, null, 2);
60
+ // ---------------------------------------------------------------------------
61
+ // Tool implementation
62
+ // ---------------------------------------------------------------------------
63
+ /**
64
+ * Probe OCAPI access via GET /system_object_definitions.
65
+ *
66
+ * @param credentials - Resolved SFCC credentials (hostname + clientId/Secret).
67
+ * @param ocapiVersion - OCAPI version to probe (defaults to DEFAULT_OCAPI_VERSION).
68
+ */
69
+ export async function checkPermissionsTool(credentials, ocapiVersion = DEFAULT_OCAPI_VERSION) {
70
+ let result;
71
+ try {
72
+ result = await ocapiGet("/system_object_definitions", credentials, ocapiVersion);
73
+ }
74
+ catch (err) {
75
+ const msg = err instanceof Error ? err.message : String(err);
76
+ return {
77
+ content: [
78
+ {
79
+ type: "text",
80
+ text: `OCAPI probe failed: ${msg}\n\n` +
81
+ ocapiSettingsInstructions(ocapiVersion),
82
+ },
83
+ ],
84
+ };
85
+ }
86
+ if (result.ok) {
87
+ const normalized = normalizeOcapiPage(result.body);
88
+ const itemCount = normalized ? normalized.count : "unknown";
89
+ return {
90
+ content: [
91
+ {
92
+ type: "text",
93
+ text: `✓ OCAPI access confirmed.\n` +
94
+ ` Instance: ${credentials.hostname.split(".")[0]}\n` +
95
+ ` OCAPI version: ${ocapiVersion}\n` +
96
+ ` GET /system_object_definitions: ${itemCount} items returned`,
97
+ },
98
+ ],
99
+ };
100
+ }
101
+ if (result.status === 401 || result.status === 403) {
102
+ return {
103
+ content: [
104
+ {
105
+ type: "text",
106
+ text: `HTTP ${result.status}: OCAPI access denied for instance ${credentials.hostname.split(".")[0]}.\n\n` +
107
+ ocapiSettingsInstructions(ocapiVersion),
108
+ },
109
+ ],
110
+ };
111
+ }
112
+ return {
113
+ content: [
114
+ {
115
+ type: "text",
116
+ text: `Unexpected OCAPI response: HTTP ${result.status}.\n` +
117
+ `Ensure your sandbox is running and the hostname in dw.json is correct.`,
118
+ },
119
+ ],
120
+ };
121
+ }
122
+ function ocapiSettingsInstructions(ocapiVersion) {
123
+ return (`To grant OCAPI access, paste the JSON below in Business Manager:\n` +
124
+ ` Administration > Site Development > Open Commerce API Settings → Data API tab\n\n` +
125
+ `--- READ-ONLY GRANTS (v1 — required now) ---\n` +
126
+ `${OCAPI_SETTINGS_READ_ONLY(ocapiVersion)}\n\n` +
127
+ `--- WRITE/IMPORT GRANTS (v2 — forward-looking, paste once) ---\n` +
128
+ `${OCAPI_SETTINGS_WRITE_IMPORT(ocapiVersion)}\n\n` +
129
+ `Replace <YOUR_CLIENT_ID> with the client_id from your dw.json.`);
130
+ }
131
+ /**
132
+ * Build the check_permissions handler bound to the gate-resolved credentials.
133
+ */
134
+ export function buildCheckPermissionsHandler(withGate) {
135
+ return withGate(async (_args, credentials) => checkPermissionsTool(credentials));
136
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * SFCC custom-object-definition introspection read tools (BAPI-402, T4–T5).
3
+ *
4
+ * Implements:
5
+ * custom_object_definition_list — GET /custom_object_definitions
6
+ * custom_object_definition_get — GET /custom_object_definitions/{type}
7
+ *
8
+ * Both tools are read-only, run behind the T1 call-time gate, and route
9
+ * oversized payloads through the sfcc/output.ts truncate-and-save seam.
10
+ *
11
+ * Note: custom object TYPE creation/update is NOT available via OCAPI REST.
12
+ * It is a v2 metadata-import capability (metadata XML → WebDAV → import job).
13
+ */
14
+ import path from "path";
15
+ import { z } from "zod";
16
+ import { ocapiGet } from "./client.js";
17
+ import { withSfccGate } from "./tool-wrapper.js";
18
+ import { normalizeOcapiBody } from "./ocapi-shape.js";
19
+ import { truncateAndSaveIfNeeded } from "./output.js";
20
+ // ---------------------------------------------------------------------------
21
+ // Annotations (SPEC: read-only against developer sandboxes)
22
+ // ---------------------------------------------------------------------------
23
+ const READ_ANNOTATIONS = {
24
+ readOnlyHint: true,
25
+ destructiveHint: false,
26
+ idempotentHint: true,
27
+ openWorldHint: true,
28
+ };
29
+ // ---------------------------------------------------------------------------
30
+ // Input schemas
31
+ // ---------------------------------------------------------------------------
32
+ const customObjectListInput = z.object({
33
+ count: z.number().optional().describe("Maximum number of custom object types to return."),
34
+ start: z.number().optional().describe("Zero-based offset for paging."),
35
+ });
36
+ const customObjectGetInput = z.object({
37
+ object_type: z
38
+ .string()
39
+ .describe('Custom object type identifier, e.g. "GiftCertificate" or a custom type id starting with "c_".'),
40
+ });
41
+ // ---------------------------------------------------------------------------
42
+ // Helpers
43
+ // ---------------------------------------------------------------------------
44
+ function safeTimestamp() {
45
+ return new Date().toISOString().replace(/[:.]/g, "-");
46
+ }
47
+ function safeType(objectType) {
48
+ return encodeURIComponent(objectType).replace(/%/g, "_");
49
+ }
50
+ function textResult(text) {
51
+ return { content: [{ type: "text", text }] };
52
+ }
53
+ async function saveAndReturn(text, dir, filename) {
54
+ const output = await truncateAndSaveIfNeeded(text, dir, filename);
55
+ return textResult(output);
56
+ }
57
+ // ---------------------------------------------------------------------------
58
+ // Handlers
59
+ // ---------------------------------------------------------------------------
60
+ function buildCustomObjectListHandler(gateDeps, getDocsDir) {
61
+ return withSfccGate(gateDeps, async (args, credentials) => {
62
+ const { count, start } = customObjectListInput.parse(args);
63
+ const queryParams = {};
64
+ if (count !== undefined)
65
+ queryParams.count = String(count);
66
+ if (start !== undefined)
67
+ queryParams.start = String(start);
68
+ const queryStr = Object.keys(queryParams).length > 0
69
+ ? "?" + new URLSearchParams(queryParams).toString()
70
+ : "";
71
+ const result = await ocapiGet(`/custom_object_definitions${queryStr}`, credentials);
72
+ if (!result.ok) {
73
+ return textResult(JSON.stringify({ error: "OCAPI error", status: result.status, body: result.body }, null, 2));
74
+ }
75
+ const normalized = normalizeOcapiBody(result.body);
76
+ const text = JSON.stringify(normalized, null, 2);
77
+ const dir = path.join(await getDocsDir(), "sfcc");
78
+ return saveAndReturn(text, dir, `custom-object-def-list-${safeTimestamp()}.json`);
79
+ });
80
+ }
81
+ function buildCustomObjectGetHandler(gateDeps, getDocsDir) {
82
+ return withSfccGate(gateDeps, async (args, credentials) => {
83
+ const { object_type } = customObjectGetInput.parse(args);
84
+ const encodedType = encodeURIComponent(object_type);
85
+ const result = await ocapiGet(`/custom_object_definitions/${encodedType}`, credentials);
86
+ if (!result.ok) {
87
+ return textResult(JSON.stringify({ error: "OCAPI error", status: result.status, body: result.body }, null, 2));
88
+ }
89
+ const normalized = normalizeOcapiBody(result.body);
90
+ const text = JSON.stringify(normalized, null, 2);
91
+ const dir = path.join(await getDocsDir(), "sfcc");
92
+ return saveAndReturn(text, dir, `custom-object-def-${safeType(object_type)}-${safeTimestamp()}.json`);
93
+ });
94
+ }
95
+ /**
96
+ * Register the two SFCC custom-object-definition introspection read tools.
97
+ *
98
+ * Called from `registerSfccTools` — no direct `index.ts` edits needed.
99
+ */
100
+ export function registerSfccCustomObjectDefReadTools(registerTool, deps) {
101
+ const { gateDeps, getDocsDir } = deps;
102
+ registerTool("custom_object_definition_list", {
103
+ description: "List all custom object type definitions from the developer sandbox via " +
104
+ "GET /custom_object_definitions. Read-only introspection; type definitions cannot be created " +
105
+ "or updated via OCAPI (v2 metadata-import only). " +
106
+ "Optional `count` and `start` for paging. Oversized outputs auto-saved locally.",
107
+ inputSchema: customObjectListInput,
108
+ annotations: READ_ANNOTATIONS,
109
+ }, buildCustomObjectListHandler(gateDeps, getDocsDir));
110
+ registerTool("custom_object_definition_get", {
111
+ description: "Retrieve an EXISTING custom object type from the developer sandbox via " +
112
+ "GET /custom_object_definitions/{type}. " +
113
+ "Returns the type with key_definition and attribute_definitions/attribute_groups. " +
114
+ "Read-only introspection; type definitions cannot be created or updated via OCAPI " +
115
+ "(v2 metadata-import only). Oversized payloads auto-saved locally.",
116
+ inputSchema: customObjectGetInput,
117
+ annotations: READ_ANNOTATIONS,
118
+ }, buildCustomObjectGetHandler(gateDeps, getDocsDir));
119
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * SFCC site-preference introspection read tools (BAPI-403, T6 — optional).
3
+ *
4
+ * Implements:
5
+ * site_preference_get — GET /site_preferences/preference_groups/{group}/{instance}
6
+ * site_preference_search — POST /site_preferences/preference_groups/{group}/{instance}/preference_search
7
+ *
8
+ * Both tools are read-only, run behind the T1 call-time gate, and route
9
+ * oversized payloads through the sfcc/output.ts truncate-and-save seam.
10
+ *
11
+ * NOTE: SCAPI Preferences API (sfcc.preferences) is intentionally NOT used.
12
+ * This keeps the tool context on the OCAPI Data API, preserving the single
13
+ * AM-OAuth boundary established in T1. Do not "modernize" these tools onto
14
+ * SCAPI without understanding the auth-surface implications.
15
+ */
16
+ import path from "path";
17
+ import { z } from "zod";
18
+ import { ocapiGet, ocapiPost } from "./client.js";
19
+ import { withSfccGate } from "./tool-wrapper.js";
20
+ import { normalizeOcapiBody } from "./ocapi-shape.js";
21
+ import { truncateAndSaveIfNeeded } from "./output.js";
22
+ // ---------------------------------------------------------------------------
23
+ // Annotations (SPEC: read-only against developer sandboxes)
24
+ // ---------------------------------------------------------------------------
25
+ const READ_ANNOTATIONS = {
26
+ readOnlyHint: true,
27
+ destructiveHint: false,
28
+ idempotentHint: true,
29
+ openWorldHint: true,
30
+ };
31
+ // ---------------------------------------------------------------------------
32
+ // Input schemas
33
+ // ---------------------------------------------------------------------------
34
+ const INSTANCE_ENUM = z.enum(["staging", "development", "sandbox", "production"]);
35
+ const INSTANCE_DESCRIBE = "OCAPI instance context. v1 supports the 'sandbox' context only; " +
36
+ "any other value is rejected with a validation error. Defaults to 'sandbox'.";
37
+ const sitePreferenceGetInput = z.object({
38
+ group: z.string().describe("Preference group ID, e.g. 'Account' or 'General'."),
39
+ instance: INSTANCE_ENUM.optional().default("sandbox").describe(INSTANCE_DESCRIBE),
40
+ start: z.number().optional().describe("Zero-based offset for paging."),
41
+ count: z.number().optional().describe("Maximum number of preferences to return."),
42
+ });
43
+ const sitePreferenceSearchInput = z.object({
44
+ group: z.string().describe("Preference group ID to search within."),
45
+ instance: INSTANCE_ENUM.optional().default("sandbox").describe(INSTANCE_DESCRIBE),
46
+ query: z.union([z.string(), z.record(z.string(), z.any())]).describe("Search query. Pass a plain string for text search across preference ids and values, " +
47
+ "or a structured OCAPI query object (term_query, filtered_query, etc.)."),
48
+ start: z.number().optional().describe("Zero-based offset for paging."),
49
+ count: z.number().optional().describe("Maximum number of results to return."),
50
+ sorts: z.array(z.any()).optional().describe("Array of OCAPI sort descriptors."),
51
+ });
52
+ // ---------------------------------------------------------------------------
53
+ // Helpers
54
+ // ---------------------------------------------------------------------------
55
+ function safeTimestamp() {
56
+ return new Date().toISOString().replace(/[:.]/g, "-");
57
+ }
58
+ function safeGroup(group) {
59
+ return encodeURIComponent(group).replace(/%/g, "_");
60
+ }
61
+ function textResult(text) {
62
+ return { content: [{ type: "text", text }] };
63
+ }
64
+ async function saveAndReturn(text, dir, filename) {
65
+ const output = await truncateAndSaveIfNeeded(text, dir, filename);
66
+ return textResult(output);
67
+ }
68
+ // D-5: reject any non-sandbox instance context (safer than silently proceeding)
69
+ function rejectIfNotSandbox(instance) {
70
+ if (instance !== "sandbox") {
71
+ return textResult(JSON.stringify({
72
+ error: "VALIDATION_ERROR",
73
+ status: 400,
74
+ message: `v1 only supports the 'sandbox' instance context. Received: '${instance}'.`,
75
+ }, null, 2));
76
+ }
77
+ return null;
78
+ }
79
+ // ---------------------------------------------------------------------------
80
+ // Handlers
81
+ // ---------------------------------------------------------------------------
82
+ export function buildSitePreferenceGetHandler(gateDeps, getDocsDir) {
83
+ return withSfccGate(gateDeps, async (args, credentials) => {
84
+ const { group, instance, start, count } = sitePreferenceGetInput.parse(args);
85
+ const guard = rejectIfNotSandbox(instance);
86
+ if (guard)
87
+ return guard;
88
+ const encodedGroup = encodeURIComponent(group);
89
+ const queryParams = {};
90
+ if (start !== undefined)
91
+ queryParams.start = String(start);
92
+ if (count !== undefined)
93
+ queryParams.count = String(count);
94
+ const queryStr = Object.keys(queryParams).length > 0
95
+ ? "?" + new URLSearchParams(queryParams).toString()
96
+ : "";
97
+ const result = await ocapiGet(`/site_preferences/preference_groups/${encodedGroup}/${instance}${queryStr}`, credentials);
98
+ if (!result.ok) {
99
+ return textResult(JSON.stringify({ error: "OCAPI error", status: result.status, body: result.body }, null, 2));
100
+ }
101
+ const normalized = normalizeOcapiBody(result.body);
102
+ const text = JSON.stringify(normalized, null, 2);
103
+ const dir = path.join(await getDocsDir(), "sfcc");
104
+ return saveAndReturn(text, dir, `site-preference-get-${safeGroup(group)}-${safeTimestamp()}.json`);
105
+ });
106
+ }
107
+ export function buildSitePreferenceSearchHandler(gateDeps, getDocsDir) {
108
+ return withSfccGate(gateDeps, async (args, credentials) => {
109
+ const { group, instance, query, start, count, sorts } = sitePreferenceSearchInput.parse(args);
110
+ const guard = rejectIfNotSandbox(instance);
111
+ if (guard)
112
+ return guard;
113
+ const encodedGroup = encodeURIComponent(group);
114
+ // Coerce plain string queries into OCAPI text_query shape
115
+ const resolvedQuery = typeof query === "string"
116
+ ? { text_query: { fields: ["id", "value"], search_phrase: query } }
117
+ : query;
118
+ const postBody = { query: resolvedQuery };
119
+ if (start !== undefined)
120
+ postBody.start = start;
121
+ if (count !== undefined)
122
+ postBody.count = count;
123
+ if (sorts !== undefined)
124
+ postBody.sorts = sorts;
125
+ const result = await ocapiPost(`/site_preferences/preference_groups/${encodedGroup}/${instance}/preference_search`, postBody, credentials);
126
+ if (!result.ok) {
127
+ return textResult(JSON.stringify({ error: "OCAPI error", status: result.status, body: result.body }, null, 2));
128
+ }
129
+ const normalized = normalizeOcapiBody(result.body);
130
+ const text = JSON.stringify(normalized, null, 2);
131
+ const dir = path.join(await getDocsDir(), "sfcc");
132
+ return saveAndReturn(text, dir, `site-preference-search-${safeGroup(group)}-${safeTimestamp()}.json`);
133
+ });
134
+ }
135
+ /**
136
+ * Register the two SFCC site-preference introspection read tools.
137
+ *
138
+ * Called from `registerSfccTools` — no direct `index.ts` edits needed.
139
+ */
140
+ export function registerSitePreferenceTools(registerTool, deps) {
141
+ const { gateDeps, getDocsDir } = deps;
142
+ registerTool("site_preference_get", {
143
+ description: "Read effective preferences for a site preference group from the developer sandbox " +
144
+ "via GET /site_preferences/preference_groups/{group}/sandbox. Read-only; v1 supports " +
145
+ "sandboxes only (non-sandbox instances are rejected). Accepts optional `start` and " +
146
+ "`count` for paging. Oversized payloads are auto-saved locally and previewed inline.",
147
+ inputSchema: sitePreferenceGetInput,
148
+ annotations: READ_ANNOTATIONS,
149
+ }, buildSitePreferenceGetHandler(gateDeps, getDocsDir));
150
+ registerTool("site_preference_search", {
151
+ description: "Search/filter preferences within a site preference group from the developer sandbox " +
152
+ "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 " +
154
+ "OCAPI query object. Oversized results are auto-saved locally.",
155
+ inputSchema: sitePreferenceSearchInput,
156
+ annotations: READ_ANNOTATIONS,
157
+ }, buildSitePreferenceSearchHandler(gateDeps, getDocsDir));
158
+ }