@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.
- package/defaults/install-manifest.json +0 -66
- package/dist/config.d.ts +12 -2
- package/dist/config.js +16 -74
- package/dist/index.js +41 -5
- package/dist/mcp/bootstrap.js +2 -213
- package/dist/mcp/gateway.js +288 -0
- package/dist/mcp/launch-plan.js +2 -4
- package/dist/mcp/native-config.js +2 -4
- package/dist/pipeline-init.js +22 -50
- package/dist/pipeline-runtime.js +7 -6
- package/docs/config-architecture.md +14 -33
- package/docs/mcp-gateway.md +26 -28
- package/docs/mcp-host-isolation.md +10 -10
- package/docs/operator-guide.md +56 -73
- package/package.json +1 -1
|
@@ -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
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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("
|
|
368
|
-
checkCommand("
|
|
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];
|
package/dist/mcp/bootstrap.js
CHANGED
|
@@ -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
|
|
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 {
|
|
27
|
+
export { DEFAULT_SKILL_INSTALLS };
|