stageflow 0.12.0 → 0.13.0
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 +5 -1
- package/dist/agent/claudeAdapter.js +31 -1
- package/dist/agent/piAdapter.d.ts +6 -4
- package/dist/agent/piAdapter.js +66 -24
- package/dist/agent/piIsolatedMcp.d.ts +23 -0
- package/dist/agent/piIsolatedMcp.js +223 -0
- package/dist/agent/piIsolatedMcpProbe.d.ts +15 -0
- package/dist/agent/piIsolatedMcpProbe.js +292 -0
- package/dist/agent/port.d.ts +2 -0
- package/dist/agent/port.js +1 -1
- package/dist/config/loadPipeline.js +1 -0
- package/dist/config/loadStage.d.ts +1 -0
- package/dist/config/loadStage.js +69 -0
- package/dist/config/normalizePipelineStageEntry.js +7 -1
- package/dist/config/pipelineStageKeys.js +1 -0
- package/dist/config/resolveStageMcpServers.d.ts +35 -0
- package/dist/config/resolveStageMcpServers.js +207 -0
- package/dist/config/validateCatalog.d.ts +1 -1
- package/dist/config/validateCatalog.js +51 -0
- package/dist/mcp/readArtifact.d.ts +2 -0
- package/dist/mcp/readArtifact.js +25 -1
- package/dist/package-meta.d.ts +1 -1
- package/dist/package-meta.js +1 -1
- package/dist/runtime/stageAttemptBootstrap.js +38 -2
- package/dist/runtime/verifiedStageExecution.d.ts +2 -0
- package/dist/runtime/verifiedStageExecution.js +10 -2
- package/dist/server/http.js +23 -4
- package/dist/server/projectMcpRoutes.d.ts +6 -0
- package/dist/server/projectMcpRoutes.js +38 -0
- package/dist/types/pipeline.d.ts +2 -0
- package/dist/types/stage.d.ts +2 -1
- package/dist/ui/assets/index-Yzh42vhf.js +118 -0
- package/dist/ui/index.html +1 -1
- package/package.json +4 -1
- package/skills/stageflow-author/SKILL.md +20 -2
- package/skills/stageflow-author/references/catalog-mapping.md +63 -2
- package/skills/stageflow-author/references/stage-prompt-template.md +63 -4
- package/skills/stageflow-session-capture/references/catalog-authoring.md +4 -1
- package/dist/ui/assets/index-CXGz4nL7.js +0 -118
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { StageMcpError, resolveStageMcpServers, } from "../config/resolveStageMcpServers.js";
|
|
2
|
+
import * as piIsolatedMcp from "./piIsolatedMcp.js";
|
|
3
|
+
export const PROJECT_MCP_PROBE_TIMEOUT_MS = 30_000;
|
|
4
|
+
const MCP_STATUS_EVENT = "pi-mcp-adapter/status/v1";
|
|
5
|
+
function scrub(message, secrets) {
|
|
6
|
+
let out = message;
|
|
7
|
+
for (const secret of secrets) {
|
|
8
|
+
if (secret.length === 0)
|
|
9
|
+
continue;
|
|
10
|
+
out = out.split(secret).join("[redacted]");
|
|
11
|
+
}
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
14
|
+
function collectInterpolatedSecrets(entry) {
|
|
15
|
+
const secrets = [];
|
|
16
|
+
for (const field of ["env", "headers"]) {
|
|
17
|
+
const value = entry[field];
|
|
18
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
for (const item of Object.values(value)) {
|
|
22
|
+
if (typeof item === "string" && item.length > 0) {
|
|
23
|
+
secrets.push(item);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return secrets;
|
|
28
|
+
}
|
|
29
|
+
function catalogErrorResult(name, err) {
|
|
30
|
+
if (!(err instanceof StageMcpError))
|
|
31
|
+
return undefined;
|
|
32
|
+
if (err.code === "unresolved_var") {
|
|
33
|
+
return { name, status: "unresolved_var", error: err.message };
|
|
34
|
+
}
|
|
35
|
+
if (err.code === "missing_catalog") {
|
|
36
|
+
return { name, status: "missing_catalog" };
|
|
37
|
+
}
|
|
38
|
+
if (err.code === "invalid_config" ||
|
|
39
|
+
err.code === "reserved_name" ||
|
|
40
|
+
err.code === "unknown_server") {
|
|
41
|
+
return { name, status: "invalid_config" };
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
function asStatusServers(data) {
|
|
46
|
+
if (data === null || typeof data !== "object")
|
|
47
|
+
return undefined;
|
|
48
|
+
const servers = data.servers;
|
|
49
|
+
if (!Array.isArray(servers))
|
|
50
|
+
return undefined;
|
|
51
|
+
const parsed = [];
|
|
52
|
+
for (const entry of servers) {
|
|
53
|
+
if (entry === null || typeof entry !== "object")
|
|
54
|
+
continue;
|
|
55
|
+
const name = entry.name;
|
|
56
|
+
const status = entry.status;
|
|
57
|
+
if (typeof name === "string" && typeof status === "string") {
|
|
58
|
+
parsed.push({ name, status });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return parsed;
|
|
62
|
+
}
|
|
63
|
+
function mapRuntimeStatus(status) {
|
|
64
|
+
if (status === "connected" || status === "cached")
|
|
65
|
+
return "connected";
|
|
66
|
+
if (status === "needs-auth")
|
|
67
|
+
return "needs_auth";
|
|
68
|
+
if (status === "failed" || status === "disabled")
|
|
69
|
+
return "connect_failed";
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
function createProbeExtensionApi(eventBus) {
|
|
73
|
+
const handlers = new Map();
|
|
74
|
+
const api = {
|
|
75
|
+
events: eventBus,
|
|
76
|
+
on(event, handler) {
|
|
77
|
+
const list = handlers.get(event) ?? [];
|
|
78
|
+
list.push(handler);
|
|
79
|
+
handlers.set(event, list);
|
|
80
|
+
},
|
|
81
|
+
registerTool() { },
|
|
82
|
+
registerCommand() { },
|
|
83
|
+
registerShortcut() { },
|
|
84
|
+
registerFlag() { },
|
|
85
|
+
getFlag() {
|
|
86
|
+
return undefined;
|
|
87
|
+
},
|
|
88
|
+
registerMessageRenderer() { },
|
|
89
|
+
registerMarkdownTransformer() { },
|
|
90
|
+
registerEntryRenderer() { },
|
|
91
|
+
sendMessage() { },
|
|
92
|
+
sendUserMessage() { },
|
|
93
|
+
appendEntry() { },
|
|
94
|
+
setSessionName() { },
|
|
95
|
+
getSessionName() {
|
|
96
|
+
return undefined;
|
|
97
|
+
},
|
|
98
|
+
setLabel() { },
|
|
99
|
+
async exec() {
|
|
100
|
+
return { code: 0, stdout: "", stderr: "" };
|
|
101
|
+
},
|
|
102
|
+
getActiveTools() {
|
|
103
|
+
return [];
|
|
104
|
+
},
|
|
105
|
+
setActiveTools() { },
|
|
106
|
+
getAllTools() {
|
|
107
|
+
return [];
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
async function emit(event, ...args) {
|
|
111
|
+
for (const handler of handlers.get(event) ?? []) {
|
|
112
|
+
await handler(...args);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
api: api,
|
|
117
|
+
emitSessionStart(cwd) {
|
|
118
|
+
void emit("session_start", {}, { mode: "print", hasUI: false, cwd });
|
|
119
|
+
},
|
|
120
|
+
emitSessionShutdown() {
|
|
121
|
+
return emit("session_shutdown", {});
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function instantiateProbeFactories(attached, projectRoot) {
|
|
126
|
+
const factories = attached.extensionFactories ?? [];
|
|
127
|
+
const eventBus = attached.eventBus;
|
|
128
|
+
if (factories.length === 0 || eventBus === undefined) {
|
|
129
|
+
return async () => { };
|
|
130
|
+
}
|
|
131
|
+
const probeApi = createProbeExtensionApi(eventBus);
|
|
132
|
+
for (const ext of factories) {
|
|
133
|
+
const factory = typeof ext === "function" ? ext : ext.factory;
|
|
134
|
+
factory(probeApi.api);
|
|
135
|
+
}
|
|
136
|
+
probeApi.emitSessionStart(projectRoot);
|
|
137
|
+
return () => probeApi.emitSessionShutdown();
|
|
138
|
+
}
|
|
139
|
+
function mapConnectingError(err, aborted) {
|
|
140
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
141
|
+
if (aborted || /cancelled/i.test(message)) {
|
|
142
|
+
return { kind: "cancelled" };
|
|
143
|
+
}
|
|
144
|
+
if (/needs-auth/.test(message)) {
|
|
145
|
+
return { kind: "mapped", status: "needs_auth" };
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
kind: "mapped",
|
|
149
|
+
status: "connect_failed",
|
|
150
|
+
error: message.length > 0 ? message : undefined,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
async function waitForProbeStatus(eventBus, serverName, connecting, options) {
|
|
154
|
+
if (options.signal?.aborted) {
|
|
155
|
+
return { kind: "cancelled" };
|
|
156
|
+
}
|
|
157
|
+
return new Promise((resolve) => {
|
|
158
|
+
let settled = false;
|
|
159
|
+
let unsubscribe;
|
|
160
|
+
let timer;
|
|
161
|
+
const finish = (result) => {
|
|
162
|
+
if (settled)
|
|
163
|
+
return;
|
|
164
|
+
settled = true;
|
|
165
|
+
unsubscribe?.();
|
|
166
|
+
if (timer !== undefined)
|
|
167
|
+
clearTimeout(timer);
|
|
168
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
169
|
+
resolve(result);
|
|
170
|
+
};
|
|
171
|
+
const onAbort = () => finish({ kind: "cancelled" });
|
|
172
|
+
const consider = (data) => {
|
|
173
|
+
const servers = asStatusServers(data);
|
|
174
|
+
if (servers === undefined)
|
|
175
|
+
return;
|
|
176
|
+
const entry = servers.find((server) => server.name === serverName);
|
|
177
|
+
if (entry === undefined)
|
|
178
|
+
return;
|
|
179
|
+
const mapped = mapRuntimeStatus(entry.status);
|
|
180
|
+
if (mapped === undefined)
|
|
181
|
+
return;
|
|
182
|
+
if (mapped === "connect_failed") {
|
|
183
|
+
finish({
|
|
184
|
+
kind: "mapped",
|
|
185
|
+
status: "connect_failed",
|
|
186
|
+
error: `MCP server "${serverName}" failed to connect (status: ${entry.status})`,
|
|
187
|
+
});
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
finish({ kind: "mapped", status: mapped });
|
|
191
|
+
};
|
|
192
|
+
const handler = (data) => consider(data);
|
|
193
|
+
const maybeUnsub = eventBus.on(MCP_STATUS_EVENT, handler);
|
|
194
|
+
unsubscribe = typeof maybeUnsub === "function" ? maybeUnsub : undefined;
|
|
195
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
196
|
+
void (connecting ?? Promise.resolve()).then(() => finish({ kind: "mapped", status: "connected" }), (err) => finish(mapConnectingError(err, options.signal?.aborted === true)));
|
|
197
|
+
timer = setTimeout(() => {
|
|
198
|
+
finish({
|
|
199
|
+
kind: "mapped",
|
|
200
|
+
status: "connect_failed",
|
|
201
|
+
error: `MCP server "${serverName}" failed to connect (status: timed out)`,
|
|
202
|
+
});
|
|
203
|
+
}, options.timeoutMs);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
export async function probeProjectMcpServer(options) {
|
|
207
|
+
const name = options.name;
|
|
208
|
+
const timeoutMs = options.timeoutMs ?? PROJECT_MCP_PROBE_TIMEOUT_MS;
|
|
209
|
+
const env = options.env ?? process.env;
|
|
210
|
+
if (options.signal?.aborted) {
|
|
211
|
+
return { name, status: "cancelled" };
|
|
212
|
+
}
|
|
213
|
+
let snapshot;
|
|
214
|
+
try {
|
|
215
|
+
snapshot = await resolveStageMcpServers({
|
|
216
|
+
projectRoot: options.projectRoot,
|
|
217
|
+
allowlist: [name],
|
|
218
|
+
env,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
return (catalogErrorResult(name, err) ?? {
|
|
223
|
+
name,
|
|
224
|
+
status: "invalid_config",
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
const resolved = snapshot[name];
|
|
228
|
+
if (resolved === undefined) {
|
|
229
|
+
return { name, status: "invalid_config" };
|
|
230
|
+
}
|
|
231
|
+
const secrets = collectInterpolatedSecrets(resolved);
|
|
232
|
+
let attached;
|
|
233
|
+
let shutdownFactories;
|
|
234
|
+
const dispose = async () => {
|
|
235
|
+
if (attached?.connecting !== undefined) {
|
|
236
|
+
void attached.connecting.then(() => undefined, () => undefined);
|
|
237
|
+
}
|
|
238
|
+
attached?.cancel?.();
|
|
239
|
+
if (shutdownFactories !== undefined) {
|
|
240
|
+
const stop = shutdownFactories;
|
|
241
|
+
shutdownFactories = undefined;
|
|
242
|
+
await stop();
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
try {
|
|
246
|
+
if (options.signal?.aborted) {
|
|
247
|
+
return { name, status: "cancelled" };
|
|
248
|
+
}
|
|
249
|
+
attached = await piIsolatedMcp.attachIsolatedMcp(snapshot, {
|
|
250
|
+
timeoutMs,
|
|
251
|
+
lifecycle: "eager",
|
|
252
|
+
});
|
|
253
|
+
if (attached.eventBus === undefined) {
|
|
254
|
+
return { name, status: "invalid_config" };
|
|
255
|
+
}
|
|
256
|
+
shutdownFactories = instantiateProbeFactories(attached, options.projectRoot);
|
|
257
|
+
const waited = await waitForProbeStatus(attached.eventBus, name, attached.connecting, { timeoutMs, signal: options.signal });
|
|
258
|
+
if (waited.kind === "cancelled") {
|
|
259
|
+
return { name, status: "cancelled" };
|
|
260
|
+
}
|
|
261
|
+
if (waited.status === "connect_failed") {
|
|
262
|
+
return {
|
|
263
|
+
name,
|
|
264
|
+
status: "connect_failed",
|
|
265
|
+
...(waited.error !== undefined
|
|
266
|
+
? { error: scrub(waited.error, secrets) }
|
|
267
|
+
: {}),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
return { name, status: waited.status };
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
if (options.signal?.aborted) {
|
|
274
|
+
return { name, status: "cancelled" };
|
|
275
|
+
}
|
|
276
|
+
const catalog = catalogErrorResult(name, err);
|
|
277
|
+
if (catalog)
|
|
278
|
+
return catalog;
|
|
279
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
280
|
+
if (/cancelled/i.test(message)) {
|
|
281
|
+
return { name, status: "cancelled" };
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
name,
|
|
285
|
+
status: "connect_failed",
|
|
286
|
+
error: scrub(message, secrets),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
finally {
|
|
290
|
+
await dispose();
|
|
291
|
+
}
|
|
292
|
+
}
|
package/dist/agent/port.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { StageConfig } from "../types/stage.js";
|
|
|
5
5
|
import type { FeedbackLoopConfig } from "../types/pipeline.js";
|
|
6
6
|
import type { TaskFile } from "../types/task.js";
|
|
7
7
|
import type { StageActivityEvent } from "./activity.js";
|
|
8
|
+
import type { ResolvedMcpServers } from "../config/resolveStageMcpServers.js";
|
|
8
9
|
import type { StageRoots } from "../runtime/stageRoots.js";
|
|
9
10
|
import type { QaExchange } from "../hitl/qaTrail.js";
|
|
10
11
|
/** Opaque to runtime; adapters interpret. */
|
|
@@ -52,6 +53,7 @@ export type StageRunInput = {
|
|
|
52
53
|
priorEnvelopes?: StageEnvelope[];
|
|
53
54
|
priorEnvelopesByStage?: Record<string, TerminalEnvelope | TerminalEnvelope[]>;
|
|
54
55
|
timeoutMs?: number;
|
|
56
|
+
resolvedMcpServers?: ResolvedMcpServers;
|
|
55
57
|
resumeToken?: StageResumeToken;
|
|
56
58
|
/** Launch/session mode; omitted is treated as `"fresh"`. */
|
|
57
59
|
sessionMode?: StageSessionMode;
|
package/dist/agent/port.js
CHANGED
|
@@ -100,6 +100,7 @@ async function loadPipelineFromPath(pipelinePath) {
|
|
|
100
100
|
const stage = {
|
|
101
101
|
...stageOutcome.value,
|
|
102
102
|
...(entry.skill !== undefined ? { skill: entry.skill } : {}),
|
|
103
|
+
...(entry.mcp !== undefined ? { mcp: entry.mcp } : {}),
|
|
103
104
|
};
|
|
104
105
|
stages.push(stage);
|
|
105
106
|
stageSources[stageId] = { kind: "file", path: entry.body.absolutePath };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type StageConfig } from "../types/stage.js";
|
|
2
2
|
import { type LoadOutcome } from "./loadOutcome.js";
|
|
3
|
+
export declare function parseStageMcp(raw: unknown, label: string, stageId?: string): LoadOutcome<string[] | undefined>;
|
|
3
4
|
export declare function loadStageFromObjectOutcome(raw: Record<string, unknown>, ctx: {
|
|
4
5
|
entryId: string;
|
|
5
6
|
declaringPath: string;
|
package/dist/config/loadStage.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { parseAgentField } from "../agent/agentBackend.js";
|
|
2
|
+
import { STAGEFLOW_MCP_SERVER_NAME } from "../agent/claudeTools.js";
|
|
2
3
|
import { CLONE_ACTIONS } from "../types/forkChoice.js";
|
|
3
4
|
import { STAGE_GATE_KINDS, } from "../types/stage.js";
|
|
4
5
|
import { compilePayloadSchema } from "../envelope/payloadSchema.js";
|
|
@@ -91,6 +92,68 @@ function parseGateKinds(raw, label) {
|
|
|
91
92
|
}
|
|
92
93
|
return loadSuccess(kinds);
|
|
93
94
|
}
|
|
95
|
+
export function parseStageMcp(raw, label, stageId) {
|
|
96
|
+
if (raw === undefined)
|
|
97
|
+
return loadSuccess(undefined);
|
|
98
|
+
if (!Array.isArray(raw)) {
|
|
99
|
+
return loadFailure([
|
|
100
|
+
{
|
|
101
|
+
code: "stage.invalid_mcp",
|
|
102
|
+
message: `Invalid stage ${label}: mcp must be an array of server names`,
|
|
103
|
+
category: "stage",
|
|
104
|
+
stageId,
|
|
105
|
+
},
|
|
106
|
+
]);
|
|
107
|
+
}
|
|
108
|
+
const names = [];
|
|
109
|
+
const seen = new Set();
|
|
110
|
+
for (const item of raw) {
|
|
111
|
+
if (typeof item !== "string") {
|
|
112
|
+
return loadFailure([
|
|
113
|
+
{
|
|
114
|
+
code: "stage.invalid_mcp",
|
|
115
|
+
message: `Invalid stage ${label}: mcp entries must be strings`,
|
|
116
|
+
category: "stage",
|
|
117
|
+
stageId,
|
|
118
|
+
},
|
|
119
|
+
]);
|
|
120
|
+
}
|
|
121
|
+
const name = item.trim();
|
|
122
|
+
if (name === "") {
|
|
123
|
+
return loadFailure([
|
|
124
|
+
{
|
|
125
|
+
code: "stage.invalid_mcp",
|
|
126
|
+
message: `Invalid stage ${label}: mcp entries must be non-empty strings`,
|
|
127
|
+
category: "stage",
|
|
128
|
+
stageId,
|
|
129
|
+
},
|
|
130
|
+
]);
|
|
131
|
+
}
|
|
132
|
+
if (name === STAGEFLOW_MCP_SERVER_NAME) {
|
|
133
|
+
return loadFailure([
|
|
134
|
+
{
|
|
135
|
+
code: "stage.invalid_mcp",
|
|
136
|
+
message: `Invalid stage ${label}: mcp must not include reserved name "${STAGEFLOW_MCP_SERVER_NAME}"`,
|
|
137
|
+
category: "stage",
|
|
138
|
+
stageId,
|
|
139
|
+
},
|
|
140
|
+
]);
|
|
141
|
+
}
|
|
142
|
+
if (seen.has(name)) {
|
|
143
|
+
return loadFailure([
|
|
144
|
+
{
|
|
145
|
+
code: "stage.invalid_mcp",
|
|
146
|
+
message: `Invalid stage ${label}: mcp contains duplicate name "${name}"`,
|
|
147
|
+
category: "stage",
|
|
148
|
+
stageId,
|
|
149
|
+
},
|
|
150
|
+
]);
|
|
151
|
+
}
|
|
152
|
+
seen.add(name);
|
|
153
|
+
names.push(name);
|
|
154
|
+
}
|
|
155
|
+
return loadSuccess(names);
|
|
156
|
+
}
|
|
94
157
|
function parseStageFields(raw, label, entryId) {
|
|
95
158
|
if (typeof raw.system_prompt !== "string" ||
|
|
96
159
|
typeof raw.model !== "string") {
|
|
@@ -223,6 +286,12 @@ function parseStageFields(raw, label, entryId) {
|
|
|
223
286
|
}
|
|
224
287
|
stage.skill = raw.skill.trim();
|
|
225
288
|
}
|
|
289
|
+
const mcpOutcome = parseStageMcp(raw.mcp, label, entryId);
|
|
290
|
+
if (!mcpOutcome.ok)
|
|
291
|
+
return mcpOutcome;
|
|
292
|
+
if (mcpOutcome.value !== undefined) {
|
|
293
|
+
stage.mcp = mcpOutcome.value;
|
|
294
|
+
}
|
|
226
295
|
const agentField = parseAgentField(raw.agent);
|
|
227
296
|
if (!agentField.ok) {
|
|
228
297
|
return loadFailure([
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { loadFailure, loadSuccess } from "./loadOutcome.js";
|
|
3
|
+
import { parseStageMcp } from "./loadStage.js";
|
|
3
4
|
import { BODY_KEYS, isAllowedPipelineStageEntryKey, } from "./pipelineStageKeys.js";
|
|
4
5
|
import { parseExecutionPolicy } from "./parseCompletionContract.js";
|
|
5
6
|
import { parsePipelineNeeds } from "./pipelineNeeds.js";
|
|
@@ -31,7 +32,7 @@ function extractBodyRaw(raw) {
|
|
|
31
32
|
return body;
|
|
32
33
|
}
|
|
33
34
|
function hasBodyKey(raw) {
|
|
34
|
-
return Object.keys(raw).some((key) => BODY_KEYS.has(key) && key !== "skill");
|
|
35
|
+
return Object.keys(raw).some((key) => BODY_KEYS.has(key) && key !== "skill" && key !== "mcp");
|
|
35
36
|
}
|
|
36
37
|
function readSkill(raw) {
|
|
37
38
|
if (raw.skill === undefined)
|
|
@@ -96,6 +97,10 @@ export function normalizePipelineStageEntries(rawEntries, ctx) {
|
|
|
96
97
|
return skillOutcome;
|
|
97
98
|
}
|
|
98
99
|
const skill = skillOutcome?.ok ? skillOutcome.value : undefined;
|
|
100
|
+
const mcpOutcome = parseStageMcp(raw.mcp, `entry at index ${index} in ${declaringPath}`);
|
|
101
|
+
if (!mcpOutcome.ok)
|
|
102
|
+
return mcpOutcome;
|
|
103
|
+
const mcp = mcpOutcome.value;
|
|
99
104
|
if (uses && hasBody) {
|
|
100
105
|
return loadFailure([
|
|
101
106
|
{
|
|
@@ -220,6 +225,7 @@ export function normalizePipelineStageEntries(rawEntries, ctx) {
|
|
|
220
225
|
? { replay_safe: raw.replay_safe }
|
|
221
226
|
: {}),
|
|
222
227
|
...(skill !== undefined ? { skill } : {}),
|
|
228
|
+
...(mcp !== undefined ? { mcp } : {}),
|
|
223
229
|
};
|
|
224
230
|
const priorPath = normalized.find((e) => e.id === id)?.declaringPath;
|
|
225
231
|
if (priorPath) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type StageMcpErrorCode = "missing_catalog" | "unknown_server" | "reserved_name" | "unresolved_var" | "invalid_config" | "connect_failed";
|
|
2
|
+
export declare class StageMcpError extends Error {
|
|
3
|
+
readonly code: StageMcpErrorCode;
|
|
4
|
+
constructor(message: string, code: StageMcpErrorCode);
|
|
5
|
+
}
|
|
6
|
+
export type ResolvedMcpServerConfig = Record<string, unknown>;
|
|
7
|
+
export type ResolvedMcpServers = Record<string, ResolvedMcpServerConfig>;
|
|
8
|
+
export declare const MCP_CATALOG_FILENAME = ".mcp.json";
|
|
9
|
+
export declare const STAGEFLOW_STAGE_ARTIFACTS_DIR_ENV = "STAGEFLOW_STAGE_ARTIFACTS_DIR";
|
|
10
|
+
export declare function stampStagePromptArtifactsDir(prompt: string, artifactsDir: string): string;
|
|
11
|
+
export type McpCatalogServers = Record<string, Record<string, unknown>>;
|
|
12
|
+
export type InspectedMcpCatalog = {
|
|
13
|
+
path: string;
|
|
14
|
+
servers: McpCatalogServers;
|
|
15
|
+
};
|
|
16
|
+
export declare function mcpCatalogPath(projectRoot: string): string;
|
|
17
|
+
export declare function parseMcpCatalog(raw: string, catalogPath?: string): McpCatalogServers;
|
|
18
|
+
export declare function loadMcpCatalog(projectRoot: string): Promise<InspectedMcpCatalog>;
|
|
19
|
+
export type ProjectMcpCatalogTransport = "stdio" | "http";
|
|
20
|
+
export type ProjectMcpCatalogEntry = {
|
|
21
|
+
name: string;
|
|
22
|
+
transport: ProjectMcpCatalogTransport;
|
|
23
|
+
};
|
|
24
|
+
export type ProjectMcpCatalogListStatus = "ok" | "missing_catalog" | "invalid_config";
|
|
25
|
+
export type ProjectMcpCatalogList = {
|
|
26
|
+
status: ProjectMcpCatalogListStatus;
|
|
27
|
+
servers: ProjectMcpCatalogEntry[];
|
|
28
|
+
};
|
|
29
|
+
export declare function listProjectMcpCatalog(projectRoot: string): Promise<ProjectMcpCatalogList>;
|
|
30
|
+
export declare function assertMcpAllowlistKnown(servers: McpCatalogServers, allowlist: readonly string[]): void;
|
|
31
|
+
export declare function resolveStageMcpServers(options: {
|
|
32
|
+
projectRoot: string;
|
|
33
|
+
allowlist?: readonly string[];
|
|
34
|
+
env: NodeJS.ProcessEnv;
|
|
35
|
+
}): Promise<ResolvedMcpServers>;
|