@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 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(mcpServerSchema).default({}),
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: profiles.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
@@ -81,7 +81,7 @@
81
81
  "prepack": "bun run build:cli"
82
82
  },
83
83
  "type": "module",
84
- "version": "1.6.0",
84
+ "version": "1.7.0",
85
85
  "description": "Config-driven multi-agent pipeline runner for repository work",
86
86
  "main": "./dist/index.js",
87
87
  "types": "./dist/index.d.ts",