@bridge_gpt/mcp-server 0.2.45 → 0.2.48
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 +103 -86
- package/build/agent-capabilities/default-deps.js +2 -2
- package/build/agent-launchers/claude.js +10 -19
- package/build/agent-launchers/cursor.js +4 -12
- package/build/agent-launchers/prompt.js +117 -0
- package/build/agents.generated.js +1 -1
- package/build/commands.generated.js +16 -22
- package/build/conduct-epic/bridge-client.js +73 -0
- package/build/conduct-epic/cli.js +152 -6
- package/build/conductor/cli.js +6 -7
- package/build/conductor/doctor.js +13 -116
- package/build/conductor/tools.js +18 -349
- package/build/conductor-bin.js +6 -30
- package/build/docs.generated.js +2 -2
- package/build/executor/deps.js +1 -0
- package/build/executor/service-lifecycle.js +6 -6
- package/build/executor/service-unit.js +13 -16
- package/build/index.js +253 -759
- package/build/init.js +15 -17
- package/build/install-doctor.js +1 -1
- package/build/learn-tool-gating.js +283 -0
- package/build/mcp-profile.js +13 -3
- package/build/mcp-server-invocation.js +14 -0
- package/build/pipelines.generated.js +20 -140
- package/build/platform-escaping.js +72 -0
- package/build/readme.generated.js +1 -1
- package/build/review-tickets.js +1 -1
- package/build/run-unit-tests-launcher.js +0 -1
- package/build/sfcc/register.js +41 -31
- package/build/sfcc/registration-inventory.js +44 -20
- package/build/start-tickets-conductor.js +2 -2
- package/build/start-tickets.js +8 -38
- package/build/tool-surface-gating.js +9 -3
- package/build/update-status.js +15 -0
- package/build/version.generated.js +2 -2
- package/docs/CONDUCTOR.md +10 -12
- package/docs/install/mcp-tool-integrations.md +9 -55
- package/package.json +2 -2
- package/pipelines/idea-to-ticket.json +2 -2
- package/pipelines/review-ticket.json +9 -8
- package/pipelines/check-ci-ticket.json +0 -36
- package/pipelines/pr-ticket.json +0 -24
package/build/init.js
CHANGED
|
@@ -882,7 +882,7 @@ Each pipeline file must be a JSON object with these fields:
|
|
|
882
882
|
| Field | Type | Required | Description |
|
|
883
883
|
|---------------|------------|----------|------------------------------------------|
|
|
884
884
|
| \`name\` | string | yes | Display name of the pipeline |
|
|
885
|
-
| \`description\` | string | no | Short description
|
|
885
|
+
| \`description\` | string | no | Short description of the pipeline |
|
|
886
886
|
| \`variables\` | string[] | no | Variable names for substitution |
|
|
887
887
|
| \`steps\` | Step[] | yes | Ordered list of steps to execute |
|
|
888
888
|
|
|
@@ -934,29 +934,27 @@ automatically provided by the server.
|
|
|
934
934
|
|
|
935
935
|
## Executing Pipelines
|
|
936
936
|
|
|
937
|
-
Pipelines defined here
|
|
938
|
-
|
|
937
|
+
Pipelines defined here are executed by the calling agent. Call the
|
|
938
|
+
\`get_pipeline_recipe\` MCP tool with the pipeline name and any variables; it
|
|
939
|
+
returns a fully resolved recipe:
|
|
939
940
|
|
|
940
|
-
- \`
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
described by \`instruction\`, then call \`resume_pipeline\` with the
|
|
944
|
-
\`pipeline_run_id\` and the resulting string as \`agent_result\`.
|
|
945
|
-
- \`failed\` — terminal failure; \`error_code\` is one of \`VALIDATION\`,
|
|
946
|
-
\`NOT_FOUND\`, \`EXPIRED\`, \`REPO_MISMATCH\`, or \`TOOL_ERROR\`.
|
|
941
|
+
- \`steps\` — the ordered step array, with variables substituted and every
|
|
942
|
+
\`instruction_file\` reference already inlined as \`instruction\` text.
|
|
943
|
+
- \`total_steps\`, \`auto_approve\`, and \`execution_mode\`.
|
|
947
944
|
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
\`
|
|
945
|
+
\`execution_mode\` is \`inline\`: there is no server-side orchestrator,
|
|
946
|
+
and no run to resume. The agent executes each step itself in order — calling the named
|
|
947
|
+
tool for an \`mcp_call\` step, following the instruction text for an
|
|
948
|
+
\`agent_task\` step — and honours each step's \`on_error\` and
|
|
949
|
+
\`requires_approval\` fields as it goes.
|
|
952
950
|
|
|
953
951
|
## \`agent_task\` Instruction Files
|
|
954
952
|
|
|
955
953
|
When you reference an instruction file (\`instruction_file: "…"\`), the file
|
|
956
954
|
markdown MUST end with a terminal \`## Return\` H2 section describing what the
|
|
957
|
-
agent should
|
|
958
|
-
|
|
959
|
-
|
|
955
|
+
agent should carry forward from that step. The return value is a string — do
|
|
956
|
+
NOT ask the agent to wrap it in JSON unless the instruction body explicitly
|
|
957
|
+
says to serialize structured output.
|
|
960
958
|
`;
|
|
961
959
|
const exampleContent = JSON.stringify({
|
|
962
960
|
name: "Example Pipeline",
|
package/build/install-doctor.js
CHANGED
|
@@ -465,7 +465,7 @@ export async function collectInstallStatusChecks(deps) {
|
|
|
465
465
|
label: "Repository indexing",
|
|
466
466
|
status: "INFO",
|
|
467
467
|
detail: "no parse job currently running — if this repository has never been indexed, " +
|
|
468
|
-
"queue one with /parse-repository (monitor with
|
|
468
|
+
"queue one with /parse-repository (monitor with parse_repository action \"status\").",
|
|
469
469
|
});
|
|
470
470
|
}
|
|
471
471
|
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Learn/onboarding tool gating — SDK enabled-state, session lifetime (BAPI-912,
|
|
3
|
+
* decision D20).
|
|
4
|
+
*
|
|
5
|
+
* ---------------------------------------------------------------------------
|
|
6
|
+
* Which gating mechanism this is, and which one it is NOT
|
|
7
|
+
* ---------------------------------------------------------------------------
|
|
8
|
+
*
|
|
9
|
+
* Bridge has two independent mechanisms that can keep a tool out of a client's
|
|
10
|
+
* `tools/list`, and conflating them is the specific mistake this comment exists
|
|
11
|
+
* to prevent:
|
|
12
|
+
*
|
|
13
|
+
* **Mechanism A — backend capability gating (BAPI-641).**
|
|
14
|
+
* `tool-surface-gating.ts`. A fail-open, purely *subtractive* filter over the
|
|
15
|
+
* SDK's own `tools/list` result, driven by a backend capability probe. That
|
|
16
|
+
* module documents a hard **NEVER `.disable()`** invariant, because a probe
|
|
17
|
+
* result must never mutate registration state. This module does not import
|
|
18
|
+
* from, call into, or modify it.
|
|
19
|
+
*
|
|
20
|
+
* **Mechanism B — SDK enabled state (used here).**
|
|
21
|
+
* The `registerTool` wrapper in `index.ts` returns the SDK registration
|
|
22
|
+
* handle and keeps a closure `active` flag in sync with `.enable()` /
|
|
23
|
+
* `.disable()`, so a disabled tool is blocked for BOTH `tools/list` and
|
|
24
|
+
* in-process `TOOL_HANDLERS` dispatch (returning the `TOOL_DISABLED`/503
|
|
25
|
+
* envelope). `poll_ci_checks` has used exactly this since BAPI-338.
|
|
26
|
+
*
|
|
27
|
+
* Mechanism A's never-`.disable()` invariant governs Mechanism A only — it
|
|
28
|
+
* constrains the capability-gating module, not `index.ts`. Reading it as a
|
|
29
|
+
* blanket prohibition would make this feature look unimplementable.
|
|
30
|
+
*
|
|
31
|
+
* The two **compose as `SDK-enabled ∩ not-capability-hidden`**. This module
|
|
32
|
+
* moves only the first term, so enabling a tool whose repository lacks the
|
|
33
|
+
* required integration correctly leaves it absent from `tools/list`. That is a
|
|
34
|
+
* supported outcome, not a bug.
|
|
35
|
+
*
|
|
36
|
+
* ---------------------------------------------------------------------------
|
|
37
|
+
* The state machine
|
|
38
|
+
* ---------------------------------------------------------------------------
|
|
39
|
+
*
|
|
40
|
+
* The three learn/onboarding tools register normally (so registration order and
|
|
41
|
+
* the ordered `EXPECTED_TOOL_NAMES` pin are untouched) and are then disabled at
|
|
42
|
+
* startup. They are enabled **all three together, exactly once**, by the first
|
|
43
|
+
* of three qualifying triggers:
|
|
44
|
+
*
|
|
45
|
+
* 1. a *successful* `get_install_manifest` response,
|
|
46
|
+
* 2. `get_pipeline_recipe` successfully resolving `learn-repository`,
|
|
47
|
+
* 3. the post-initialize client fallback, for any client not on the
|
|
48
|
+
* known-good list.
|
|
49
|
+
*
|
|
50
|
+
* Trigger 3 is why this is safe for clients that ignore a mid-session
|
|
51
|
+
* `notifications/tools/list_changed`: they simply receive the trio in their very
|
|
52
|
+
* first `tools/list`. The direction is deliberately **fail-open** — an
|
|
53
|
+
* unrecognized client gets the tools. Enabling only un-hides tools the resolved
|
|
54
|
+
* profile already registered; it never bypasses `ACTIVE_GROUPS`, the capability
|
|
55
|
+
* gate, or backend authorization.
|
|
56
|
+
*
|
|
57
|
+
* Nothing here writes a notification to the transport. The SDK's `update()` path
|
|
58
|
+
* (`mcp.js`) fires `sendToolListChanged()` itself and no-ops it while
|
|
59
|
+
* unconnected, so the startup `.disable()` emits nothing and a post-connect
|
|
60
|
+
* `.enable()` notifies automatically.
|
|
61
|
+
*/
|
|
62
|
+
/**
|
|
63
|
+
* The three gated tools, as a fixed list.
|
|
64
|
+
*
|
|
65
|
+
* This is the user-visible identity of the gated set and is asserted verbatim by
|
|
66
|
+
* tests; it is deliberately a literal rather than something derived from the
|
|
67
|
+
* registration order in `index.ts`.
|
|
68
|
+
*/
|
|
69
|
+
export const LEARN_GATED_TOOL_NAMES = [
|
|
70
|
+
"apply_install_manifest",
|
|
71
|
+
"invite_member",
|
|
72
|
+
"regenerate_directory_map",
|
|
73
|
+
];
|
|
74
|
+
/** The pipeline name whose successful resolution is a qualifying trigger. */
|
|
75
|
+
export const LEARN_REPOSITORY_PIPELINE_NAME = "learn-repository";
|
|
76
|
+
/**
|
|
77
|
+
* Clients empirically verified to honor a mid-session
|
|
78
|
+
* `notifications/tools/list_changed` — the only clients for which it is safe to
|
|
79
|
+
* hide the trio at startup.
|
|
80
|
+
*
|
|
81
|
+
* `claude-code` is an **observed MCP handshake identity**, captured on
|
|
82
|
+
* 2026-08-26 from a live Claude Code 2.1.246 session against a throwaway stdio
|
|
83
|
+
* MCP server, which recorded:
|
|
84
|
+
*
|
|
85
|
+
* "clientInfo": { "name": "claude-code", "title": "Claude Code",
|
|
86
|
+
* "version": "2.1.246", ... }
|
|
87
|
+
*
|
|
88
|
+
* The same capture confirmed the behavior that matters: after the server emitted
|
|
89
|
+
* `notifications/tools/list_changed`, Claude Code re-issued `tools/list` and
|
|
90
|
+
* called the newly revealed tool inside the same session.
|
|
91
|
+
*
|
|
92
|
+
* It happens to equal the `MCP_HOST_TARGETS` id `"claude-code"`, but it is NOT
|
|
93
|
+
* copied from it — those ids are host-config target names, a different
|
|
94
|
+
* namespace. This constant is pinned to the observed handshake value.
|
|
95
|
+
*
|
|
96
|
+
* Deliberately NOT on this list:
|
|
97
|
+
* - **Cursor** — reported to ignore mid-session list changes; could not be
|
|
98
|
+
* verified locally (`cursor-agent` requires authentication). Fail-open
|
|
99
|
+
* covers it: Cursor receives the trio at startup.
|
|
100
|
+
* - **GitHub Copilot CLI** (`github-copilot-developer`) — *passed* the same
|
|
101
|
+
* probe on 2026-08-26 (CLI 1.0.71 re-listed and called the revealed tool).
|
|
102
|
+
* Left off pending owner sign-off, since this list ships with Claude Code as
|
|
103
|
+
* its sole initial entry; promoting it is a one-line, zero-risk follow-up.
|
|
104
|
+
* - **VS Code / Copilot Chat extension** — a different product from the CLI
|
|
105
|
+
* above, with its own handshake identity. Not probed; not claimed.
|
|
106
|
+
*/
|
|
107
|
+
export const KNOWN_LIST_CHANGED_CLIENT_NAMES = new Set([
|
|
108
|
+
"claude-code",
|
|
109
|
+
]);
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Client identity
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
/**
|
|
114
|
+
* Normalize a client-supplied name for *matching*: require a string, trim it,
|
|
115
|
+
* lowercase it. Anything else — absent, null, number, object, array, blank — is
|
|
116
|
+
* unrecognized, which is the fail-open direction.
|
|
117
|
+
*
|
|
118
|
+
* The result of this function is only ever compared for exact membership. It is
|
|
119
|
+
* never logged; `sanitizeClientName` owns the display value, and the two must
|
|
120
|
+
* not be interchanged.
|
|
121
|
+
*/
|
|
122
|
+
export function normalizeClientName(raw) {
|
|
123
|
+
if (typeof raw !== "string")
|
|
124
|
+
return null;
|
|
125
|
+
const normalized = raw.trim().toLowerCase();
|
|
126
|
+
return normalized === "" ? null : normalized;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Exact, trimmed, case-insensitive token membership.
|
|
130
|
+
*
|
|
131
|
+
* Deliberately NOT substring, prefix, or product-family matching:
|
|
132
|
+
* `clientInfo.name` is client-controlled, and a substring test would let
|
|
133
|
+
* `"not-claude-code"` or `"claude-code-preview"` keep tools hidden from a client
|
|
134
|
+
* that may not honor `tools/list_changed` at all.
|
|
135
|
+
*/
|
|
136
|
+
export function isKnownListChangedClient(raw) {
|
|
137
|
+
const normalized = normalizeClientName(raw);
|
|
138
|
+
return normalized !== null && KNOWN_LIST_CHANGED_CLIENT_NAMES.has(normalized);
|
|
139
|
+
}
|
|
140
|
+
/** Longest client name rendered into a log line. */
|
|
141
|
+
export const MAX_LOGGED_CLIENT_NAME_LENGTH = 64;
|
|
142
|
+
/** Substituted for absent, non-string, blank, or fully-stripped input. */
|
|
143
|
+
export const UNKNOWN_CLIENT_NAME = "unknown";
|
|
144
|
+
/**
|
|
145
|
+
* Bound and restrict an untrusted `clientInfo.name` for logging.
|
|
146
|
+
*
|
|
147
|
+
* `clientInfo.name` is attacker-controlled text that lands on stderr, so this is
|
|
148
|
+
* a log-forging control, not cosmetics: newlines, ANSI escapes, quotes, and
|
|
149
|
+
* shell punctuation all fall outside the safe class and become `_`, so no client
|
|
150
|
+
* can inject a second line that reads like a genuine record. The result is
|
|
151
|
+
* single-line, length-bounded, and drawn only from `[A-Za-z0-9._/-]`.
|
|
152
|
+
*
|
|
153
|
+
* The same posture `mcp-profile.ts` states for profile tokens: raw input is
|
|
154
|
+
* never echoed.
|
|
155
|
+
*/
|
|
156
|
+
export function sanitizeClientName(raw) {
|
|
157
|
+
if (typeof raw !== "string")
|
|
158
|
+
return UNKNOWN_CLIENT_NAME;
|
|
159
|
+
const cleaned = raw
|
|
160
|
+
.trim()
|
|
161
|
+
.slice(0, MAX_LOGGED_CLIENT_NAME_LENGTH)
|
|
162
|
+
.replace(/[^A-Za-z0-9._/-]/g, "_");
|
|
163
|
+
// A value made entirely of unsafe characters degrades to underscores, which
|
|
164
|
+
// reads as a real name; report it as unknown instead.
|
|
165
|
+
return cleaned === "" || /^_+$/.test(cleaned) ? UNKNOWN_CLIENT_NAME : cleaned;
|
|
166
|
+
}
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Trigger predicates
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
/**
|
|
171
|
+
* Extract the text payload from either a raw string or an MCP `{content:[…]}`
|
|
172
|
+
* response envelope. Returns null when there is no text content.
|
|
173
|
+
*/
|
|
174
|
+
function extractResponseText(candidate) {
|
|
175
|
+
if (typeof candidate === "string")
|
|
176
|
+
return candidate;
|
|
177
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
|
|
178
|
+
return null;
|
|
179
|
+
const content = candidate.content;
|
|
180
|
+
if (!Array.isArray(content))
|
|
181
|
+
return null;
|
|
182
|
+
for (const item of content) {
|
|
183
|
+
if (item && typeof item === "object" && !Array.isArray(item)) {
|
|
184
|
+
const text = item.text;
|
|
185
|
+
if (typeof text === "string")
|
|
186
|
+
return text;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* `get_install_manifest` appends a save note (`\n\n---\nSaved to …`) to its JSON
|
|
193
|
+
* body whenever `save_locally` is not explicitly false — which is the default,
|
|
194
|
+
* and therefore the common case. Stripping it here is what keeps the predicate
|
|
195
|
+
* from silently rejecting every ordinary successful manifest.
|
|
196
|
+
*/
|
|
197
|
+
function stripSaveNote(text) {
|
|
198
|
+
const marker = text.indexOf("\n\n---\n");
|
|
199
|
+
return marker === -1 ? text : text.slice(0, marker);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* True only for a genuinely successful install manifest.
|
|
203
|
+
*
|
|
204
|
+
* "Successful" is defined against the real response contract rather than
|
|
205
|
+
* "did not throw": the body must parse as a JSON object (not null, not an
|
|
206
|
+
* array), must not be an error envelope, and must carry a nonblank
|
|
207
|
+
* `snapshot_token` — the signed token `apply_install_manifest` requires, and the
|
|
208
|
+
* field whose presence distinguishes a real manifest from any other JSON the
|
|
209
|
+
* endpoint might return.
|
|
210
|
+
*
|
|
211
|
+
* Every parse failure is swallowed and reported as false, leaving gate state
|
|
212
|
+
* unchanged. A non-JSON body, an error response, `{}`, a blank token, or a
|
|
213
|
+
* whitespace-only token must never enable the set.
|
|
214
|
+
*/
|
|
215
|
+
export function isSuccessfulInstallManifestResponse(candidate) {
|
|
216
|
+
const text = extractResponseText(candidate);
|
|
217
|
+
if (text === null)
|
|
218
|
+
return false;
|
|
219
|
+
let parsed;
|
|
220
|
+
try {
|
|
221
|
+
parsed = JSON.parse(stripSaveNote(text));
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
227
|
+
return false;
|
|
228
|
+
const body = parsed;
|
|
229
|
+
// An explicit error envelope (handleResponse's failure shape, or the
|
|
230
|
+
// TOOL_DISABLED envelope) is never a success, even if it somehow carried a
|
|
231
|
+
// token-shaped field.
|
|
232
|
+
if (typeof body.error === "string")
|
|
233
|
+
return false;
|
|
234
|
+
const token = body.snapshot_token;
|
|
235
|
+
return typeof token === "string" && token.trim() !== "";
|
|
236
|
+
}
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
// The one-shot gate
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
/**
|
|
241
|
+
* Build the learn-tool gate over three SDK handles.
|
|
242
|
+
*
|
|
243
|
+
* The gate owns one boolean for the whole set: the three tools are enabled
|
|
244
|
+
* atomically from the server's perspective, so no caller can observe a partially
|
|
245
|
+
* revealed trio, and there is no per-tool dispatch registry to drift.
|
|
246
|
+
*/
|
|
247
|
+
export function createLearnToolGate(deps) {
|
|
248
|
+
const sink = deps.sink ?? ((message) => console.error(message));
|
|
249
|
+
let enabled = false;
|
|
250
|
+
let cause = null;
|
|
251
|
+
const ordered = LEARN_GATED_TOOL_NAMES.map((name) => deps.handles[name]);
|
|
252
|
+
return {
|
|
253
|
+
disableAll() {
|
|
254
|
+
for (const handle of ordered)
|
|
255
|
+
handle.disable();
|
|
256
|
+
},
|
|
257
|
+
enableOnce(trigger) {
|
|
258
|
+
if (enabled)
|
|
259
|
+
return;
|
|
260
|
+
// Set the transition state BEFORE touching the handles: `.enable()` drives
|
|
261
|
+
// the SDK update path, which can notify synchronously, and a re-entrant
|
|
262
|
+
// trigger must find the set already transitioned rather than enable twice.
|
|
263
|
+
enabled = true;
|
|
264
|
+
cause = trigger;
|
|
265
|
+
for (const handle of ordered)
|
|
266
|
+
handle.enable();
|
|
267
|
+
// Exactly one structured stderr line per gated-set enable. Follows the
|
|
268
|
+
// established key=value shape (`tool-surface gating: reason=… hidden=…`).
|
|
269
|
+
// The client name is the sanitized display value — never the raw
|
|
270
|
+
// handshake input, and never the normalized matching token.
|
|
271
|
+
sink(`learn tool gating: enabled=${ordered.length} ` +
|
|
272
|
+
`trigger=${trigger} ` +
|
|
273
|
+
`client=${sanitizeClientName(deps.getClientName())} ` +
|
|
274
|
+
`tools=[${LEARN_GATED_TOOL_NAMES.join(",")}]`);
|
|
275
|
+
},
|
|
276
|
+
isEnabled() {
|
|
277
|
+
return enabled;
|
|
278
|
+
},
|
|
279
|
+
enabledBy() {
|
|
280
|
+
return cause;
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
}
|
package/build/mcp-profile.js
CHANGED
|
@@ -5,13 +5,18 @@
|
|
|
5
5
|
* profile to a multi-valued group set. BRIDGE_MCP_PROFILE accepts a
|
|
6
6
|
* comma-separated list of group tokens; unknown tokens are silently dropped
|
|
7
7
|
* (security sentinel — raw input is never echoed). "core" is always implicitly
|
|
8
|
-
* included. "full" is an input shortcut that expands to
|
|
8
|
+
* included. "full" is an input shortcut that expands to all six known groups.
|
|
9
9
|
*
|
|
10
|
-
* Groups are resolved once at process startup and cannot change mid-session
|
|
10
|
+
* Groups are resolved once at process startup and cannot change mid-session:
|
|
11
|
+
* changing BRIDGE_MCP_PROFILE requires restarting the MCP server. This is
|
|
12
|
+
* startup-time registration filtering, which is a different mechanism from the
|
|
13
|
+
* session-lifetime SDK enabled-state gating applied to the learn/onboarding
|
|
14
|
+
* trio in index.ts (BAPI-912).
|
|
11
15
|
*/
|
|
12
16
|
const VALID_GROUPS = new Set([
|
|
13
17
|
"core",
|
|
14
18
|
"conductor",
|
|
19
|
+
"estimation",
|
|
15
20
|
"pipeline-authoring",
|
|
16
21
|
"sfcc",
|
|
17
22
|
"sfcc-write",
|
|
@@ -20,7 +25,8 @@ const VALID_GROUPS = new Set([
|
|
|
20
25
|
* Resolve the active MCP groups from a raw env-var string.
|
|
21
26
|
*
|
|
22
27
|
* - `undefined` or blank → `{"core"}` (fail-safe to smallest surface).
|
|
23
|
-
* - `"full"` (any case) → all
|
|
28
|
+
* - `"full"` (any case) → all six groups, `estimation` and `sfcc-write`
|
|
29
|
+
* included.
|
|
24
30
|
* - Comma-separated list: each token is trimmed, lowercased, and checked
|
|
25
31
|
* against VALID_GROUPS. Unknown tokens are dropped. Known tokens are unioned
|
|
26
32
|
* with the implicit `"core"` baseline.
|
|
@@ -38,9 +44,13 @@ export function resolveProfiles(raw) {
|
|
|
38
44
|
return baseline;
|
|
39
45
|
const tokens = trimmed.split(",").map((t) => t.trim().toLowerCase());
|
|
40
46
|
if (tokens.some((t) => t === "full")) {
|
|
47
|
+
// Deliberately a hard-coded complete expansion, not `VALID_GROUPS`: `full`
|
|
48
|
+
// must name every group so that adding a group is a conscious decision here
|
|
49
|
+
// rather than an implicit widening of an existing user's surface.
|
|
41
50
|
return new Set([
|
|
42
51
|
"core",
|
|
43
52
|
"conductor",
|
|
53
|
+
"estimation",
|
|
44
54
|
"pipeline-authoring",
|
|
45
55
|
"sfcc",
|
|
46
56
|
"sfcc-write",
|
|
@@ -30,6 +30,20 @@ export const MCP_SERVER_PACKAGE_NAME = MCP_PACKAGE_NAME;
|
|
|
30
30
|
* reintroduce the ETARGET/404 failure the absolute-path form exists to remove.
|
|
31
31
|
*/
|
|
32
32
|
export const DEFAULT_MCP_SERVER_NPM_CHANNEL_SPEC = `${MCP_PACKAGE_NAME}@latest`;
|
|
33
|
+
/**
|
|
34
|
+
* Detect a packaged-CLI entry path that lives inside a transient npx cache
|
|
35
|
+
* directory (`~/.npm/_npx/<hash>/…` on POSIX, `…\npm-cache\_npx\<hash>\…` on
|
|
36
|
+
* Windows). Such an entry point may be garbage-collected when the npx cache is
|
|
37
|
+
* pruned, so anything that bakes it into durable state (a generated service
|
|
38
|
+
* unit, a registered schedule) would later fail with ENOENT on
|
|
39
|
+
* `node <gone-path>`. Matched on the `_npx` path *segment* so detection is
|
|
40
|
+
* independent of the surrounding cache root and works for either separator.
|
|
41
|
+
*/
|
|
42
|
+
export function isTransientNpxEntryPath(entryPath) {
|
|
43
|
+
if (!entryPath)
|
|
44
|
+
return false;
|
|
45
|
+
return /[\\/]_npx[\\/]/.test(entryPath);
|
|
46
|
+
}
|
|
33
47
|
/**
|
|
34
48
|
* Build the concrete `{ command, args }` launch pair for a target + worktree from
|
|
35
49
|
* a structured invocation. Pure — no filesystem or process access. The
|