@bridge_gpt/mcp-server 0.2.25 → 0.2.27

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 (73) hide show
  1. package/README.md +59 -9
  2. package/build/agents.generated.js +1 -1
  3. package/build/bridge-api-urls.js +31 -0
  4. package/build/commands.generated.js +4 -4
  5. package/build/conductor-bin.js +68 -9267
  6. package/build/conductor-bundle-artifacts.js +802 -0
  7. package/build/conductor-bundle-cli.js +256 -0
  8. package/build/conductor-claude-hook-bin.js +2 -172
  9. package/build/decision-page-template.js +2 -31
  10. package/build/docs.generated.js +2 -1
  11. package/build/doctor.js +148 -1
  12. package/build/env-flags.js +31 -0
  13. package/build/index.js +4659 -34836
  14. package/build/init.js +87 -59
  15. package/build/install-bridge.js +380 -22
  16. package/build/mcp-host-config.js +521 -0
  17. package/build/mcp-host-targets.js +194 -0
  18. package/build/mcp-install-state.js +175 -0
  19. package/build/pipelines.generated.js +5 -4
  20. package/build/readme.generated.js +1 -1
  21. package/build/start-tickets.js +118 -5
  22. package/build/tool-surface-gating.js +396 -0
  23. package/build/version.generated.js +1 -1
  24. package/docs/install/mcp-tool-integrations.md +2 -2
  25. package/package.json +5 -7
  26. package/design-assets/favicon/android-chrome-192x192.png +0 -0
  27. package/design-assets/favicon/android-chrome-512x512.png +0 -0
  28. package/design-assets/favicon/apple-touch-icon.png +0 -0
  29. package/design-assets/favicon/favicon-16x16.png +0 -0
  30. package/design-assets/favicon/favicon.ico +0 -0
  31. package/design-assets/favicon/site.webmanifest +0 -1
  32. package/public/css/main.min.css +0 -4971
  33. package/public/css/main.min.css.map +0 -1
  34. package/public/fonts/OFL.txt +0 -93
  35. package/public/fonts/SourceSansPro-Black.ttf +0 -0
  36. package/public/fonts/SourceSansPro-BlackItalic.ttf +0 -0
  37. package/public/fonts/SourceSansPro-Bold.ttf +0 -0
  38. package/public/fonts/SourceSansPro-BoldItalic.ttf +0 -0
  39. package/public/fonts/SourceSansPro-ExtraLight.ttf +0 -0
  40. package/public/fonts/SourceSansPro-ExtraLightItalic.ttf +0 -0
  41. package/public/fonts/SourceSansPro-Italic.ttf +0 -0
  42. package/public/fonts/SourceSansPro-Light.ttf +0 -0
  43. package/public/fonts/SourceSansPro-LightItalic.ttf +0 -0
  44. package/public/fonts/SourceSansPro-Regular.ttf +0 -0
  45. package/public/fonts/SourceSansPro-SemiBold.ttf +0 -0
  46. package/public/fonts/SourceSansPro-SemiBoldItalic.ttf +0 -0
  47. package/public/img/bridge-logo-160x51.webp +0 -0
  48. package/public/img/bridge-logo-300x92.webp +0 -0
  49. package/public/img/favicon/android-chrome-192x192.png +0 -0
  50. package/public/img/favicon/android-chrome-512x512.png +0 -0
  51. package/public/img/favicon/apple-touch-icon.png +0 -0
  52. package/public/img/favicon/favicon-16x16.png +0 -0
  53. package/public/img/favicon/favicon-32x32.png +0 -0
  54. package/public/img/favicon/favicon.ico +0 -0
  55. package/public/img/favicon/site.webmanifest +0 -1
  56. package/public/img/installation/bitbucket/app-password-1.png +0 -0
  57. package/public/img/installation/bitbucket/app-password-2.png +0 -0
  58. package/public/img/installation/bitbucket/create-token-1.png +0 -0
  59. package/public/img/installation/bitbucket/create-token-2.png +0 -0
  60. package/public/img/installation/bitbucket/webhook-1.png +0 -0
  61. package/public/img/installation/github/github-review-webhook.png +0 -0
  62. package/public/img/installation/jira/credentials/api-key.png +0 -0
  63. package/public/img/installation/jira/webhook/create-rule.png +0 -0
  64. package/public/img/installation/jira/webhook/project-settings.png +0 -0
  65. package/public/img/installation/jira/webhook/rule-create-1.png +0 -0
  66. package/public/img/installation/jira/webhook/rule-create-2.png +0 -0
  67. package/public/img/installation/jira/webhook/rule-create-3.png +0 -0
  68. package/public/img/installation/pinecone/pinecone-api-key.png +0 -0
  69. package/public/img/installation/pinecone/pinecone-index.png +0 -0
  70. package/public/js/main.min.js +0 -10427
  71. package/public/js/main.min.js.map +0 -1
  72. package/smoke-test/SMOKE-TEST.md +0 -560
  73. package/smoke-test/smoke-test-mcp.md +0 -23
@@ -0,0 +1,256 @@
1
+ /**
2
+ * BAPI-634: thin CLI handler exposing the deterministic conductor-bundle helpers
3
+ * to `emit-conductor-bundle.md`.
4
+ *
5
+ * Why this exists: the instruction ships **compiled** to consuming repositories
6
+ * inside `build/pipelines.generated.js`. `mcp_server/src/` does not exist there,
7
+ * so an instruction that told the agent to "call `validateConductorBundleInputs`"
8
+ * would be referencing a path only this repository has. This subcommand is the
9
+ * reachable surface, mirroring how `store-and-approve-epic-plan.md` invokes
10
+ * `setup-epic` rather than hand-rolling HTTP calls.
11
+ *
12
+ * It holds no policy of its own: it parses argv, delegates to
13
+ * `conductor-bundle-artifacts.ts`, and prints a JSON envelope. All I/O is behind
14
+ * an injectable seam so tests never touch a real disk or construct a server.
15
+ */
16
+ import path from "node:path";
17
+ import { finalizeEpicPlanSidecar, serializeSiblingTicketManifest, validateConductorBundleInputs, writeJsonAtomically, } from "./conductor-bundle-artifacts.js";
18
+ const USAGE = [
19
+ "Usage: emit-conductor-bundle <validate|finalize> --input <file> [--docs-dir <dir>] [--json]",
20
+ "",
21
+ " validate Validate identities, the node->ticket mapping, manifest agreement,",
22
+ " and path containment. Writes nothing.",
23
+ " finalize Validate, then finalize epic-plan.dag.json with real keys and",
24
+ " per-node touched_files, and write the sibling-ticket manifest.",
25
+ "",
26
+ "Options:",
27
+ " --input <file> JSON document with epic_key, epic_slug, mappings,",
28
+ " decomposition_fingerprint, and (for finalize)",
29
+ " touched_files_by_key.",
30
+ " --docs-dir <dir> Docs directory (default: $BAPI_DOCS_DIR, else docs/tmp).",
31
+ " --json Emit a machine-readable result on stdout.",
32
+ " -h, --help Show this help.",
33
+ ].join("\n");
34
+ /** Read a flag's value, rejecting a missing or flag-shaped value. */
35
+ function takeValue(argv, index, flag) {
36
+ const value = argv[index + 1];
37
+ if (value === undefined || value.startsWith("-")) {
38
+ throw new Error(`Flag "${flag}" requires a value.`);
39
+ }
40
+ return value;
41
+ }
42
+ /** Parse argv into options, or throw with an actionable message. */
43
+ export function parseConductorBundleArgs(argv) {
44
+ const mode = argv[0];
45
+ if (mode === "-h" || mode === "--help" || mode === undefined) {
46
+ return { mode: "validate", inputFile: "", json: false, help: true };
47
+ }
48
+ if (mode !== "validate" && mode !== "finalize") {
49
+ throw new Error(`Unknown subcommand "${mode}". Expected "validate" or "finalize".`);
50
+ }
51
+ let inputFile;
52
+ let docsDir;
53
+ let json = false;
54
+ let help = false;
55
+ for (let i = 1; i < argv.length; i++) {
56
+ const arg = argv[i];
57
+ switch (arg) {
58
+ case "--input":
59
+ inputFile = takeValue(argv, i, "--input");
60
+ i++;
61
+ break;
62
+ case "--docs-dir":
63
+ docsDir = takeValue(argv, i, "--docs-dir");
64
+ i++;
65
+ break;
66
+ case "--json":
67
+ json = true;
68
+ break;
69
+ case "-h":
70
+ case "--help":
71
+ help = true;
72
+ break;
73
+ default:
74
+ throw new Error(`Unknown argument "${arg}".`);
75
+ }
76
+ }
77
+ if (help)
78
+ return { mode, inputFile: inputFile ?? "", docsDir, json, help: true };
79
+ if (!inputFile)
80
+ throw new Error('Flag "--input" is required.');
81
+ return { mode, inputFile, docsDir, json, help: false };
82
+ }
83
+ /** Parse JSON, converting a syntax error into an actionable message. */
84
+ async function readJson(filePath, fs, label) {
85
+ let raw;
86
+ try {
87
+ raw = await fs.readFile(filePath);
88
+ }
89
+ catch (err) {
90
+ throw new Error(`Could not read ${label} at ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
91
+ }
92
+ try {
93
+ return JSON.parse(raw);
94
+ }
95
+ catch (err) {
96
+ throw new Error(`${label} at ${filePath} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
97
+ }
98
+ }
99
+ /** Read an optional JSON file, returning null when it does not exist. */
100
+ async function readOptionalJson(filePath, fs, label) {
101
+ try {
102
+ await fs.readFile(filePath);
103
+ }
104
+ catch {
105
+ return null;
106
+ }
107
+ return readJson(filePath, fs, label);
108
+ }
109
+ /**
110
+ * Run the `emit-conductor-bundle` subcommand. Returns a process exit code; never
111
+ * throws for an expected validation failure.
112
+ */
113
+ export async function runConductorBundleCli(argv, overrides = {}) {
114
+ const deps = {
115
+ env: process.env,
116
+ cwd: process.cwd(),
117
+ fs: createDefaultBundleFs(),
118
+ log: (m) => console.log(m),
119
+ // Diagnostics go to stderr so `--json` keeps stdout a single result object.
120
+ errorLog: (m) => console.error(m),
121
+ ...overrides,
122
+ };
123
+ let opts;
124
+ try {
125
+ opts = parseConductorBundleArgs(argv);
126
+ }
127
+ catch (err) {
128
+ deps.errorLog(err instanceof Error ? err.message : String(err));
129
+ deps.errorLog(USAGE);
130
+ return 1;
131
+ }
132
+ if (opts.help) {
133
+ deps.log(USAGE);
134
+ return 0;
135
+ }
136
+ const emitFailure = (error) => {
137
+ if (opts.json)
138
+ deps.log(JSON.stringify({ ok: false, error }));
139
+ else
140
+ deps.errorLog(`Error: ${error}`);
141
+ return 1;
142
+ };
143
+ const docsDir = path.resolve(deps.cwd, opts.docsDir ?? deps.env.BAPI_DOCS_DIR ?? path.join("docs", "tmp"));
144
+ let input;
145
+ try {
146
+ input = (await readJson(path.resolve(deps.cwd, opts.inputFile), deps.fs, "--input document"));
147
+ }
148
+ catch (err) {
149
+ return emitFailure(err instanceof Error ? err.message : String(err));
150
+ }
151
+ if (!input || typeof input !== "object") {
152
+ return emitFailure("--input document must be a JSON object.");
153
+ }
154
+ const epicDir = path.resolve(docsDir, "epic-plans", String(input.epic_slug));
155
+ const sidecarPath = path.join(epicDir, "epic-plan.dag.json");
156
+ const manifestPath = path.join(epicDir, "sibling-ticket-manifest.json");
157
+ let sidecar;
158
+ let existingManifest;
159
+ try {
160
+ sidecar = await readJson(sidecarPath, deps.fs, "epic-plan.dag.json");
161
+ existingManifest = await readOptionalJson(manifestPath, deps.fs, "sibling-ticket-manifest.json");
162
+ }
163
+ catch (err) {
164
+ return emitFailure(err instanceof Error ? err.message : String(err));
165
+ }
166
+ const validation = await validateConductorBundleInputs({
167
+ epic_key: input.epic_key,
168
+ epic_slug: input.epic_slug,
169
+ docs_dir: docsDir,
170
+ mappings: input.mappings,
171
+ sidecar,
172
+ existing_manifest: existingManifest ?? undefined,
173
+ decomposition_fingerprint: input.decomposition_fingerprint,
174
+ }, deps.fs);
175
+ if (!validation.ok)
176
+ return emitFailure(validation.error);
177
+ if (opts.mode === "validate") {
178
+ const result = {
179
+ ok: true,
180
+ mode: "validate",
181
+ epic_key: validation.value.epic_key,
182
+ plan_version: validation.value.plan_version,
183
+ mapped_nodes: validation.value.mappings.length,
184
+ sidecar_path: validation.value.sidecar_path,
185
+ };
186
+ if (opts.json)
187
+ deps.log(JSON.stringify(result));
188
+ else {
189
+ deps.log(`Validated ${result.mapped_nodes} node mapping(s) for ${result.epic_key} ` +
190
+ `(plan v${result.plan_version}). Nothing was written.`);
191
+ }
192
+ return 0;
193
+ }
194
+ // ---- finalize ----------------------------------------------------------
195
+ const nodeKeyMap = {};
196
+ for (const m of validation.value.mappings) {
197
+ nodeKeyMap[m.plan_node_id] = m.ticket_key;
198
+ }
199
+ const finalized = finalizeEpicPlanSidecar({
200
+ sidecar,
201
+ node_key_map: nodeKeyMap,
202
+ touched_files_by_key: input.touched_files_by_key ?? {},
203
+ plan_version_already_stored: input.plan_version_already_stored === true,
204
+ });
205
+ if (!finalized.ok)
206
+ return emitFailure(finalized.error);
207
+ const sidecarWrite = await writeJsonAtomically(validation.value.sidecar_path, finalized.value, deps.fs);
208
+ if (!sidecarWrite.ok)
209
+ return emitFailure(sidecarWrite.error);
210
+ const manifest = serializeSiblingTicketManifest({
211
+ schema_version: 1,
212
+ epic_key: validation.value.epic_key,
213
+ epic_slug: validation.value.epic_slug,
214
+ plan_version: validation.value.plan_version,
215
+ decomposition_fingerprint: input.decomposition_fingerprint,
216
+ finalized_fingerprint: null,
217
+ run_phase: input.run_phase ?? "staged",
218
+ mappings: validation.value.mappings.map((m) => ({
219
+ plan_node_id: m.plan_node_id,
220
+ ticket_key: m.ticket_key,
221
+ exploration_path: m.exploration_path,
222
+ draft_path: m.draft_path,
223
+ })),
224
+ decisions: [],
225
+ completed_mutations: [],
226
+ });
227
+ const manifestWrite = await writeJsonAtomically(validation.value.manifest_path, manifest, deps.fs);
228
+ if (!manifestWrite.ok)
229
+ return emitFailure(manifestWrite.error);
230
+ const result = {
231
+ ok: true,
232
+ mode: "finalize",
233
+ epic_key: validation.value.epic_key,
234
+ plan_version: validation.value.plan_version,
235
+ sidecar_path: validation.value.sidecar_path,
236
+ manifest_path: validation.value.manifest_path,
237
+ ticket_keys: validation.value.mappings.map((m) => m.ticket_key),
238
+ };
239
+ if (opts.json)
240
+ deps.log(JSON.stringify(result));
241
+ else {
242
+ deps.log(`Finalized ${result.sidecar_path} with ${result.ticket_keys.length} real key(s) ` +
243
+ `and per-node touched_files. Manifest: ${result.manifest_path}`);
244
+ }
245
+ return 0;
246
+ }
247
+ /** Real filesystem seam, loaded lazily so importing this module stays cheap. */
248
+ function createDefaultBundleFs() {
249
+ return {
250
+ readFile: async (p) => (await import("node:fs/promises")).readFile(p, "utf-8"),
251
+ writeFile: async (p, data) => (await import("node:fs/promises")).writeFile(p, data, "utf-8"),
252
+ rename: async (from, to) => (await import("node:fs/promises")).rename(from, to),
253
+ unlink: async (p) => (await import("node:fs/promises")).unlink(p),
254
+ realpath: async (p) => (await import("node:fs/promises")).realpath(p),
255
+ };
256
+ }
@@ -1,173 +1,3 @@
1
1
  #!/usr/bin/env node
2
-
3
- // src/conductor/claude-hook.ts
4
- import { spawnSync } from "node:child_process";
5
- import { readFileSync } from "node:fs";
6
- var CONDUCTOR_HOOK_EMIT_FAILED_WARNING = "Warning: conductor hook emit failed.";
7
- function resolveClaudeHookEventName(payload) {
8
- if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
9
- return null;
10
- }
11
- const record = payload;
12
- const candidates = ["hook_event_name", "hookEventName", "event_name", "event"];
13
- for (const key of candidates) {
14
- const value = record[key];
15
- if (typeof value === "string" && value.trim().length > 0) {
16
- return value.trim();
17
- }
18
- }
19
- return null;
20
- }
21
- function mapClaudeHookEventToSemanticType(eventName) {
22
- switch (eventName) {
23
- case "SessionStart":
24
- return "run.started";
25
- case "SessionEnd":
26
- return "run.stopped";
27
- case "Stop":
28
- return null;
29
- case "SubagentStop":
30
- return null;
31
- case "Notification":
32
- return "agent.notification";
33
- case "PreToolUse":
34
- return "tool.intent";
35
- default:
36
- return null;
37
- }
38
- }
39
- var REQUIRED_IDENTITY_ENV = [
40
- "BAPI_CONDUCTOR_RUN_ID",
41
- "BAPI_CONDUCTOR_WORKER_ID",
42
- "BAPI_CONDUCTOR_TICKET_KEY",
43
- "BAPI_CONDUCTOR_WORKTREE_PATH"
44
- ];
45
- function nonEmpty(value) {
46
- return typeof value === "string" && value.trim().length > 0;
47
- }
48
- function buildClaudeHookConductorEvent(payload, env) {
49
- const eventName = resolveClaudeHookEventName(payload);
50
- const type = mapClaudeHookEventToSemanticType(eventName);
51
- if (type === null) {
52
- return null;
53
- }
54
- if (env.BAPI_CONDUCTOR_ENABLED !== "1") {
55
- return null;
56
- }
57
- for (const key of REQUIRED_IDENTITY_ENV) {
58
- if (!nonEmpty(env[key])) {
59
- return null;
60
- }
61
- }
62
- const ticketKey = env.BAPI_CONDUCTOR_TICKET_KEY.trim();
63
- const worktreePath = env.BAPI_CONDUCTOR_WORKTREE_PATH.trim();
64
- const details = {
65
- ticket_key: ticketKey,
66
- worktree_path: worktreePath
67
- };
68
- if (nonEmpty(env.BAPI_CONDUCTOR_REPO_NAME)) {
69
- details.repo = env.BAPI_CONDUCTOR_REPO_NAME.trim();
70
- }
71
- const raw = payload !== null && typeof payload === "object" && !Array.isArray(payload) ? payload : { payload };
72
- return {
73
- source: "claude-code",
74
- type,
75
- subject: ticketKey,
76
- run_id: env.BAPI_CONDUCTOR_RUN_ID.trim(),
77
- worker_id: env.BAPI_CONDUCTOR_WORKER_ID.trim(),
78
- producer: "claude-code-hook",
79
- observed_via: "claude-code-hook",
80
- data: {
81
- details,
82
- raw
83
- }
84
- };
85
- }
86
- function buildConductorEmitEventArgs(event) {
87
- const args = ["--type", event.type, "--source", event.source];
88
- if (nonEmpty(event.subject ?? void 0)) args.push("--subject", event.subject);
89
- if (nonEmpty(event.run_id ?? void 0)) args.push("--run-id", event.run_id);
90
- if (nonEmpty(event.worker_id ?? void 0)) args.push("--worker-id", event.worker_id);
91
- if (nonEmpty(event.producer ?? void 0)) args.push("--producer", event.producer);
92
- if (nonEmpty(event.observed_via ?? void 0)) {
93
- args.push("--observed-via", event.observed_via);
94
- }
95
- args.push("--data-json-stdin", "--json");
96
- return args;
97
- }
98
- function resolveConductorEmitCommand(env, emitArgs) {
99
- const cliFile = env.BAPI_CONDUCTOR_CLI_FILE;
100
- if (nonEmpty(cliFile)) {
101
- return {
102
- command: process.execPath,
103
- args: [cliFile.trim(), "emit-event", ...emitArgs]
104
- };
105
- }
106
- const bin = nonEmpty(env.BAPI_CONDUCTOR_BIN) ? env.BAPI_CONDUCTOR_BIN.trim() : "conductor";
107
- return { command: bin, args: ["emit-event", ...emitArgs] };
108
- }
109
- var defaultConductorSpawn = (command, args, input) => {
110
- const result = spawnSync(command, args, {
111
- input,
112
- encoding: "utf-8",
113
- // Never use a shell: list args + stdin keep raw payloads/secrets out of any
114
- // shell-interpreted command string.
115
- shell: false
116
- });
117
- return { status: result.status, error: result.error };
118
- };
119
- function emitClaudeHookEventWithConductorCli(event, deps = {}) {
120
- const env = deps.env ?? process.env;
121
- const spawn = deps.spawn ?? defaultConductorSpawn;
122
- const emitArgs = buildConductorEmitEventArgs(event);
123
- const { command, args } = resolveConductorEmitCommand(env, emitArgs);
124
- const stdinPayload = JSON.stringify(event.data ?? {});
125
- try {
126
- const result = spawn(command, args, stdinPayload);
127
- if (result.error) return false;
128
- return result.status === 0;
129
- } catch {
130
- return false;
131
- }
132
- }
133
- function runClaudeConductorHookCli(deps = {}) {
134
- const env = deps.env ?? process.env;
135
- const readStdin = deps.readStdin ?? (() => readStdinSync());
136
- const warn = deps.warn ?? ((m) => process.stderr.write(`${m}
137
- `));
138
- let payload;
139
- try {
140
- const raw = readStdin();
141
- payload = JSON.parse(raw);
142
- } catch {
143
- warn(CONDUCTOR_HOOK_EMIT_FAILED_WARNING);
144
- return 0;
145
- }
146
- let event;
147
- try {
148
- event = buildClaudeHookConductorEvent(payload, env);
149
- } catch {
150
- warn(CONDUCTOR_HOOK_EMIT_FAILED_WARNING);
151
- return 0;
152
- }
153
- if (event === null) {
154
- return 0;
155
- }
156
- const ok = emitClaudeHookEventWithConductorCli(event, { env, spawn: deps.spawn });
157
- if (!ok) {
158
- warn(CONDUCTOR_HOOK_EMIT_FAILED_WARNING);
159
- }
160
- return 0;
161
- }
162
- function readStdinSync() {
163
- return readFileSync(0, "utf-8");
164
- }
165
-
166
- // src/conductor-claude-hook-bin.ts
167
- var exitCode = 0;
168
- try {
169
- exitCode = runClaudeConductorHookCli();
170
- } catch {
171
- exitCode = 0;
172
- }
173
- process.exit(exitCode);
2
+ import{spawnSync}from"node:child_process";import{readFileSync}from"node:fs";var CONDUCTOR_HOOK_EMIT_FAILED_WARNING="Warning: conductor hook emit failed.";function resolveClaudeHookEventName(payload){if(payload===null||typeof payload!="object"||Array.isArray(payload))return null;let record=payload,candidates=["hook_event_name","hookEventName","event_name","event"];for(let key of candidates){let value=record[key];if(typeof value=="string"&&value.trim().length>0)return value.trim()}return null}function mapClaudeHookEventToSemanticType(eventName){switch(eventName){case"SessionStart":return"run.started";case"SessionEnd":return"run.stopped";case"Stop":return null;case"SubagentStop":return null;case"Notification":return"agent.notification";case"PreToolUse":return"tool.intent";default:return null}}var REQUIRED_IDENTITY_ENV=["BAPI_CONDUCTOR_RUN_ID","BAPI_CONDUCTOR_WORKER_ID","BAPI_CONDUCTOR_TICKET_KEY","BAPI_CONDUCTOR_WORKTREE_PATH"];function nonEmpty(value){return typeof value=="string"&&value.trim().length>0}function buildClaudeHookConductorEvent(payload,env){let eventName=resolveClaudeHookEventName(payload),type=mapClaudeHookEventToSemanticType(eventName);if(type===null||env.BAPI_CONDUCTOR_ENABLED!=="1")return null;for(let key of REQUIRED_IDENTITY_ENV)if(!nonEmpty(env[key]))return null;let ticketKey=env.BAPI_CONDUCTOR_TICKET_KEY.trim(),worktreePath=env.BAPI_CONDUCTOR_WORKTREE_PATH.trim(),details={ticket_key:ticketKey,worktree_path:worktreePath};nonEmpty(env.BAPI_CONDUCTOR_REPO_NAME)&&(details.repo=env.BAPI_CONDUCTOR_REPO_NAME.trim());let raw=payload!==null&&typeof payload=="object"&&!Array.isArray(payload)?payload:{payload};return{source:"claude-code",type,subject:ticketKey,run_id:env.BAPI_CONDUCTOR_RUN_ID.trim(),worker_id:env.BAPI_CONDUCTOR_WORKER_ID.trim(),producer:"claude-code-hook",observed_via:"claude-code-hook",data:{details,raw}}}function buildConductorEmitEventArgs(event){let args=["--type",event.type,"--source",event.source];return nonEmpty(event.subject??void 0)&&args.push("--subject",event.subject),nonEmpty(event.run_id??void 0)&&args.push("--run-id",event.run_id),nonEmpty(event.worker_id??void 0)&&args.push("--worker-id",event.worker_id),nonEmpty(event.producer??void 0)&&args.push("--producer",event.producer),nonEmpty(event.observed_via??void 0)&&args.push("--observed-via",event.observed_via),args.push("--data-json-stdin","--json"),args}function resolveConductorEmitCommand(env,emitArgs){let cliFile=env.BAPI_CONDUCTOR_CLI_FILE;return nonEmpty(cliFile)?{command:process.execPath,args:[cliFile.trim(),"emit-event",...emitArgs]}:{command:nonEmpty(env.BAPI_CONDUCTOR_BIN)?env.BAPI_CONDUCTOR_BIN.trim():"conductor",args:["emit-event",...emitArgs]}}var defaultConductorSpawn=(command,args,input)=>{let result=spawnSync(command,args,{input,encoding:"utf-8",shell:!1});return{status:result.status,error:result.error}};function emitClaudeHookEventWithConductorCli(event,deps={}){let env=deps.env??process.env,spawn=deps.spawn??defaultConductorSpawn,emitArgs=buildConductorEmitEventArgs(event),{command,args}=resolveConductorEmitCommand(env,emitArgs),stdinPayload=JSON.stringify(event.data??{});try{let result=spawn(command,args,stdinPayload);return result.error?!1:result.status===0}catch{return!1}}function runClaudeConductorHookCli(deps={}){let env=deps.env??process.env,readStdin=deps.readStdin??(()=>readStdinSync()),warn=deps.warn??(m=>process.stderr.write(`${m}
3
+ `)),payload;try{let raw=readStdin();payload=JSON.parse(raw)}catch{return warn(CONDUCTOR_HOOK_EMIT_FAILED_WARNING),0}let event;try{event=buildClaudeHookConductorEvent(payload,env)}catch{return warn(CONDUCTOR_HOOK_EMIT_FAILED_WARNING),0}return event===null||emitClaudeHookEventWithConductorCli(event,{env,spawn:deps.spawn})||warn(CONDUCTOR_HOOK_EMIT_FAILED_WARNING),0}function readStdinSync(){return readFileSync(0,"utf-8")}var exitCode=0;try{exitCode=runClaudeConductorHookCli()}catch{exitCode=0}process.exit(exitCode);
@@ -59,7 +59,7 @@ function hasDisplayText(value) {
59
59
  // ---------------------------------------------------------------------------
60
60
  // Template
61
61
  // ---------------------------------------------------------------------------
62
- const DEFAULT_ASSETS = { faviconBase64: "", logoBase64: "", fontsRelPath: "" };
62
+ const DEFAULT_ASSETS = { faviconBase64: "", logoBase64: "" };
63
63
  export function generateDecisionPageHtml(data, assets = DEFAULT_ASSETS) {
64
64
  const { ticket_key, actionable_items, clear_improvements } = data;
65
65
  const hasDecisions = actionable_items.length > 0;
@@ -73,7 +73,6 @@ export function generateDecisionPageHtml(data, assets = DEFAULT_ASSETS) {
73
73
  const faviconLink = assets.faviconBase64
74
74
  ? `<link rel="icon" type="image/png" sizes="32x32" href="data:image/png;base64,${assets.faviconBase64}">`
75
75
  : "";
76
- const fontFaces = assets.fontsRelPath ? renderFontFaces(assets.fontsRelPath) : "";
77
76
  // The has-decisions intro is an overridable label; the no-decisions copy is a
78
77
  // fixed constant. For review_decisions pages the MCP handler short-circuits
79
78
  // before rendering when there are no decisions, so that branch is reachable
@@ -93,7 +92,6 @@ export function generateDecisionPageHtml(data, assets = DEFAULT_ASSETS) {
93
92
  <title>${escapeHtml(effectiveLabels.title)}: ${escapeHtml(ticket_key)}</title>
94
93
  ${faviconLink}
95
94
  <style>
96
- ${fontFaces}
97
95
  :root {
98
96
  --primary-color: #E2624B;
99
97
  --primary-dark: #C51616;
@@ -112,7 +110,7 @@ ${fontFaces}
112
110
  }
113
111
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
114
112
  body {
115
- font-family: 'Source Sans Pro', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
113
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
116
114
  background-color: var(--background-color);
117
115
  color: var(--text-color);
118
116
  line-height: 1.5;
@@ -541,33 +539,6 @@ ${needsForm ? renderScript(data, isPlanning) : ""}
541
539
  </html>`;
542
540
  }
543
541
  // ---------------------------------------------------------------------------
544
- // Font faces
545
- // ---------------------------------------------------------------------------
546
- // fontsRelPath comes from server-controlled env vars, so this is not an
547
- // injection vector — but a deploy path containing a single quote or backslash
548
- // would silently break CSS parsing and stop fonts from loading. Escape both
549
- // so the url('...') stays well-formed regardless of deploy layout.
550
- function escapeCssUrl(value) {
551
- return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
552
- }
553
- function renderFontFaces(fontsRelPath) {
554
- const weights = [
555
- { weight: "400", style: "normal", file: "SourceSansPro-Regular.ttf" },
556
- { weight: "400", style: "italic", file: "SourceSansPro-Italic.ttf" },
557
- { weight: "600", style: "normal", file: "SourceSansPro-SemiBold.ttf" },
558
- { weight: "600", style: "italic", file: "SourceSansPro-SemiBoldItalic.ttf" },
559
- { weight: "700", style: "normal", file: "SourceSansPro-Bold.ttf" },
560
- { weight: "700", style: "italic", file: "SourceSansPro-BoldItalic.ttf" },
561
- ];
562
- const safePath = escapeCssUrl(fontsRelPath);
563
- return weights.map(({ weight, style, file }) => ` @font-face {
564
- font-family: 'Source Sans Pro';
565
- src: url('${safePath}/${escapeCssUrl(file)}') format('truetype');
566
- font-weight: ${weight};
567
- font-style: ${style};
568
- }`).join("\n");
569
- }
570
- // ---------------------------------------------------------------------------
571
542
  // Section renderers
572
543
  // ---------------------------------------------------------------------------
573
544
  function renderHeader(assets) {
@@ -1,5 +1,6 @@
1
1
  // AUTO-GENERATED — do not edit manually. Regenerate with: npm run build
2
2
  // This file is produced by scripts/bundle-docs.js
3
3
  export const DOCS = {
4
- "docs/mcp-tool-integrations.md": "# MCP tool integrations — the human \"why\" behind the capability report\n\nThis catalog is **explanatory prose only**. It exists so the `/install-bridge`\ncapability report can cite a human-readable \"why\" for each gate. It is **not** a\nsource of truth for gating: the server computes every `locked_tools` /\n`unlocked_tools` membership decision itself and the agent must never recompute a\ntool's dependencies from this document.\n\n**Authoritative source of gating.** The enforced rules — which tools are blocked,\nwhich are degraded, and what each requires — live in\n`api/library/vcs/vcs_route_operations.py`:\n\n- `VCS_ROUTE_REQUIREMENTS` — routes that **BLOCK** (are unavailable) without a\n VCS connection.\n- `VCS_ROUTE_WARNINGS` — routes that **DEGRADE** (stay usable, but without\n codebase context) without a VCS connection.\n- `NEVER_GATED_ROUTE_KEYS` — routes that are never gated on any integration.\n- `INDEX_REQUIRED_ROUTE_KEYS`, `INDEX_REQUIRED_BRAINSTORM_MODES`,\n `CREATE_DOC_CODEBASE_CONTEXT_DOC_TYPES`, `CREATE_DOC_WARN_DOC_TYPES` — the\n conditional \"requires a successful code index\" dimension.\n- The resolver helpers `get_required_vcs_operation()`, `get_warn_vcs_operation()`,\n and `requires_successful_index()` are the authoritative functions that decide a\n case. The capability report is derived from these; this catalog explains them.\n\n## Reading the capability report\n\nEach tool entry the server returns has the exact shape\n`{tool, effect, missing, semantics}`:\n\n- **`effect`**\n - **`BLOCK`** — the tool is **unavailable** until every listed dependency is\n met. It will refuse to run without them.\n - **`DEGRADE`** — the tool is **usable right now**, but **without codebase\n context** (it cannot ground its output in your repository). Connecting the\n listed dependency upgrades it from \"works blind\" to \"works with full context\".\n A `DEGRADE` tool is never \"failed\".\n- **`missing`** — the server-computed dependency identifiers still needed:\n integration ids such as `github_app` / `vcs_access_token`, and the synthetic\n `code_index` (a successful repository index).\n- **`semantics`**\n - **`all_of`** — every id in `missing` is required.\n - **`any_of`** — the VCS-provider candidates in `missing` are alternatives:\n **either** `github_app` **or** `vcs_access_token` satisfies the VCS\n requirement (this is the \"provider unknown\" case). When `code_index` also\n appears, it remains separately required — `semantics` describes only the VCS\n provider candidates, and a code index is always mandatory in addition.\n\nThe three readiness dimensions `configured` / `learned` / `indexed` are reported\nindependently. `indexed` may be `true`, `false`, or `null` — a `null` means the\nindex status could not be confirmed and must **not** be read as \"indexed\".\n\n## The integrations\n\n| Integration id | What it is | What it unlocks |\n| --- | --- | --- |\n| `jira` | Jira API access | Ticket reads/writes, estimation and review automations, status transitions. |\n| `github_app` | GitHub App installation | Pull requests, code review, and private-repo parsing on GitHub projects. |\n| `vcs_access_token` | VCS access token | Pull requests, code review, and private-repo parsing on Bitbucket projects. |\n| `vcs_webhook` | VCS webhook secret | Merge webhooks and CI follow-up triggers. |\n| `code_index` | A successful repository index | Codebase-grounded planning, architecture, reimplementation, and technical/discovery brainstorms. Produced by `/parse-repository`. |\n\nA project's `github_app` **or** `vcs_access_token` provides the VCS connection;\nwhich one applies depends on the project's version-control system. When the\nproject's provider is unknown, either credential satisfies the requirement — the\nreport expresses that as `semantics: any_of`.\n\n## The gates, by capability\n\n### Pull requests and CI (BLOCK on VCS)\n\nTools like `create_pull_request`, `resolve_ci_checks`, `poll_ci_checks`, and\n`materialize_fresh_base` are **unavailable** (`BLOCK`) until a VCS connection is\nconfigured. They act directly on the version-control host, so without a\nconnection there is nothing for them to talk to.\n\n### Repository indexing and maps (BLOCK on VCS)\n\n`parse_repository` and `regenerate_directory_map` need a VCS connection to read\nthe repository. They **BLOCK** until VCS is connected.\n\n### Codebase-grounded generation (BLOCK on VCS **and** a code index)\n\nPlanning and architecture tools — `generate_plan_direct`,\n`generate_architecture_direct`, `request_reimplement_context`,\n`code_writer_generate_plan`, `code_writer_generate_architecture`, and\n`create_doc` for **TDD** / **architecture** documents — ground their output in\nyour indexed codebase. They **BLOCK** until BOTH a VCS connection AND a\nsuccessful code index exist (`all_of`, with `code_index` in `missing`).\n\n### Brainstorms (BLOCK on a code index, mode-dependent)\n\n`request_brainstorm` in **technical** or **discovery** mode searches your indexed\ncodebase, so it **BLOCK**s on `code_index`. **Design**-mode brainstorming never\nqueries the index and is never gated.\n\n### Document generation that DEGRADEs (usable without codebase context)\n\nTools like `generate_prd_direct`, `generate_fsd_direct`,\n`code_writer_generate_fsd`, `generate_clarifying_questions_direct`,\n`generate_ticket_critique_direct`, `generate_ticket_review_direct`, and\n`create_doc` for **PRD** / **FSD** documents **DEGRADE** rather than block: they\nrun today from the ticket alone, and connecting VCS simply lets them ground their\noutput in your codebase. They always appear under \"Tools you can use now\", with a\nreduced-context caveat when the VCS connection is missing.\n\n### Never gated\n\nSetup and bootstrap tools (`ping`, `config_field`, `get_install_manifest`,\n`apply_install_manifest`, `get_my_role`, `persist_routing_credential`,\n`get_docs_dir`, and the bootstrap-invite exchange) are always available — they\nare how you configure everything else.\n"
4
+ "docs/mcp-tool-integrations.md": "# MCP tool integrations — the human \"why\" behind the capability report\n\nThis catalog is **explanatory prose only**. It exists so the `/install-bridge`\ncapability report can cite a human-readable \"why\" for each gate. It is **not** a\nsource of truth for gating: the server computes every `locked_tools` /\n`unlocked_tools` membership decision itself and the agent must never recompute a\ntool's dependencies from this document.\n\n**Authoritative source of gating.** The enforced rules — which tools are blocked,\nwhich are degraded, and what each requires — live in\n`api/library/vcs/vcs_route_operations.py`:\n\n- `VCS_ROUTE_REQUIREMENTS` — routes that **BLOCK** (are unavailable) without a\n VCS connection.\n- `VCS_ROUTE_WARNINGS` — routes that **DEGRADE** (stay usable, but without\n codebase context) without a VCS connection.\n- `NEVER_GATED_ROUTE_KEYS` — routes that are never gated on any integration.\n- `INDEX_REQUIRED_ROUTE_KEYS`, `INDEX_REQUIRED_BRAINSTORM_MODES`,\n `CREATE_DOC_CODEBASE_CONTEXT_DOC_TYPES`, `CREATE_DOC_WARN_DOC_TYPES` — the\n conditional \"requires a successful code index\" dimension.\n- The resolver helpers `get_required_vcs_operation()`, `get_warn_vcs_operation()`,\n and `requires_successful_index()` are the authoritative functions that decide a\n case. The capability report is derived from these; this catalog explains them.\n\n## Reading the capability report\n\nEach tool entry the server returns has the exact shape\n`{tool, effect, missing, semantics}`:\n\n- **`effect`**\n - **`BLOCK`** — the tool is **unavailable** until every listed dependency is\n met. It will refuse to run without them.\n - **`DEGRADE`** — the tool is **usable right now**, but **without codebase\n context** (it cannot ground its output in your repository). Connecting the\n listed dependency upgrades it from \"works blind\" to \"works with full context\".\n A `DEGRADE` tool is never \"failed\".\n- **`missing`** — the server-computed dependency identifiers still needed:\n integration ids such as `github_app` / `vcs_access_token`, and the synthetic\n `code_index` (a successful repository index).\n- **`semantics`**\n - **`all_of`** — every id in `missing` is required.\n - **`any_of`** — the VCS-provider candidates in `missing` are alternatives:\n **either** `github_app` **or** `vcs_access_token` satisfies the VCS\n requirement (this is the \"provider unknown\" case). When `code_index` also\n appears, it remains separately required — `semantics` describes only the VCS\n provider candidates, and a code index is always mandatory in addition.\n\nThe three readiness dimensions `configured` / `learned` / `indexed` are reported\nindependently. `indexed` may be `true`, `false`, or `null` — a `null` means the\nindex status could not be confirmed and must **not** be read as \"indexed\".\n\n## The integrations\n\n| Integration id | What it is | What it unlocks |\n| --- | --- | --- |\n| `jira` | Jira API access | Ticket reads/writes, estimation and review automations, status transitions. |\n| `github_app` | GitHub App installation | Pull requests, code review, and private-repo parsing on GitHub projects. |\n| `vcs_access_token` | VCS access token | Pull requests, code review, and private-repo parsing on Bitbucket projects. |\n| `vcs_webhook` | VCS webhook secret | Merge webhooks and CI follow-up triggers. |\n| `code_index` | A successful repository index | Codebase-grounded planning, architecture, reimplementation, and technical/discovery brainstorms. Produced by `/parse-repository`. |\n\nA project's `github_app` **or** `vcs_access_token` provides the VCS connection;\nwhich one applies depends on the project's version-control system. When the\nproject's provider is unknown, either credential satisfies the requirement — the\nreport expresses that as `semantics: any_of`.\n\n## The gates, by capability\n\n### Pull requests and CI (BLOCK on VCS)\n\nTools like `create_pull_request`, `resolve_ci_checks`, `poll_ci_checks`, and\n`materialize_fresh_base` are **unavailable** (`BLOCK`) until a VCS connection is\nconfigured. They act directly on the version-control host, so without a\nconnection there is nothing for them to talk to.\n\n### Repository indexing and maps (BLOCK on VCS)\n\n`parse_repository` and `regenerate_directory_map` need a VCS connection to read\nthe repository. They **BLOCK** until VCS is connected.\n\n### Codebase-grounded generation (BLOCK on VCS **and** a code index)\n\nPlanning and architecture tools — `generate_plan_direct`,\n`generate_architecture_direct`, `request_reimplement_context`,\n`code_writer_generate_plan`, `code_writer_generate_architecture`, and\n`create_doc` for **TDD** / **architecture** documents — ground their output in\nyour indexed codebase. They **BLOCK** until BOTH a VCS connection AND a\nsuccessful code index exist (`all_of`, with `code_index` in `missing`).\n\n### Council (BLOCK on a code index, mode-dependent)\n\n`request_council` in **technical** or **discovery** mode searches your indexed\ncodebase, so it **BLOCK**s on `code_index`. **Design**-mode brainstorming never\nqueries the index and is never gated.\n\n### Document generation that DEGRADEs (usable without codebase context)\n\nTools like `generate_prd_direct`, `generate_fsd_direct`,\n`code_writer_generate_fsd`, `generate_clarifying_questions_direct`,\n`generate_ticket_critique_direct`, `generate_ticket_review_direct`, and\n`create_doc` for **PRD** / **FSD** documents **DEGRADE** rather than block: they\nrun today from the ticket alone, and connecting VCS simply lets them ground their\noutput in your codebase. They always appear under \"Tools you can use now\", with a\nreduced-context caveat when the VCS connection is missing.\n\n### Never gated\n\nSetup and bootstrap tools (`ping`, `config_field`, `get_install_manifest`,\n`apply_install_manifest`, `get_my_role`, `persist_routing_credential`,\n`get_docs_dir`, and the bootstrap-invite exchange) are always available — they\nare how you configure everything else.\n",
5
+ "docs/install/sfcc-integration.md": "# Installing the SFCC Integration (OCAPI)\n\nBridge's Salesforce B2C Commerce (SFCC) tools give an AI coding agent read access to\na sandbox's object model, custom object definitions, and site preferences — plus a\nsmall set of sandbox-only writes — through the **OCAPI Data API**. This guide covers\nsetting up the OCAPI client that those tools authenticate against.\n\n> **Sandbox / local development only.** This integration is intended for a **developer\n> sandbox**. The tools reject non-sandbox instances, and the permissions grant below is\n> deliberately broad (all methods, all resources) — appropriate for a throwaway dev\n> sandbox, **never** for staging or production. Do not configure this grant on any\n> instance that holds real data. Credentials stay local (in `dw.json` or `SFCC_*` env\n> vars) and are never sent to Bridge.\n\nFor the full per-tool list and what each SFCC tool depends on, see\n[MCP Tool Integration Dependencies](./mcp-tool-integrations.md). For the tool reference\nand the `BRIDGE_MCP_PROFILE` gating, see the SFCC section of the\n[package README](../../README.md).\n\n## Prerequisites\n\n- A running SFCC **developer sandbox** and its hostname\n (e.g. `zzzz-001.sandbox.us01.dx.commercecloud.salesforce.com`).\n- An **Account Manager API client** — a `client-id` and `client-secret`. This is the\n OCAPI client the tools use to obtain an OAuth token. Create one in Account Manager\n (**API Client** → *Add API Client*) if you don't already have it, and note its\n `client_id`.\n- Business Manager access to the sandbox with permission to edit **Open Commerce API\n Settings**.\n\n## 1. Grant the OCAPI client access in Business Manager\n\nIn Business Manager for the sandbox:\n\n**Administration → Site Development → Open Commerce API Settings → Data API** tab.\n\nAdd the following client entry to the `clients` array of the Data API settings, then\n**Save**. This grants the client full read/write access to every Data API resource —\nacceptable only on a developer sandbox:\n\n```json\n{\n \"client_id\": \"<your-client-id-here>\",\n \"resources\": [\n {\n \"methods\": [\"get\", \"post\", \"put\", \"patch\", \"delete\"],\n \"read_attributes\": \"(**)\",\n \"write_attributes\": \"(**)\",\n \"resource_id\": \"/**\"\n }\n ]\n}\n```\n\nNotes:\n\n- The `client_id` **must match** the Account Manager API client whose credentials you\n put in `dw.json` / `SFCC_*` below. Replace the value above with your own client id if\n it differs.\n- If the Data API settings are empty, wrap the entry in the standard settings envelope:\n\n ```json\n {\n \"_v\": \"23.2\",\n \"clients\": [\n {\n \"client_id\": \"<your-client-id-here>\",\n \"resources\": [\n {\n \"methods\": [\"get\", \"post\", \"put\", \"patch\", \"delete\"],\n \"read_attributes\": \"(**)\",\n \"write_attributes\": \"(**)\",\n \"resource_id\": \"/**\"\n }\n ]\n }\n ]\n }\n ```\n\n- `check_permissions` (below) prints a ready-to-paste grant JSON on a 401/403, so you can\n also let the tool tell you exactly what to add.\n\n## 2. Provide credentials locally\n\nCreate a `dw.json` in your project root (auto-added to git exclude — never commit it):\n\n```json\n{\n \"hostname\": \"zzzz-001.sandbox.us01.dx.commercecloud.salesforce.com\",\n \"client-id\": \"<your-client-id-here>\",\n \"client-secret\": \"<account-manager-client-secret>\"\n}\n```\n\nAccepted key spellings: `hostname`/`host`, `client-id`/`clientId`/`client_id`,\n`client-secret`/`clientSecret`/`client_secret`. Prefer a single config — a multi-entry\n`configs[]` array forces an explicit `instance` on every call. Alternatively, export\n`SFCC_HOSTNAME` / `SFCC_CLIENT_ID` / `SFCC_CLIENT_SECRET` in the MCP server environment.\n\n## 3. Set the repo `version` config field\n\nSet the repo's `version` config to your SFCC project type — one of\n`sfra | pwakit | sitegenesis | storefrontnext | hybrid`. The call-time gate reads this;\na non-SFCC value blocks every SFCC tool except `sfcc_setup_status`. Set it via your\nnormal config path, the `config_field` MCP tool (operation `update`, field `version`),\nor the `/teach-bridge` skill.\n\n## 4. Enable the SFCC tools\n\nThe two diagnostic tools (`sfcc_setup_status`, `check_permissions`) are always\nregistered. The read tools, write tools, and `sfcc_log_query` are gated behind the\n`sfcc` profile. Add `sfcc` to `BRIDGE_MCP_PROFILE` in the MCP server `env` block (it is\ncomma-separated; `full` also works), then **restart the MCP client**:\n\n```json\n\"env\": { \"BRIDGE_MCP_PROFILE\": \"sfcc\" }\n```\n\n## 5. Verify\n\nAsk your agent to run:\n\n1. `sfcc_setup_status` — expect all prerequisite checks ✓ (Bridge API key, repo name,\n `version` config, `dw.json` presence/uniqueness, AM/OCAPI token acquisition).\n2. `check_permissions` — probes OCAPI via `GET /system_object_definitions`. A 200 (with\n the OCAPI version) confirms the grant. On 401/403 it prints the exact grant JSON to\n paste back in step 1.\n\nRestart the MCP client after any credential, grant, or env change — a running session\ndoes not pick them up.\n\n## Notes\n\n- **WebDAV logs are separate.** `sfcc_log_query` authenticates with a Business Manager\n username + a 40-character **WebDAV access key** over HTTP Basic auth — *not* the OCAPI\n OAuth token configured here. `sfcc_setup_status` reports OCAPI (step 5) and WebDAV\n (step 6) independently; one can be green while the other is not.\n- **Writes are sandbox-only.** The write tools (attribute/preference create/update) target\n a developer sandbox and echo a paste-ready grant JSON on a 403.\n"
5
6
  };