@vornrun/mcp 0.5.4 → 0.5.5

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.
Files changed (2) hide show
  1. package/dist/index.js +344 -5
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -415,6 +415,9 @@ function createSchema() {
415
415
  project_path TEXT,
416
416
  approved_at TEXT,
417
417
  diagnostics TEXT,
418
+ output TEXT,
419
+ structured_output TEXT,
420
+ iteration INTEGER,
418
421
  FOREIGN KEY (run_id) REFERENCES workflow_runs(id) ON DELETE CASCADE
419
422
  );
420
423
 
@@ -894,7 +897,19 @@ function verifySchema(d) {
894
897
  {
895
898
  column: "diagnostics",
896
899
  ddl: "ALTER TABLE workflow_run_nodes ADD COLUMN diagnostics TEXT"
897
- }
900
+ },
901
+ // A step's result was held only in renderer memory. After a reload the
902
+ // typed verdict was gone, {{steps.<slug>.<field>}} silently fell back to
903
+ // raw logs, and verdictOf reported "completed" for a run whose agent had
904
+ // said otherwise — worst on a run parked at an approval gate, which is
905
+ // exactly the case that outlives a restart.
906
+ { column: "output", ddl: "ALTER TABLE workflow_run_nodes ADD COLUMN output TEXT" },
907
+ {
908
+ column: "structured_output",
909
+ ddl: "ALTER TABLE workflow_run_nodes ADD COLUMN structured_output TEXT"
910
+ },
911
+ // Which pass of a loop produced this row.
912
+ { column: "iteration", ddl: "ALTER TABLE workflow_run_nodes ADD COLUMN iteration INTEGER" }
898
913
  ],
899
914
  tasks: [
900
915
  {
@@ -1554,6 +1569,7 @@ function rowToWorkspace(r) {
1554
1569
  };
1555
1570
  }
1556
1571
  function mapNodeRow(n) {
1572
+ const structured = n.structured_output != null ? parseStructuredOutput(n.structured_output) : void 0;
1557
1573
  return {
1558
1574
  nodeId: n.node_id,
1559
1575
  status: n.status,
@@ -1568,9 +1584,20 @@ function mapNodeRow(n) {
1568
1584
  ...n.project_name != null && { projectName: n.project_name },
1569
1585
  ...n.project_path != null && { projectPath: n.project_path },
1570
1586
  ...n.approved_at != null && { approvedAt: n.approved_at },
1571
- ...n.diagnostics != null && { diagnostics: n.diagnostics }
1587
+ ...n.diagnostics != null && { diagnostics: n.diagnostics },
1588
+ ...n.output != null && { output: n.output },
1589
+ ...structured !== void 0 && { structuredOutput: structured },
1590
+ ...n.iteration != null && { iteration: n.iteration }
1572
1591
  };
1573
1592
  }
1593
+ function parseStructuredOutput(raw) {
1594
+ try {
1595
+ const parsed = JSON.parse(raw);
1596
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
1597
+ } catch {
1598
+ return void 0;
1599
+ }
1600
+ }
1574
1601
  function fetchNodesByRunIds(d, runIds) {
1575
1602
  if (runIds.length === 0) return /* @__PURE__ */ new Map();
1576
1603
  const placeholders = runIds.map(() => "?").join(",");
@@ -2709,6 +2736,103 @@ function registerSessionTools(server) {
2709
2736
  // src/tools/workflows.ts
2710
2737
  import crypto2 from "crypto";
2711
2738
  import { z as z5 } from "zod";
2739
+
2740
+ // src/workflow-portability.ts
2741
+ var PROJECT_PATH_TOKEN = "{{project.path}}";
2742
+ var PROJECT_NAME_TOKEN = "{{project.name}}";
2743
+ var PORTABLE_FORMAT_VERSION = 1;
2744
+ function importedWorkflowId(bundle, slug) {
2745
+ return `import:${bundle}:${slug}`;
2746
+ }
2747
+ function slugify(name) {
2748
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "workflow";
2749
+ }
2750
+ function portabilityBlockers(workflow) {
2751
+ const blockers = [];
2752
+ for (const node of workflow.nodes) {
2753
+ const config = node.config;
2754
+ if (node.type === "trigger" && config.triggerType === "connectorPoll") {
2755
+ blockers.push(`the trigger polls a connector connection, which exists only on this machine`);
2756
+ }
2757
+ if (node.type === "callConnectorAction") {
2758
+ blockers.push(`step "${node.label}" calls a connector action bound to a local connection`);
2759
+ }
2760
+ }
2761
+ return blockers;
2762
+ }
2763
+ function toPortable(workflow, projectPath) {
2764
+ const slug = slugify(workflow.name);
2765
+ const nodes = workflow.nodes.map((node) => {
2766
+ const config = { ...node.config };
2767
+ if (node.type === "launchAgent" || node.type === "script") {
2768
+ for (const key of ["projectPath", "cwd", "existingWorktreePath"]) {
2769
+ const value = config[key];
2770
+ if (typeof value === "string" && value) {
2771
+ config[key] = replacePath(value, projectPath);
2772
+ }
2773
+ }
2774
+ if (typeof config.projectName === "string" && config.projectName) {
2775
+ config[`projectName`] = PROJECT_NAME_TOKEN;
2776
+ }
2777
+ delete config.remoteHostId;
2778
+ }
2779
+ return { ...node, config };
2780
+ });
2781
+ return {
2782
+ version: PORTABLE_FORMAT_VERSION,
2783
+ slug,
2784
+ name: workflow.name,
2785
+ ...workflow.icon && { icon: workflow.icon },
2786
+ ...workflow.iconColor && { iconColor: workflow.iconColor },
2787
+ ...workflow.staggerDelayMs !== void 0 && { staggerDelayMs: workflow.staggerDelayMs },
2788
+ nodes,
2789
+ edges: workflow.edges
2790
+ };
2791
+ }
2792
+ function normalizeForCompare(p) {
2793
+ return p.replace(/\\/g, "/").replace(/\/+$/, "");
2794
+ }
2795
+ function replacePath(value, projectPath) {
2796
+ const v = normalizeForCompare(value);
2797
+ const root = normalizeForCompare(projectPath);
2798
+ if (v === root) return PROJECT_PATH_TOKEN;
2799
+ if (v.startsWith(`${root}/`)) return `${PROJECT_PATH_TOKEN}/${v.slice(root.length + 1)}`;
2800
+ return value;
2801
+ }
2802
+ function fromPortable(portable, bundle, project) {
2803
+ const nodes = portable.nodes.map((node) => {
2804
+ const config = { ...node.config };
2805
+ for (const [key, value] of Object.entries(config)) {
2806
+ if (typeof value !== "string") continue;
2807
+ config[key] = value.split(PROJECT_PATH_TOKEN).join(project.path.replace(/[/\\]+$/, "")).split(PROJECT_NAME_TOKEN).join(project.name);
2808
+ }
2809
+ return { ...node, config };
2810
+ });
2811
+ return {
2812
+ id: importedWorkflowId(bundle, portable.slug),
2813
+ name: portable.name,
2814
+ icon: portable.icon ?? "Zap",
2815
+ iconColor: portable.iconColor ?? "#6366f1",
2816
+ enabled: true,
2817
+ ...portable.staggerDelayMs !== void 0 && { staggerDelayMs: portable.staggerDelayMs },
2818
+ nodes,
2819
+ edges: portable.edges
2820
+ };
2821
+ }
2822
+ var MACHINE_PATH = /(^|["'\s])(\/(Users|home)\/|[A-Za-z]:[\\/]|\\\\[^\\/\s]+[\\/])/;
2823
+ function residualAbsolutePaths(portable) {
2824
+ const found = [];
2825
+ for (const node of portable.nodes) {
2826
+ for (const [key, value] of Object.entries(node.config)) {
2827
+ if (typeof value === "string" && MACHINE_PATH.test(value)) {
2828
+ found.push(`${node.id}.${key}`);
2829
+ }
2830
+ }
2831
+ }
2832
+ return found;
2833
+ }
2834
+
2835
+ // src/tools/workflows.ts
2712
2836
  var launchAgentConfigSchema = z5.object({
2713
2837
  agentType: z5.enum(["claude", "copilot", "codex", "opencode", "gemini"]),
2714
2838
  projectName: V.name,
@@ -2825,7 +2949,8 @@ var nodeSchema = z5.object({
2825
2949
  "condition",
2826
2950
  "approval",
2827
2951
  "createTaskFromItem",
2828
- "callConnectorAction"
2952
+ "callConnectorAction",
2953
+ "loop"
2829
2954
  ]),
2830
2955
  label: V.shortText,
2831
2956
  // Referenced by typed step vars as `{{steps.<slug>.<field>}}`. Set one on any
@@ -2833,7 +2958,43 @@ var nodeSchema = z5.object({
2833
2958
  slug: V.shortText.optional(),
2834
2959
  config: z5.record(z5.string(), z5.unknown()),
2835
2960
  position: z5.object({ x: z5.number(), y: z5.number() })
2961
+ }).superRefine((node, ctx) => {
2962
+ if (node.type !== "loop") return;
2963
+ const config = node.config;
2964
+ if (!Array.isArray(config.bodyNodeIds) || config.bodyNodeIds.length === 0) {
2965
+ ctx.addIssue({
2966
+ code: "custom",
2967
+ path: ["config", "bodyNodeIds"],
2968
+ message: `loop "${node.id}" must list at least one body step in bodyNodeIds`
2969
+ });
2970
+ }
2971
+ const max = config.maxIterations;
2972
+ if (typeof max !== "number" || !Number.isInteger(max) || max < 1 || max > MAX_LOOP_ITERATIONS) {
2973
+ ctx.addIssue({
2974
+ code: "custom",
2975
+ path: ["config", "maxIterations"],
2976
+ message: `loop "${node.id}" needs maxIterations as a whole number from 1 to ${MAX_LOOP_ITERATIONS}`
2977
+ });
2978
+ }
2836
2979
  });
2980
+ var MAX_LOOP_ITERATIONS = 10;
2981
+ function validateLoopBodies(nodes) {
2982
+ const ids = new Set(nodes.map((n) => n.id));
2983
+ const errors = [];
2984
+ for (const node of nodes) {
2985
+ if (node.type !== "loop") continue;
2986
+ const body = node.config.bodyNodeIds ?? [];
2987
+ for (const id of body) {
2988
+ if (!ids.has(id)) {
2989
+ errors.push(`loop "${node.label || node.id}" references unknown body step "${id}"`);
2990
+ }
2991
+ }
2992
+ if (body.includes(node.id)) {
2993
+ errors.push(`loop "${node.label || node.id}" lists itself as a body step`);
2994
+ }
2995
+ }
2996
+ return errors;
2997
+ }
2837
2998
  var edgeSchema = z5.object({
2838
2999
  id: V.id,
2839
3000
  source: V.id,
@@ -2978,6 +3139,15 @@ function registerWorkflowTools(server) {
2978
3139
  if (args.nodes && args.edges) {
2979
3140
  nodes = args.nodes;
2980
3141
  edges = args.edges;
3142
+ const loopErrors = validateLoopBodies(
3143
+ nodes
3144
+ );
3145
+ if (loopErrors.length > 0) {
3146
+ return {
3147
+ content: [{ type: "text", text: `Error: ${loopErrors.join("; ")}` }],
3148
+ isError: true
3149
+ };
3150
+ }
2981
3151
  } else {
2982
3152
  const trigger = args.trigger ?? { triggerType: "manual" };
2983
3153
  const actions = args.actions ?? [];
@@ -3029,7 +3199,18 @@ function registerWorkflowTools(server) {
3029
3199
  }
3030
3200
  const updates = {};
3031
3201
  if (args.name !== void 0) updates.name = args.name;
3032
- if (args.nodes !== void 0) updates.nodes = args.nodes;
3202
+ if (args.nodes !== void 0) {
3203
+ const loopErrors = validateLoopBodies(
3204
+ args.nodes
3205
+ );
3206
+ if (loopErrors.length > 0) {
3207
+ return {
3208
+ content: [{ type: "text", text: `Error: ${loopErrors.join("; ")}` }],
3209
+ isError: true
3210
+ };
3211
+ }
3212
+ updates.nodes = args.nodes;
3213
+ }
3033
3214
  if (args.edges !== void 0) updates.edges = args.edges;
3034
3215
  if (args.icon !== void 0) updates.icon = args.icon;
3035
3216
  if (args.icon_color !== void 0) updates.iconColor = args.icon_color;
@@ -3191,6 +3372,164 @@ Run history: list_workflow_runs with workflow_id ${args.workflow_id}`
3191
3372
  };
3192
3373
  }
3193
3374
  );
3375
+ server.tool(
3376
+ "export_workflow",
3377
+ "Export a workflow as a portable file you can commit beside the code it drives. Absolute paths become {{project.path}} and the local remote-host binding is dropped, so it runs on another machine after import. Refuses a workflow bound to a connector connection, whose id means nothing elsewhere.",
3378
+ {
3379
+ workflow_id: V.id.optional().describe("Workflow ID (from list_workflows)"),
3380
+ id: V.id.optional().describe("Deprecated alias for workflow_id")
3381
+ },
3382
+ async (args) => {
3383
+ const resolved = resolveWorkflowId(args);
3384
+ if ("error" in resolved) {
3385
+ return { content: [{ type: "text", text: `Error: ${resolved.error}` }], isError: true };
3386
+ }
3387
+ const workflow = dbListWorkflows().find((w) => w.id === resolved.id);
3388
+ if (!workflow) {
3389
+ return {
3390
+ content: [{ type: "text", text: `Error: workflow "${resolved.id}" not found` }],
3391
+ isError: true
3392
+ };
3393
+ }
3394
+ const blockers = portabilityBlockers(workflow);
3395
+ if (blockers.length > 0) {
3396
+ return {
3397
+ content: [
3398
+ {
3399
+ type: "text",
3400
+ text: `Error: "${workflow.name}" cannot be exported portably because ` + blockers.join("; ") + ". Rebuild those steps without the connection, or keep this workflow local."
3401
+ }
3402
+ ],
3403
+ isError: true
3404
+ };
3405
+ }
3406
+ const projects = dbListProjects();
3407
+ const projectName = workflow.nodes.map((n) => n.config.projectName).find((name) => typeof name === "string" && name.length > 0);
3408
+ const project = projects.find((p) => p.name === projectName);
3409
+ if (!project) {
3410
+ return {
3411
+ content: [
3412
+ {
3413
+ type: "text",
3414
+ text: `Error: no project named "${projectName ?? "(none)"}" is registered, so this workflow's paths cannot be made relative to anything.`
3415
+ }
3416
+ ],
3417
+ isError: true
3418
+ };
3419
+ }
3420
+ const portable = toPortable(workflow, project.path);
3421
+ const residual = residualAbsolutePaths(portable);
3422
+ return {
3423
+ content: [
3424
+ {
3425
+ type: "text",
3426
+ text: JSON.stringify(portable, null, 2) + (residual.length > 0 ? `
3427
+
3428
+ Warning: these still hold a machine-specific path and will not travel: ${residual.join(", ")}` : "")
3429
+ }
3430
+ ]
3431
+ };
3432
+ }
3433
+ );
3434
+ server.tool(
3435
+ "import_workflow",
3436
+ "Import a workflow exported by export_workflow, resolving {{project.path}} and {{project.name}} against a registered project. The id is derived from the bundle and the workflow's slug, so importing the same file again updates it in place instead of creating a duplicate.",
3437
+ {
3438
+ workflow: z5.string().max(5e5).describe("The exported workflow JSON"),
3439
+ project_name: V.name.describe("Registered project to resolve paths against"),
3440
+ bundle: V.name.optional().describe("Namespace for the derived id (default: the project name)")
3441
+ },
3442
+ async (args) => {
3443
+ let parsed;
3444
+ try {
3445
+ parsed = JSON.parse(args.workflow);
3446
+ } catch (err) {
3447
+ return {
3448
+ content: [
3449
+ {
3450
+ type: "text",
3451
+ text: `Error: workflow is not valid JSON \u2014 ${String(err).slice(0, 200)}`
3452
+ }
3453
+ ],
3454
+ isError: true
3455
+ };
3456
+ }
3457
+ if (parsed?.version !== PORTABLE_FORMAT_VERSION) {
3458
+ return {
3459
+ content: [
3460
+ {
3461
+ type: "text",
3462
+ text: `Error: unsupported format version ${parsed?.version}; this build reads version ${PORTABLE_FORMAT_VERSION}`
3463
+ }
3464
+ ],
3465
+ isError: true
3466
+ };
3467
+ }
3468
+ if (!Array.isArray(parsed.nodes) || !Array.isArray(parsed.edges) || !parsed.name) {
3469
+ return {
3470
+ content: [{ type: "text", text: "Error: workflow is missing name, nodes or edges" }],
3471
+ isError: true
3472
+ };
3473
+ }
3474
+ const project = dbListProjects().find((p) => p.name === args.project_name);
3475
+ if (!project) {
3476
+ return {
3477
+ content: [
3478
+ {
3479
+ type: "text",
3480
+ text: `Error: no project named "${args.project_name}". Create it first so its path is known.`
3481
+ }
3482
+ ],
3483
+ isError: true
3484
+ };
3485
+ }
3486
+ const loopErrors = validateLoopBodies(
3487
+ parsed.nodes
3488
+ );
3489
+ if (loopErrors.length > 0) {
3490
+ return {
3491
+ content: [{ type: "text", text: `Error: ${loopErrors.join("; ")}` }],
3492
+ isError: true
3493
+ };
3494
+ }
3495
+ const bundle = args.bundle ?? slugify(project.name);
3496
+ const definition = fromPortable(
3497
+ { ...parsed, slug: parsed.slug ?? slugify(parsed.name) },
3498
+ bundle,
3499
+ {
3500
+ name: project.name,
3501
+ path: project.path
3502
+ }
3503
+ );
3504
+ const blockers = portabilityBlockers(definition);
3505
+ if (blockers.length > 0) {
3506
+ return {
3507
+ content: [
3508
+ {
3509
+ type: "text",
3510
+ text: `Error: this workflow cannot be imported because ${blockers.join("; ")}.`
3511
+ }
3512
+ ],
3513
+ isError: true
3514
+ };
3515
+ }
3516
+ const existing = dbListWorkflows().find((w) => w.id === definition.id);
3517
+ if (existing) {
3518
+ dbUpdateWorkflow(definition.id, definition);
3519
+ } else {
3520
+ dbInsertWorkflow(definition);
3521
+ }
3522
+ dbSignalChange();
3523
+ return {
3524
+ content: [
3525
+ {
3526
+ type: "text",
3527
+ text: `${existing ? "Updated" : "Imported"} "${definition.name}" as ${definition.id}, resolved against ${project.path}`
3528
+ }
3529
+ ]
3530
+ };
3531
+ }
3532
+ );
3194
3533
  }
3195
3534
 
3196
3535
  // src/tools/config.ts
@@ -3556,7 +3895,7 @@ console.warn = (...args) => _origError("[mcp:warn]", ...args);
3556
3895
  console.error = (...args) => _origError("[mcp:error]", ...args);
3557
3896
  async function main() {
3558
3897
  configManager.init();
3559
- const version = true ? "0.5.4" : createRequire(import.meta.url)("../package.json").version;
3898
+ const version = true ? "0.5.5" : createRequire(import.meta.url)("../package.json").version;
3560
3899
  const server = createMcpServer(version);
3561
3900
  const transport = new StdioServerTransport();
3562
3901
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vornrun/mcp",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "description": "Vorn MCP server — task management, git, and workflow tools for AI coding agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -38,8 +38,8 @@
38
38
  "zod": "^4.4.3"
39
39
  },
40
40
  "devDependencies": {
41
- "@vornrun/server": "0.5.4",
42
- "@vornrun/shared": "0.5.4",
41
+ "@vornrun/server": "0.5.5",
42
+ "@vornrun/shared": "0.5.5",
43
43
  "tsup": "^8.5.1",
44
44
  "tsx": "^4.23.1",
45
45
  "typescript": "^6.0.3"