@bridge_gpt/mcp-server 0.2.12 → 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.
- package/README.md +63 -1
- package/build/conductor/doctor.js +78 -1
- package/build/conductor/epic-runtime.js +126 -54
- package/build/conductor/epic-state.js +20 -5
- package/build/conductor/local-merge.js +212 -0
- package/build/conductor/pr-ci-producer.js +12 -2
- package/build/conductor/store.js +7 -4
- package/build/conductor/taxonomy.js +4 -0
- package/build/conductor-bin.js +476 -137
- package/build/doctor.js +3 -0
- package/build/index.js +1818 -441
- package/build/init.js +57 -0
- package/build/mcp-profile.js +33 -30
- package/build/readme.generated.js +1 -1
- package/build/sfcc/client.js +151 -0
- package/build/sfcc/config.js +39 -0
- package/build/sfcc/credentials.js +136 -0
- package/build/sfcc/ocapi-shape.js +77 -0
- package/build/sfcc/output.js +39 -0
- package/build/sfcc/permissions.js +136 -0
- package/build/sfcc/reads-custom-object-def.js +119 -0
- package/build/sfcc/reads-site-preference.js +158 -0
- package/build/sfcc/reads-system-object.js +162 -0
- package/build/sfcc/register.js +73 -0
- package/build/sfcc/setup-status.js +114 -0
- package/build/sfcc/tool-wrapper.js +70 -0
- package/build/start-tickets-conductor.js +9 -1
- package/build/start-tickets.js +47 -4
- package/build/version.generated.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SFCC credential resolution — dw.json only (D-2: no Bridge credential store).
|
|
3
|
+
*
|
|
4
|
+
* Precedence: explicit tool arg → environment variable → dw.json.
|
|
5
|
+
* Multi-sandbox detection fails loudly with actionable guidance; no silent
|
|
6
|
+
* first-pick. Secret values (client_secret, tokens) are never included in
|
|
7
|
+
* any returned or thrown string — only source and target are reported.
|
|
8
|
+
*/
|
|
9
|
+
import { ensureGitInfoExcluded } from "../git-ignore-utils.js";
|
|
10
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
11
|
+
import path from "path";
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Environment variable names (precedence tier 2)
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
const ENV_HOSTNAME = "SFCC_HOSTNAME";
|
|
16
|
+
const ENV_CLIENT_ID = "SFCC_CLIENT_ID";
|
|
17
|
+
const ENV_CLIENT_SECRET = "SFCC_CLIENT_SECRET";
|
|
18
|
+
/** Canonical dw.json filename. */
|
|
19
|
+
const DW_JSON = "dw.json";
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Helpers
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
/** Redact-safe summary of a resolved hostname (no secrets). */
|
|
24
|
+
function safeHostLabel(hostname) {
|
|
25
|
+
return hostname.split(".")[0] ?? hostname;
|
|
26
|
+
}
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Main resolver
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
/**
|
|
31
|
+
* Resolve SFCC credentials with strict precedence and redacted error messages.
|
|
32
|
+
*
|
|
33
|
+
* @param explicitHostname - Optional instance hostname from tool argument (tier 1).
|
|
34
|
+
* @param env - Environment variables to check (tier 2). Defaults to `process.env`.
|
|
35
|
+
* @param deps - Optional filesystem I/O overrides for testing.
|
|
36
|
+
*/
|
|
37
|
+
export async function resolveSfccCredentials(explicitHostname, env = process.env, deps = {}) {
|
|
38
|
+
// --- Tier 1: explicit tool argument ---
|
|
39
|
+
if (explicitHostname) {
|
|
40
|
+
const clientId = env[ENV_CLIENT_ID];
|
|
41
|
+
const clientSecret = env[ENV_CLIENT_SECRET];
|
|
42
|
+
if (!clientId || !clientSecret) {
|
|
43
|
+
return {
|
|
44
|
+
ok: false,
|
|
45
|
+
error: `Explicit instance '${safeHostLabel(explicitHostname)}' provided but ` +
|
|
46
|
+
`${ENV_CLIENT_ID} and/or ${ENV_CLIENT_SECRET} are not set in environment. ` +
|
|
47
|
+
`Set them and retry.`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
ok: true,
|
|
52
|
+
credentials: {
|
|
53
|
+
hostname: explicitHostname,
|
|
54
|
+
clientId,
|
|
55
|
+
clientSecret,
|
|
56
|
+
source: `explicit arg + env (${ENV_CLIENT_ID}/${ENV_CLIENT_SECRET})`,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// --- Tier 2: environment variables ---
|
|
61
|
+
const envHostname = env[ENV_HOSTNAME];
|
|
62
|
+
const envClientId = env[ENV_CLIENT_ID];
|
|
63
|
+
const envClientSecret = env[ENV_CLIENT_SECRET];
|
|
64
|
+
if (envHostname && envClientId && envClientSecret) {
|
|
65
|
+
return {
|
|
66
|
+
ok: true,
|
|
67
|
+
credentials: {
|
|
68
|
+
hostname: envHostname,
|
|
69
|
+
clientId: envClientId,
|
|
70
|
+
clientSecret: envClientSecret,
|
|
71
|
+
source: `env (${ENV_HOSTNAME}/${ENV_CLIENT_ID}/${ENV_CLIENT_SECRET})`,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
// --- Tier 3: dw.json ---
|
|
76
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
77
|
+
const rf = deps.readFile ?? ((p) => readFile(p, "utf-8"));
|
|
78
|
+
const wf = deps.writeFile ?? ((p, data) => writeFile(p, data, "utf-8"));
|
|
79
|
+
const mk = deps.mkdir ?? ((p, opts) => mkdir(p, opts));
|
|
80
|
+
// Ensure dw.json is excluded from git tracking.
|
|
81
|
+
try {
|
|
82
|
+
await ensureGitInfoExcluded(cwd, DW_JSON, { readFile: rf, writeFile: wf, mkdir: mk });
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// Non-fatal — continue even if we can't update .git/info/exclude.
|
|
86
|
+
}
|
|
87
|
+
const dwJsonPath = path.join(cwd, DW_JSON);
|
|
88
|
+
let dwJson;
|
|
89
|
+
try {
|
|
90
|
+
const raw = await rf(dwJsonPath);
|
|
91
|
+
dwJson = JSON.parse(raw);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
error: "Could not read dw.json. Create a dw.json file in your project root " +
|
|
97
|
+
"with your SFCC sandbox credentials (hostname, client-id, client-secret).",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
// Detect multiple sandbox configurations.
|
|
101
|
+
// dw.json may carry a top-level 'configs' array (multi-sandbox format) or just a single config.
|
|
102
|
+
const configs = Array.isArray(dwJson.configs)
|
|
103
|
+
? dwJson.configs
|
|
104
|
+
: null;
|
|
105
|
+
if (configs && configs.length > 1) {
|
|
106
|
+
const instances = configs
|
|
107
|
+
.map((c) => safeHostLabel(String(c.hostname ?? c.host ?? "unknown")))
|
|
108
|
+
.join(", ");
|
|
109
|
+
return {
|
|
110
|
+
ok: false,
|
|
111
|
+
error: `dw.json contains multiple sandboxes (${instances}). ` +
|
|
112
|
+
`Pass an explicit 'instance' argument to select one: ${instances}.`,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
// Extract single-config credentials — support both top-level and configs[0] shapes.
|
|
116
|
+
const cfg = configs && configs.length === 1 ? configs[0] : dwJson;
|
|
117
|
+
const hostname = String(cfg.hostname ?? cfg.host ?? "");
|
|
118
|
+
const clientId = String(cfg["client-id"] ?? cfg.clientId ?? cfg.client_id ?? "");
|
|
119
|
+
const clientSecret = String(cfg["client-secret"] ?? cfg.clientSecret ?? cfg.client_secret ?? "");
|
|
120
|
+
if (!hostname || !clientId || !clientSecret) {
|
|
121
|
+
return {
|
|
122
|
+
ok: false,
|
|
123
|
+
error: "dw.json is present but missing required fields (hostname, client-id, client-secret). " +
|
|
124
|
+
"Ensure all three fields are set.",
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
ok: true,
|
|
129
|
+
credentials: {
|
|
130
|
+
hostname,
|
|
131
|
+
clientId,
|
|
132
|
+
clientSecret,
|
|
133
|
+
source: `dw.json (instance: ${safeHostLabel(hostname)})`,
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -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
|
+
}
|