@oisincoveney/pipeline 1.13.0 → 1.14.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.
@@ -5,71 +5,5 @@
5
5
  "source": "oisincoveney/skills",
6
6
  "args": ["--agent", "codex", "--skill", "*", "--yes", "--copy"]
7
7
  }
8
- ],
9
- "mcps": [
10
- {
11
- "name": "oisin-pipeline-backlog",
12
- "transport": "stdio",
13
- "command": "backlog",
14
- "args": ["mcp", "start"]
15
- },
16
- {
17
- "name": "oisin-pipeline-context7",
18
- "transport": "remote",
19
- "catalog": "context7"
20
- },
21
- {
22
- "name": "oisin-pipeline-github-readonly",
23
- "transport": "stdio",
24
- "command": "docker",
25
- "args": [
26
- "run",
27
- "-i",
28
- "--rm",
29
- "-e",
30
- "GITHUB_PERSONAL_ACCESS_TOKEN",
31
- "ghcr.io/github/github-mcp-server",
32
- "stdio",
33
- "--read-only"
34
- ],
35
- "env": {
36
- "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}"
37
- }
38
- },
39
- {
40
- "name": "oisin-pipeline-playwright",
41
- "transport": "stdio",
42
- "catalog": "playwright"
43
- },
44
- {
45
- "name": "oisin-pipeline-qdrant",
46
- "transport": "remote",
47
- "url": "https://memory-mcp.momokaya.ee/mcp/",
48
- "optionalRegistration": true,
49
- "headers": {
50
- "Authorization": {
51
- "sources": [{ "env": "MEMORY_MCP_BASIC_AUTH", "prefix": "Basic " }]
52
- }
53
- }
54
- },
55
- {
56
- "name": "oisin-pipeline-semgrep",
57
- "transport": "stdio",
58
- "command": "docker",
59
- "args": ["run", "-i", "--rm", "ghcr.io/semgrep/mcp", "-t", "stdio"]
60
- },
61
- {
62
- "name": "oisin-pipeline-serena",
63
- "transport": "stdio",
64
- "command": "uvx",
65
- "args": [
66
- "--from",
67
- "git+https://github.com/oraios/serena",
68
- "serena",
69
- "start-mcp-server",
70
- "--project-from-cwd",
71
- "--context=codex"
72
- ]
73
- }
74
8
  ]
75
9
  }
package/dist/config.d.ts CHANGED
@@ -194,6 +194,16 @@ declare const configSchema: z.ZodObject<{
194
194
  timeout_ms: z.ZodOptional<z.ZodNumber>;
195
195
  trusted: z.ZodOptional<z.ZodBoolean>;
196
196
  }, z.core.$strict>>>;
197
+ mcp_gateway: z.ZodOptional<z.ZodObject<{
198
+ default_profile: z.ZodOptional<z.ZodString>;
199
+ mode: z.ZodEnum<{
200
+ hosted: "hosted";
201
+ local: "local";
202
+ }>;
203
+ provider: z.ZodLiteral<"toolhive">;
204
+ token_env: z.ZodDefault<z.ZodString>;
205
+ url_env: z.ZodDefault<z.ZodString>;
206
+ }, z.core.$strict>>;
197
207
  mcp_servers: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
198
208
  args: z.ZodOptional<z.ZodArray<z.ZodString>>;
199
209
  bearer_token_env_var: z.ZodOptional<z.ZodString>;
@@ -247,6 +257,7 @@ declare const configSchema: z.ZodObject<{
247
257
  runner: z.ZodString;
248
258
  skills: z.ZodOptional<z.ZodArray<z.ZodString>>;
249
259
  tools: z.ZodOptional<z.ZodArray<z.ZodEnum<{
260
+ task: "task";
250
261
  read: "read";
251
262
  list: "list";
252
263
  grep: "grep";
@@ -254,7 +265,6 @@ declare const configSchema: z.ZodObject<{
254
265
  bash: "bash";
255
266
  edit: "edit";
256
267
  write: "write";
257
- task: "task";
258
268
  }>>>;
259
269
  }, z.core.$strict>>>;
260
270
  rules: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
@@ -282,6 +292,7 @@ declare const configSchema: z.ZodObject<{
282
292
  rules: z.ZodOptional<z.ZodBoolean>;
283
293
  skills: z.ZodOptional<z.ZodBoolean>;
284
294
  tools: z.ZodOptional<z.ZodArray<z.ZodEnum<{
295
+ task: "task";
285
296
  read: "read";
286
297
  list: "list";
287
298
  grep: "grep";
@@ -289,7 +300,6 @@ declare const configSchema: z.ZodObject<{
289
300
  bash: "bash";
290
301
  edit: "edit";
291
302
  write: "write";
292
- task: "task";
293
303
  }>>>;
294
304
  }, z.core.$strict>;
295
305
  command: z.ZodOptional<z.ZodString>;
package/dist/config.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { resolveFileReference } from "./path-refs.js";
2
- import { parseJson } from "./safe-json.js";
3
2
  import { existsSync, readFileSync } from "node:fs";
4
3
  import { join } from "node:path";
5
4
  import { parseDocument } from "yaml";
@@ -55,6 +54,7 @@ const RETRY_REASONS = [
55
54
  "timeout"
56
55
  ];
57
56
  const SCHEDULE_BASELINES = ["epic", "pipe"];
57
+ const PIPELINE_GATEWAY_SERVER_ID = "pipeline-gateway";
58
58
  var PipelineConfigError = class extends Error {
59
59
  code;
60
60
  issues;
@@ -126,21 +126,13 @@ const mcpServerSchema = z.object({
126
126
  path: ["bearer_token_env_var"]
127
127
  });
128
128
  });
129
- const mcpServerRefSchema = z.object({
130
- format: z.literal("mcp-json").default("mcp-json"),
131
- id: z.string().min(1).optional(),
132
- path: z.string().min(1)
129
+ const mcpGatewaySchema = z.object({
130
+ default_profile: z.string().min(1).optional(),
131
+ mode: z.enum(["hosted", "local"]),
132
+ provider: z.literal("toolhive"),
133
+ token_env: z.string().min(1).default("PIPELINE_MCP_GATEWAY_TOKEN"),
134
+ url_env: z.string().min(1).default("PIPELINE_MCP_GATEWAY_URL")
133
135
  }).strict();
134
- const mcpServerDefinitionSchema = z.union([mcpServerSchema, z.object({ ref: mcpServerRefSchema }).strict()]);
135
- const mcpJsonServerSchema = z.object({
136
- args: z.array(z.string()).optional(),
137
- bearer_token_env_var: z.string().min(1).optional(),
138
- command: z.string().min(1).optional(),
139
- env: z.record(z.string(), z.string()).optional(),
140
- headers: z.record(z.string(), z.string()).optional(),
141
- url: z.string().min(1).optional()
142
- }).passthrough();
143
- const mcpJsonFileSchema = z.object({ mcpServers: strictRecord(mcpJsonServerSchema) }).passthrough();
144
136
  const instructionsSchema = z.object({
145
137
  inline: z.string().min(1).optional(),
146
138
  path: z.string().min(1).optional()
@@ -327,7 +319,8 @@ const runnersFileSchema = z.object({
327
319
  version: z.literal(1)
328
320
  }).strict();
329
321
  const profilesFileSchema = z.object({
330
- mcp_servers: strictRecord(mcpServerDefinitionSchema).default({}),
322
+ mcp_gateway: mcpGatewaySchema.optional(),
323
+ mcp_servers: strictRecord(z.never()).default({}),
331
324
  profiles: strictRecord(profileSchema).default({}),
332
325
  rules: strictRecord(pathRefSchema).default({}),
333
326
  skills: strictRecord(pathRefSchema).default({}),
@@ -347,6 +340,7 @@ const configSchema = z.object({
347
340
  default_workflow: z.string(),
348
341
  entrypoints: strictRecord(entrypointSchema).default({}),
349
342
  hooks: strictRecord(hookSchema).default({}),
343
+ mcp_gateway: mcpGatewaySchema.optional(),
350
344
  mcp_servers: strictRecord(mcpServerSchema).default({}),
351
345
  orchestrator: orchestratorSchema,
352
346
  profiles: strictRecord(profileSchema).default({}),
@@ -464,12 +458,12 @@ function parsePipelineConfigParts(sources, projectRoot, sourcePaths = {
464
458
  const runners = parseYamlAs(sources.runners, sourcePaths.runners, runnersFileSchema);
465
459
  const profiles = parseYamlAs(sources.profiles, sourcePaths.profiles, profilesFileSchema);
466
460
  const pipeline = parseYamlAs(sources.pipeline, sourcePaths.pipeline, pipelineFileSchema);
467
- const mcpServers = resolveMcpServerDefinitions(profiles.mcp_servers, projectRoot);
468
461
  return validatePipelineConfig({
469
462
  default_workflow: pipeline.default_workflow,
470
463
  entrypoints: pipeline.entrypoints,
471
464
  hooks: pipeline.hooks,
472
- mcp_servers: mcpServers,
465
+ ...profiles.mcp_gateway ? { mcp_gateway: profiles.mcp_gateway } : {},
466
+ mcp_servers: profiles.mcp_servers,
473
467
  orchestrator: pipeline.orchestrator,
474
468
  profiles: profiles.profiles,
475
469
  rules: profiles.rules,
@@ -497,61 +491,6 @@ function parseYamlAs(source, sourcePath, schema) {
497
491
  })));
498
492
  return parsed.data;
499
493
  }
500
- function resolveMcpServerDefinitions(registry, projectRoot) {
501
- return Object.fromEntries(Object.entries(registry).map(([id, definition]) => [id, "ref" in definition ? resolveMcpServerRef(id, definition.ref, projectRoot) : definition]));
502
- }
503
- function resolveMcpServerRef(id, ref, projectRoot) {
504
- if (!projectRoot) throw validationError([{
505
- path: `mcp_servers.${id}.ref.path`,
506
- message: "MCP server refs require a project root"
507
- }]);
508
- const filePath = resolveFileReference(projectRoot, ref.path);
509
- if (!existsSync(filePath)) throw validationError([{
510
- path: `mcp_servers.${id}.ref.path`,
511
- message: `referenced MCP config file '${ref.path}' does not exist`
512
- }]);
513
- const parsed = parseMcpJsonFile(id, ref, filePath);
514
- const importedId = ref.id ?? id;
515
- const imported = parsed.mcpServers[importedId];
516
- if (!imported) throw validationError([{
517
- path: `mcp_servers.${id}.ref.id`,
518
- message: `MCP config '${ref.path}' does not declare server '${importedId}'`
519
- }]);
520
- const normalized = normalizeMcpJsonServer(imported);
521
- const result = mcpServerSchema.safeParse(normalized);
522
- if (!result.success) throw validationError(result.error.issues.map((issue) => ({
523
- path: [`mcp_servers.${id}.ref`, ...issue.path].join("."),
524
- message: issue.message
525
- })));
526
- return result.data;
527
- }
528
- function parseMcpJsonFile(id, ref, filePath) {
529
- let raw;
530
- try {
531
- raw = parseJson(readFileSync(filePath, "utf8"), `MCP config ${ref.path}`);
532
- } catch (err) {
533
- throw new PipelineConfigError("PIPELINE_CONFIG_PARSE_ERROR", `Failed to parse MCP config ${ref.path}`, [{
534
- path: `mcp_servers.${id}.ref.path`,
535
- message: err instanceof Error ? err.message : String(err)
536
- }]);
537
- }
538
- const parsed = mcpJsonFileSchema.safeParse(raw);
539
- if (!parsed.success) throw validationError(parsed.error.issues.map((issue) => ({
540
- path: [`mcp_servers.${id}.ref`, ...issue.path].join("."),
541
- message: issue.message
542
- })));
543
- return parsed.data;
544
- }
545
- function normalizeMcpJsonServer(server) {
546
- return {
547
- ...server.command ? { command: server.command } : {},
548
- ...server.args ? { args: server.args } : {},
549
- ...server.env ? { env: server.env } : {},
550
- ...server.url ? { url: server.url } : {},
551
- ...server.headers ? { headers: server.headers } : {},
552
- ...server.bearer_token_env_var ? { bearer_token_env_var: server.bearer_token_env_var } : {}
553
- };
554
- }
555
494
  function validatePipelineConfig(rawConfig, projectRoot, options = {}) {
556
495
  const parsed = configSchema.safeParse(rawConfig);
557
496
  if (!parsed.success) throw validationError(parsed.error.issues.map((issue) => ({
@@ -629,7 +568,10 @@ function validateActor(label, path, actor, runner, config, issues, projectRoot,
629
568
  validatePath(`${path}.instructions.path`, actor.instructions.path, projectRoot, issues, options);
630
569
  validateReferences(`${path}.rules`, actor.rules, config.rules, "rule", issues);
631
570
  validateReferences(`${path}.skills`, actor.skills, config.skills, "skill", issues);
632
- validateReferences(`${path}.mcp_servers`, actor.mcp_servers, config.mcp_servers, "MCP server", issues);
571
+ validateReferences(`${path}.mcp_servers`, actor.mcp_servers, config.mcp_gateway ? {
572
+ ...config.mcp_servers,
573
+ [PIPELINE_GATEWAY_SERVER_ID]: {}
574
+ } : config.mcp_servers, "MCP server", issues);
633
575
  validateBooleanCapability(`${path}.rules`, actor.rules, runner.capabilities.rules, "rules", issues);
634
576
  validateBooleanCapability(`${path}.skills`, actor.skills, runner.capabilities.skills, "skills", issues);
635
577
  validateBooleanCapability(`${path}.mcp_servers`, actor.mcp_servers, runner.capabilities.mcp_servers, "MCP servers", issues);
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { PipelineConfigError, loadPipelineConfig, tryLoadPipelineConfig } from "./config.js";
3
+ import { configureGatewayHosts, localGatewayStatus, renderGatewayConfig, runGatewayDoctor, startLocalGateway } from "./mcp/gateway.js";
3
4
  import { compileWorkflowPlan } from "./workflow-planner.js";
4
5
  import { formatInstallCommandsResult, installCommands, parseCommandHost } from "./install-commands.js";
5
6
  import { createOrchestratorLaunchPlan, createRunnerLaunchPlan } from "./runner.js";
6
7
  import { formatConfigError, runPipelineFromConfig } from "./pipeline-runtime.js";
7
8
  import { runKubernetesRunnerJob } from "./kubernetes-runner.js";
8
- import { DEFAULT_MCPM_ARGS } from "./mcp/bootstrap.js";
9
9
  import { formatPipelineInitResult, initPipelineProject } from "./pipeline-init.js";
10
10
  import { compileScheduleArtifact, generateScheduleArtifact, parseScheduleArtifact } from "./schedule-planner.js";
11
11
  import { existsSync, readFileSync, realpathSync } from "node:fs";
@@ -191,6 +191,7 @@ const BUILTIN_PIPE_COMMANDS = new Set([
191
191
  "doctor",
192
192
  "init",
193
193
  "install-commands",
194
+ "mcp",
194
195
  "runner-job"
195
196
  ]);
196
197
  function createCliProgram() {
@@ -225,6 +226,39 @@ function createCliProgram() {
225
226
  console.log(formatDoctorResult(result));
226
227
  if (!result.passed) throw new Error("Doctor checks failed.");
227
228
  });
229
+ const gatewayCommand = program.command("mcp").description("Manage the hosted-first MCP gateway").command("gateway").description("Inspect and configure the pipeline MCP gateway");
230
+ gatewayCommand.command("doctor").description("Check MCP gateway configuration and legacy direct MCP entries").action(async () => {
231
+ const cwd = process.env.PIPELINE_TARGET_PATH ?? process.cwd();
232
+ const result = await runGatewayDoctor(loadPipelineConfig(cwd, { allowMissingLintFileReferences: true }), cwd);
233
+ console.log(formatDoctorResult(result));
234
+ if (!result.passed) throw new Error("MCP gateway doctor checks failed.");
235
+ });
236
+ gatewayCommand.command("config").description("Print resolved MCP gateway client configuration").action(() => {
237
+ const config = loadPipelineConfig(process.env.PIPELINE_TARGET_PATH ?? process.cwd(), { allowMissingLintFileReferences: true });
238
+ console.log(renderGatewayConfig(config));
239
+ });
240
+ gatewayCommand.command("configure-host").description("Rewrite host MCP config to the singleton pipeline gateway").addOption(new Option("--host <host>", "host config to update").choices([
241
+ "all",
242
+ "opencode",
243
+ "codex"
244
+ ]).default("all").argParser(parseCommandHost)).addOption(new Option("--scope <scope>", "config scope to update").choices(["project", "global"]).default("project").argParser(parseGatewayHostScope)).action((flags) => {
245
+ const cwd = process.env.PIPELINE_TARGET_PATH ?? process.cwd();
246
+ const result = configureGatewayHosts(loadPipelineConfig(cwd, { allowMissingLintFileReferences: true }), {
247
+ cwd,
248
+ host: flags.host ?? "all",
249
+ scope: flags.scope ?? "project"
250
+ });
251
+ console.log(result.map((item) => [`${item.host}: ${item.path}`, item.backupPath ? `backup=${item.backupPath}` : "backup=none"].join(" ")).join("\n"));
252
+ });
253
+ gatewayCommand.command("local-start").description("Start a local ToolHive vMCP gateway for local mode").option("--detach", "reserved for future background startup", false).action(async (flags) => {
254
+ if (flags.detach) throw new Error("Detached local gateway startup is not implemented.");
255
+ const cwd = process.env.PIPELINE_TARGET_PATH ?? process.cwd();
256
+ await startLocalGateway(loadPipelineConfig(cwd, { allowMissingLintFileReferences: true }), cwd);
257
+ });
258
+ gatewayCommand.command("local-status").description("Show local ToolHive MCP server status").action(async () => {
259
+ const cwd = process.env.PIPELINE_TARGET_PATH ?? process.cwd();
260
+ console.log(await localGatewayStatus(cwd));
261
+ });
228
262
  program.command("init").description("Scaffold the default .pipeline/pipeline.yaml workflow").option("--overwrite", "replace existing pipeline scaffold files", false).action(async (flags) => {
229
263
  const result = await initPipelineProject({
230
264
  cwd: process.env.PIPELINE_TARGET_PATH ?? process.cwd(),
@@ -276,6 +310,10 @@ function registerConfiguredEntrypointCommands(program, config) {
276
310
  }
277
311
  return registered;
278
312
  }
313
+ function parseGatewayHostScope(value) {
314
+ if (value === "project" || value === "global") return value;
315
+ throw new Error("scope must be project or global");
316
+ }
279
317
  function lintPipelineConfig(config, projectRoot) {
280
318
  return [
281
319
  ...lintShadowedEntrypoints(config),
@@ -364,10 +402,8 @@ function shouldParsePipeArgsDirectly(program, firstArg) {
364
402
  async function runDoctor(cwd) {
365
403
  const commandChecks = await Promise.all([
366
404
  checkCommand("npx", ["--version"], cwd),
367
- checkCommand("backlog", ["--version"], cwd),
368
- checkCommand("uvx", ["--version"], cwd),
369
- checkCommandWithRunner("mcpm-cli", "uvx", [...DEFAULT_MCPM_ARGS, "--version"], cwd),
370
- checkCommand("codex", ["--version"], cwd)
405
+ checkCommand("codex", ["--version"], cwd),
406
+ checkCommand("opencode", ["--version"], cwd)
371
407
  ]);
372
408
  const configCheck = checkPipelineConfig(cwd);
373
409
  const checks = [...commandChecks, configCheck];
@@ -1,97 +1,15 @@
1
1
  import { parseJson } from "../safe-json.js";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { z } from "zod";
4
- import { execa } from "execa";
5
4
  //#region src/mcp/bootstrap.ts
6
- var PipelineMcpInstallError = class extends Error {
7
- constructor(message) {
8
- super(message);
9
- this.name = "PipelineMcpInstallError";
10
- }
11
- };
12
- var PipelineMcpMissingCredentialError = class extends PipelineMcpInstallError {
13
- headerName;
14
- missingEnv;
15
- serverName;
16
- constructor(serverName, headerName, missingEnv) {
17
- super([`MCP server ${serverName} requires ${headerName} credentials before it can be registered.`, `Set ${missingEnv.join(" or ")} and re-run pipeline init.`].join("\n"));
18
- this.name = "PipelineMcpMissingCredentialError";
19
- this.serverName = serverName;
20
- this.headerName = headerName;
21
- this.missingEnv = missingEnv;
22
- }
23
- };
24
5
  var PipelineDefaultManifestError = class extends Error {
25
6
  constructor(message) {
26
7
  super(message);
27
8
  this.name = "PipelineDefaultManifestError";
28
9
  }
29
10
  };
30
- const DEFAULT_MCPM_ARGS = [
31
- "--python",
32
- "3.12",
33
- "mcpm"
34
- ];
35
11
  const DEFAULT_INSTALL_MANIFEST_URL = new URL("../../defaults/install-manifest.json", import.meta.url);
36
- const pipelineMcpHeaderSourceSchema = z.object({
37
- env: z.string().min(1),
38
- prefix: z.string().optional(),
39
- suffix: z.string().optional()
40
- }).strict();
41
- const pipelineMcpHeaderValueSchema = z.union([z.string(), z.object({ sources: z.array(pipelineMcpHeaderSourceSchema).min(1) }).strict()]);
42
- const pipelineMcpInstallSpecSchema = z.object({
43
- args: z.array(z.string()).optional(),
44
- catalog: z.string().min(1).optional(),
45
- command: z.string().min(1).optional(),
46
- env: z.record(z.string(), z.string()).optional(),
47
- headers: z.record(z.string(), pipelineMcpHeaderValueSchema).optional(),
48
- name: z.string().min(1),
49
- optionalRegistration: z.boolean().optional(),
50
- transport: z.enum(["remote", "stdio"]),
51
- url: z.string().url().optional()
52
- }).strict().superRefine((spec, ctx) => {
53
- if (spec.catalog) return;
54
- if (spec.transport === "remote") {
55
- if (!spec.url) ctx.addIssue({
56
- code: "custom",
57
- message: "remote MCP install spec must declare url or catalog",
58
- path: ["url"]
59
- });
60
- if (spec.command) ctx.addIssue({
61
- code: "custom",
62
- message: "remote MCP install spec cannot declare command",
63
- path: ["command"]
64
- });
65
- if (spec.args) ctx.addIssue({
66
- code: "custom",
67
- message: "remote MCP install spec cannot declare args",
68
- path: ["args"]
69
- });
70
- if (spec.env) ctx.addIssue({
71
- code: "custom",
72
- message: "remote MCP install spec cannot declare env",
73
- path: ["env"]
74
- });
75
- return;
76
- }
77
- if (!spec.command) ctx.addIssue({
78
- code: "custom",
79
- message: "stdio MCP install spec must declare command or catalog",
80
- path: ["command"]
81
- });
82
- if (spec.headers) ctx.addIssue({
83
- code: "custom",
84
- message: "stdio MCP install spec cannot declare headers",
85
- path: ["headers"]
86
- });
87
- if (spec.url) ctx.addIssue({
88
- code: "custom",
89
- message: "stdio MCP install spec cannot declare url",
90
- path: ["url"]
91
- });
92
- });
93
12
  const defaultInstallManifestSchema = z.object({
94
- mcps: z.array(pipelineMcpInstallSpecSchema),
95
13
  skills: z.array(z.object({
96
14
  args: z.array(z.string()).optional(),
97
15
  source: z.string().min(1)
@@ -104,135 +22,6 @@ function loadDefaultInstallManifest() {
104
22
  if (!parsed.success) throw new PipelineDefaultManifestError(["Invalid defaults/install-manifest.json.", ...parsed.error.issues.map((issue) => [issue.path.join("."), issue.message].filter(Boolean).join(": "))].join("\n"));
105
23
  return parsed.data;
106
24
  }
107
- const DEFAULT_INSTALL_MANIFEST = loadDefaultInstallManifest();
108
- const DEFAULT_MCP_INSTALLS = DEFAULT_INSTALL_MANIFEST.mcps;
109
- const DEFAULT_SKILL_INSTALLS = DEFAULT_INSTALL_MANIFEST.skills;
110
- function defaultMcpJson() {
111
- return `${JSON.stringify({ mcpServers: Object.fromEntries([
112
- ["backlog", "oisin-pipeline-backlog"],
113
- ["context7", "oisin-pipeline-context7"],
114
- ["github-readonly", "oisin-pipeline-github-readonly"],
115
- ["playwright", "oisin-pipeline-playwright"],
116
- ["qdrant", "oisin-pipeline-qdrant"],
117
- ["semgrep", "oisin-pipeline-semgrep"],
118
- ["serena", "oisin-pipeline-serena"]
119
- ].map(([server, installName]) => [server, {
120
- args: [
121
- ...DEFAULT_MCPM_ARGS,
122
- "run",
123
- installName
124
- ],
125
- command: "uvx"
126
- }])) }, null, 2)}\n`;
127
- }
128
- async function installDefaultMcpsWithCli(specs, cwd) {
129
- const skipped = [];
130
- for (const spec of specs) {
131
- const install = mcpInstallArgs(spec);
132
- if ("skipped" in install) {
133
- skipped.push(install.skipped);
134
- continue;
135
- }
136
- try {
137
- await execa("uvx", [...DEFAULT_MCPM_ARGS, ...install.args], {
138
- cwd,
139
- env: {
140
- MCPM_FORCE: "true",
141
- MCPM_JSON_OUTPUT: "true",
142
- MCPM_NON_INTERACTIVE: "true"
143
- },
144
- stdin: "ignore"
145
- });
146
- } catch (err) {
147
- const error = err;
148
- throw new PipelineMcpInstallError([
149
- `Failed to register MCP server ${spec.name} with MCPM.`,
150
- "Pipeline init runs MCPM through `uvx --python 3.12 mcpm`.",
151
- "Install uv/uvx from https://docs.astral.sh/uv/ and re-run pipeline init.",
152
- redactMcpInstallOutput(error.shortMessage, install.redactions),
153
- redactMcpInstallOutput(error.stderr, install.redactions),
154
- redactMcpInstallOutput(error.stdout, install.redactions)
155
- ].filter(Boolean).join("\n"));
156
- }
157
- }
158
- return { skipped };
159
- }
160
- function mcpInstallArgs(spec) {
161
- if (spec.catalog) return {
162
- args: [
163
- "install",
164
- spec.catalog,
165
- "--force",
166
- "--alias",
167
- spec.name
168
- ],
169
- redactions: []
170
- };
171
- const args = [
172
- "new",
173
- spec.name,
174
- "--type",
175
- spec.transport,
176
- "--force"
177
- ];
178
- if (spec.transport === "remote") {
179
- if (!spec.url) throw new PipelineMcpInstallError(`MCP server ${spec.name} is remote but has no url.`);
180
- const redactions = [];
181
- try {
182
- const headers = Object.entries(spec.headers ?? {}).flatMap(([key, value]) => {
183
- const headerValue = resolveMcpHeaderValue(spec.name, key, value);
184
- redactions.push(headerValue);
185
- return ["--headers", `${key}=${headerValue}`];
186
- });
187
- return {
188
- args: [
189
- ...args,
190
- "--url",
191
- spec.url,
192
- ...headers
193
- ],
194
- redactions
195
- };
196
- } catch (err) {
197
- if (spec.optionalRegistration && err instanceof PipelineMcpMissingCredentialError) return { skipped: {
198
- missingEnv: err.missingEnv,
199
- name: spec.name,
200
- reason: `missing ${err.headerName} credentials`
201
- } };
202
- throw err;
203
- }
204
- }
205
- if (!spec.command) throw new PipelineMcpInstallError(`MCP server ${spec.name} is stdio but has no command.`);
206
- return {
207
- args: [
208
- ...args,
209
- "--command",
210
- spec.command,
211
- ...spec.args?.length ? ["--args", spec.args.join(" ")] : [],
212
- ...Object.entries(spec.env ?? {}).flatMap(([key, value]) => ["--env", `${key}=${value}`])
213
- ],
214
- redactions: []
215
- };
216
- }
217
- const MCP_CREDENTIAL_PATTERN = /^\S+\s+(.+)$/;
218
- function redactMcpInstallOutput(value, redactions) {
219
- if (!value) return value;
220
- const sensitiveValues = redactions.flatMap((item) => {
221
- const trimmed = item.trim();
222
- const credential = trimmed.match(MCP_CREDENTIAL_PATTERN)?.[1]?.trim();
223
- return credential ? [trimmed, credential] : [trimmed];
224
- }).filter((item) => item.length > 0);
225
- const escaped = [...new Set(sensitiveValues)].sort((a, b) => b.length - a.length).map((item) => item.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
226
- const sensitivePattern = escaped.length > 0 ? new RegExp(escaped.join("|"), "g") : null;
227
- return (sensitivePattern ? value.replace(sensitivePattern, "[REDACTED]") : value).replace(/Authorization=[^\r\n'"]+/gi, "Authorization=[REDACTED]");
228
- }
229
- function resolveMcpHeaderValue(serverName, headerName, header) {
230
- if (typeof header === "string") return header;
231
- for (const source of header.sources ?? []) {
232
- const rawValue = process.env[source.env];
233
- if (rawValue && rawValue.trim().length > 0) return `${source.prefix ?? ""}${rawValue}${source.suffix ?? ""}`;
234
- }
235
- throw new PipelineMcpMissingCredentialError(serverName, headerName, header.sources.map((source) => source.env));
236
- }
25
+ const DEFAULT_SKILL_INSTALLS = loadDefaultInstallManifest().skills;
237
26
  //#endregion
238
- export { DEFAULT_MCPM_ARGS, DEFAULT_MCP_INSTALLS, DEFAULT_SKILL_INSTALLS, defaultMcpJson, installDefaultMcpsWithCli };
27
+ export { DEFAULT_SKILL_INSTALLS };