@oisincoveney/pipeline 1.6.0 → 1.7.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/dist/config.js +74 -3
- package/docs/config-architecture.md +20 -0
- package/package.json +1 -1
package/dist/config.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { isAbsolute, join } from "node:path";
|
|
3
3
|
import { parseDocument } from "yaml";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
//#region src/config.ts
|
|
@@ -125,6 +125,21 @@ const mcpServerSchema = z.object({
|
|
|
125
125
|
path: ["bearer_token_env_var"]
|
|
126
126
|
});
|
|
127
127
|
});
|
|
128
|
+
const mcpServerRefSchema = z.object({
|
|
129
|
+
format: z.literal("mcp-json").default("mcp-json"),
|
|
130
|
+
id: z.string().min(1).optional(),
|
|
131
|
+
path: z.string().min(1)
|
|
132
|
+
}).strict();
|
|
133
|
+
const mcpServerDefinitionSchema = z.union([mcpServerSchema, z.object({ ref: mcpServerRefSchema }).strict()]);
|
|
134
|
+
const mcpJsonServerSchema = z.object({
|
|
135
|
+
args: z.array(z.string()).optional(),
|
|
136
|
+
bearer_token_env_var: z.string().min(1).optional(),
|
|
137
|
+
command: z.string().min(1).optional(),
|
|
138
|
+
env: z.record(z.string(), z.string()).optional(),
|
|
139
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
140
|
+
url: z.string().min(1).optional()
|
|
141
|
+
}).passthrough();
|
|
142
|
+
const mcpJsonFileSchema = z.object({ mcpServers: strictRecord(mcpJsonServerSchema) }).passthrough();
|
|
128
143
|
const instructionsSchema = z.object({
|
|
129
144
|
inline: z.string().min(1).optional(),
|
|
130
145
|
path: z.string().min(1).optional()
|
|
@@ -286,7 +301,7 @@ const runnersFileSchema = z.object({
|
|
|
286
301
|
version: z.literal(1)
|
|
287
302
|
}).strict();
|
|
288
303
|
const profilesFileSchema = z.object({
|
|
289
|
-
mcp_servers: strictRecord(
|
|
304
|
+
mcp_servers: strictRecord(mcpServerDefinitionSchema).default({}),
|
|
290
305
|
profiles: strictRecord(profileSchema).default({}),
|
|
291
306
|
rules: strictRecord(pathRefSchema).default({}),
|
|
292
307
|
skills: strictRecord(pathRefSchema).default({}),
|
|
@@ -356,11 +371,12 @@ function parsePipelineConfigParts(sources, projectRoot, sourcePaths = {
|
|
|
356
371
|
const runners = parseYamlAs(sources.runners, sourcePaths.runners, runnersFileSchema);
|
|
357
372
|
const profiles = parseYamlAs(sources.profiles, sourcePaths.profiles, profilesFileSchema);
|
|
358
373
|
const pipeline = parseYamlAs(sources.pipeline, sourcePaths.pipeline, pipelineFileSchema);
|
|
374
|
+
const mcpServers = resolveMcpServerDefinitions(profiles.mcp_servers, projectRoot);
|
|
359
375
|
return validatePipelineConfig({
|
|
360
376
|
default_workflow: pipeline.default_workflow,
|
|
361
377
|
entrypoints: pipeline.entrypoints,
|
|
362
378
|
hooks: pipeline.hooks,
|
|
363
|
-
mcp_servers:
|
|
379
|
+
mcp_servers: mcpServers,
|
|
364
380
|
orchestrator: pipeline.orchestrator,
|
|
365
381
|
profiles: profiles.profiles,
|
|
366
382
|
rules: profiles.rules,
|
|
@@ -387,6 +403,61 @@ function parseYamlAs(source, sourcePath, schema) {
|
|
|
387
403
|
})));
|
|
388
404
|
return parsed.data;
|
|
389
405
|
}
|
|
406
|
+
function resolveMcpServerDefinitions(registry, projectRoot) {
|
|
407
|
+
return Object.fromEntries(Object.entries(registry).map(([id, definition]) => [id, "ref" in definition ? resolveMcpServerRef(id, definition.ref, projectRoot) : definition]));
|
|
408
|
+
}
|
|
409
|
+
function resolveMcpServerRef(id, ref, projectRoot) {
|
|
410
|
+
if (!projectRoot) throw validationError([{
|
|
411
|
+
path: `mcp_servers.${id}.ref.path`,
|
|
412
|
+
message: "MCP server refs require a project root"
|
|
413
|
+
}]);
|
|
414
|
+
const filePath = isAbsolute(ref.path) ? ref.path : join(projectRoot, ref.path);
|
|
415
|
+
if (!existsSync(filePath)) throw validationError([{
|
|
416
|
+
path: `mcp_servers.${id}.ref.path`,
|
|
417
|
+
message: `referenced MCP config file '${ref.path}' does not exist`
|
|
418
|
+
}]);
|
|
419
|
+
const parsed = parseMcpJsonFile(id, ref, filePath);
|
|
420
|
+
const importedId = ref.id ?? id;
|
|
421
|
+
const imported = parsed.mcpServers[importedId];
|
|
422
|
+
if (!imported) throw validationError([{
|
|
423
|
+
path: `mcp_servers.${id}.ref.id`,
|
|
424
|
+
message: `MCP config '${ref.path}' does not declare server '${importedId}'`
|
|
425
|
+
}]);
|
|
426
|
+
const normalized = normalizeMcpJsonServer(imported);
|
|
427
|
+
const result = mcpServerSchema.safeParse(normalized);
|
|
428
|
+
if (!result.success) throw validationError(result.error.issues.map((issue) => ({
|
|
429
|
+
path: [`mcp_servers.${id}.ref`, ...issue.path].join("."),
|
|
430
|
+
message: issue.message
|
|
431
|
+
})));
|
|
432
|
+
return result.data;
|
|
433
|
+
}
|
|
434
|
+
function parseMcpJsonFile(id, ref, filePath) {
|
|
435
|
+
let raw;
|
|
436
|
+
try {
|
|
437
|
+
raw = JSON.parse(readFileSync(filePath, "utf8"));
|
|
438
|
+
} catch (err) {
|
|
439
|
+
throw new PipelineConfigError("PIPELINE_CONFIG_PARSE_ERROR", `Failed to parse MCP config ${ref.path}`, [{
|
|
440
|
+
path: `mcp_servers.${id}.ref.path`,
|
|
441
|
+
message: err instanceof Error ? err.message : String(err)
|
|
442
|
+
}]);
|
|
443
|
+
}
|
|
444
|
+
const parsed = mcpJsonFileSchema.safeParse(raw);
|
|
445
|
+
if (!parsed.success) throw validationError(parsed.error.issues.map((issue) => ({
|
|
446
|
+
path: [`mcp_servers.${id}.ref`, ...issue.path].join("."),
|
|
447
|
+
message: issue.message
|
|
448
|
+
})));
|
|
449
|
+
return parsed.data;
|
|
450
|
+
}
|
|
451
|
+
function normalizeMcpJsonServer(server) {
|
|
452
|
+
return {
|
|
453
|
+
...server.command ? { command: server.command } : {},
|
|
454
|
+
...server.args ? { args: server.args } : {},
|
|
455
|
+
...server.env ? { env: server.env } : {},
|
|
456
|
+
...server.url ? { url: server.url } : {},
|
|
457
|
+
...server.headers ? { headers: server.headers } : {},
|
|
458
|
+
...server.bearer_token_env_var ? { bearer_token_env_var: server.bearer_token_env_var } : {}
|
|
459
|
+
};
|
|
460
|
+
}
|
|
390
461
|
function validatePipelineConfig(config, projectRoot) {
|
|
391
462
|
const issues = [];
|
|
392
463
|
validateRegistryIds("runners", config.runners, issues);
|
|
@@ -154,6 +154,26 @@ Exactly one of `command` or `url` is required. `args` and `env` apply only to
|
|
|
154
154
|
command servers. `headers` and `bearer_token_env_var` apply only to URL
|
|
155
155
|
servers.
|
|
156
156
|
|
|
157
|
+
MCP registry entries can also reference an existing MCP JSON config file. This
|
|
158
|
+
keeps the pipeline config decoupled from whatever generated the MCP config:
|
|
159
|
+
|
|
160
|
+
```yaml
|
|
161
|
+
mcp_servers:
|
|
162
|
+
serena:
|
|
163
|
+
ref:
|
|
164
|
+
path: .mcp.json
|
|
165
|
+
id: serena
|
|
166
|
+
|
|
167
|
+
profiles:
|
|
168
|
+
inspector:
|
|
169
|
+
runner: codex
|
|
170
|
+
mcp_servers: [serena]
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`ref.path` is resolved from the repository root and currently supports the
|
|
174
|
+
standard `mcp-json` shape: `{ "mcpServers": { "<id>": { ... } } }`. If `id` is
|
|
175
|
+
omitted, the registry key is used.
|
|
176
|
+
|
|
157
177
|
JSON Schema outputs are hard contracts. The runtime validates normalized agent
|
|
158
178
|
output before the node can pass. Schema outputs also get a bounded repair pass
|
|
159
179
|
by default:
|
package/package.json
CHANGED