@vornrun/mcp 0.5.3 → 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 +560 -36
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -50,28 +50,6 @@ var logger_default = log;
50
50
  import { execFileSync, execFile } from "child_process";
51
51
  import fs from "fs";
52
52
  import path from "path";
53
- function getUserShellEnv() {
54
- if (process.platform === "win32") return { ...process.env };
55
- try {
56
- const shell = process.env.SHELL || "/bin/zsh";
57
- const output = execFileSync(shell, ["-ilc", "env"], {
58
- encoding: "utf-8",
59
- timeout: 5e3,
60
- stdio: ["pipe", "pipe", "pipe"]
61
- });
62
- const env = {};
63
- for (const line of output.split("\n")) {
64
- const idx = line.indexOf("=");
65
- if (idx > 0) {
66
- env[line.substring(0, idx)] = line.substring(idx + 1);
67
- }
68
- }
69
- return env;
70
- } catch {
71
- return { ...process.env };
72
- }
73
- }
74
- var resolvedEnv = getUserShellEnv();
75
53
  function getDefaultShell(configured) {
76
54
  const chosen = configured?.trim();
77
55
  if (chosen) return chosen;
@@ -95,6 +73,8 @@ function findWindowsShell() {
95
73
  if (fs.existsSync(windowsPowerShell)) return windowsPowerShell;
96
74
  return process.env.COMSPEC || "cmd.exe";
97
75
  }
76
+ var STRIP_ENV_KEYS = ["CLAUDECODE"];
77
+ var STRIP_ENV_KEYS_UPPER = STRIP_ENV_KEYS.map((k) => k.toUpperCase());
98
78
 
99
79
  // ../shared/src/types.ts
100
80
  var DEFAULT_WORKSPACE = {
@@ -435,6 +415,9 @@ function createSchema() {
435
415
  project_path TEXT,
436
416
  approved_at TEXT,
437
417
  diagnostics TEXT,
418
+ output TEXT,
419
+ structured_output TEXT,
420
+ iteration INTEGER,
438
421
  FOREIGN KEY (run_id) REFERENCES workflow_runs(id) ON DELETE CASCADE
439
422
  );
440
423
 
@@ -914,7 +897,19 @@ function verifySchema(d) {
914
897
  {
915
898
  column: "diagnostics",
916
899
  ddl: "ALTER TABLE workflow_run_nodes ADD COLUMN diagnostics TEXT"
917
- }
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" }
918
913
  ],
919
914
  tasks: [
920
915
  {
@@ -994,6 +989,15 @@ function loadDefaults(d) {
994
989
  hasSeenOnboarding: map.hasSeenOnboarding
995
990
  },
996
991
  ...map.reopenSessions !== void 0 && { reopenSessions: map.reopenSessions },
992
+ // Saving iterates over every key in defaults, but loading is this explicit
993
+ // list — so a key missing here round-trips to nothing and its feature is
994
+ // silently inert.
995
+ // Array-checked rather than cast: the value came from JSON.parse of a row a
996
+ // user can edit, and broadcasting a non-array under a string[] type would
997
+ // break any consumer that trusts the declaration.
998
+ ...Array.isArray(map.envPassthrough) && {
999
+ envPassthrough: map.envPassthrough.filter((k) => typeof k === "string")
1000
+ },
997
1001
  // Terminal block rendering. Default on; the key only appears once the
998
1002
  // user has toggled it, so absence means "not yet decided", not "off".
999
1003
  domBlockRendering: map.domBlockRendering ?? true,
@@ -1565,6 +1569,7 @@ function rowToWorkspace(r) {
1565
1569
  };
1566
1570
  }
1567
1571
  function mapNodeRow(n) {
1572
+ const structured = n.structured_output != null ? parseStructuredOutput(n.structured_output) : void 0;
1568
1573
  return {
1569
1574
  nodeId: n.node_id,
1570
1575
  status: n.status,
@@ -1579,9 +1584,20 @@ function mapNodeRow(n) {
1579
1584
  ...n.project_name != null && { projectName: n.project_name },
1580
1585
  ...n.project_path != null && { projectPath: n.project_path },
1581
1586
  ...n.approved_at != null && { approvedAt: n.approved_at },
1582
- ...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 }
1583
1591
  };
1584
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
+ }
1585
1601
  function fetchNodesByRunIds(d, runIds) {
1586
1602
  if (runIds.length === 0) return /* @__PURE__ */ new Map();
1587
1603
  const placeholders = runIds.map(() => "?").join(",");
@@ -2720,6 +2736,103 @@ function registerSessionTools(server) {
2720
2736
  // src/tools/workflows.ts
2721
2737
  import crypto2 from "crypto";
2722
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
2723
2836
  var launchAgentConfigSchema = z5.object({
2724
2837
  agentType: z5.enum(["claude", "copilot", "codex", "opencode", "gemini"]),
2725
2838
  projectName: V.name,
@@ -2748,10 +2861,68 @@ var launchAgentConfigSchema = z5.object({
2748
2861
  message: "outputSchema requires headless: true",
2749
2862
  path: ["outputSchema"]
2750
2863
  });
2864
+ var workflowInputDefSchema = z5.object({
2865
+ key: z5.string().regex(
2866
+ /^[A-Za-z_][A-Za-z0-9_]*$/,
2867
+ "key must be a valid identifier \u2014 it becomes {{inputs.<key>}}"
2868
+ ).max(100),
2869
+ label: V.shortText,
2870
+ type: z5.enum(["text", "textarea", "number", "select", "boolean", "project", "branch"]),
2871
+ required: z5.boolean().optional(),
2872
+ defaultValue: V.shortText.optional(),
2873
+ options: z5.array(z5.object({ value: V.shortText, label: V.shortText })).optional(),
2874
+ placeholder: V.shortText.optional(),
2875
+ description: V.shortText.optional()
2876
+ }).superRefine((def, ctx) => {
2877
+ const options = def.options ?? [];
2878
+ if (def.type === "select" && options.length === 0) {
2879
+ ctx.addIssue({
2880
+ code: "custom",
2881
+ path: ["options"],
2882
+ message: `select input "${def.key}" declares no options, so the run dialog could offer nothing`
2883
+ });
2884
+ }
2885
+ if (def.defaultValue === void 0) return;
2886
+ if (def.type === "number" && !Number.isFinite(Number(def.defaultValue))) {
2887
+ ctx.addIssue({
2888
+ code: "custom",
2889
+ path: ["defaultValue"],
2890
+ message: `default "${def.defaultValue}" for number input "${def.key}" is not a finite number`
2891
+ });
2892
+ }
2893
+ if (def.type === "boolean" && !["true", "false"].includes(def.defaultValue)) {
2894
+ ctx.addIssue({
2895
+ code: "custom",
2896
+ path: ["defaultValue"],
2897
+ message: `default "${def.defaultValue}" for boolean input "${def.key}" must be "true" or "false"`
2898
+ });
2899
+ }
2900
+ if (def.type === "select" && options.length > 0 && !options.some((o) => o.value === def.defaultValue)) {
2901
+ ctx.addIssue({
2902
+ code: "custom",
2903
+ path: ["defaultValue"],
2904
+ message: `default "${def.defaultValue}" for select input "${def.key}" is not one of its options`
2905
+ });
2906
+ }
2907
+ });
2908
+ var workflowInputsSchema = z5.array(workflowInputDefSchema).superRefine((inputs, ctx) => {
2909
+ const seen = /* @__PURE__ */ new Set();
2910
+ inputs.forEach((def, index) => {
2911
+ if (seen.has(def.key)) {
2912
+ ctx.addIssue({
2913
+ code: "custom",
2914
+ path: [index, "key"],
2915
+ message: `duplicate input key "${def.key}" \u2014 only one value can survive under {{inputs.${def.key}}}`
2916
+ });
2917
+ }
2918
+ seen.add(def.key);
2919
+ });
2920
+ });
2751
2921
  var triggerConfigSchema = z5.union([
2752
2922
  z5.object({
2753
2923
  triggerType: z5.literal("manual"),
2754
- contextual: z5.boolean().optional()
2924
+ contextual: z5.boolean().optional(),
2925
+ inputs: workflowInputsSchema.optional()
2755
2926
  }),
2756
2927
  z5.object({ triggerType: z5.literal("once"), runAt: V.shortText }),
2757
2928
  z5.object({
@@ -2778,7 +2949,8 @@ var nodeSchema = z5.object({
2778
2949
  "condition",
2779
2950
  "approval",
2780
2951
  "createTaskFromItem",
2781
- "callConnectorAction"
2952
+ "callConnectorAction",
2953
+ "loop"
2782
2954
  ]),
2783
2955
  label: V.shortText,
2784
2956
  // Referenced by typed step vars as `{{steps.<slug>.<field>}}`. Set one on any
@@ -2786,7 +2958,43 @@ var nodeSchema = z5.object({
2786
2958
  slug: V.shortText.optional(),
2787
2959
  config: z5.record(z5.string(), z5.unknown()),
2788
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
+ }
2789
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
+ }
2790
2998
  var edgeSchema = z5.object({
2791
2999
  id: V.id,
2792
3000
  source: V.id,
@@ -2827,6 +3035,75 @@ function buildGraphFromFlat(trigger, actions) {
2827
3035
  }
2828
3036
  return { nodes, edges };
2829
3037
  }
3038
+ function resolveWorkflowInputs(defs, supplied) {
3039
+ const errors = [];
3040
+ const known = new Set(defs.map((d) => d.key));
3041
+ for (const key of Object.keys(supplied)) {
3042
+ if (!known.has(key)) {
3043
+ errors.push(
3044
+ `unknown input "${key}" \u2014 this workflow declares: ${Array.from(known).join(", ") || "(none)"}`
3045
+ );
3046
+ }
3047
+ }
3048
+ const values = {};
3049
+ for (const def of defs) {
3050
+ const provided = Object.prototype.hasOwnProperty.call(supplied, def.key);
3051
+ const raw = provided ? supplied[def.key] : def.defaultValue;
3052
+ if (def.type === "boolean") {
3053
+ if (raw === void 0) {
3054
+ values[def.key] = false;
3055
+ } else if (typeof raw === "boolean") {
3056
+ values[def.key] = raw;
3057
+ } else if (raw === "true" || raw === "false") {
3058
+ values[def.key] = raw === "true";
3059
+ } else {
3060
+ errors.push(`input "${def.key}" must be a boolean, got ${JSON.stringify(raw)}`);
3061
+ }
3062
+ continue;
3063
+ }
3064
+ if (raw === void 0 || typeof raw === "string" && raw.trim() === "") {
3065
+ if (def.required) errors.push(`missing required input "${def.key}" (${def.label})`);
3066
+ continue;
3067
+ }
3068
+ switch (def.type) {
3069
+ case "number": {
3070
+ if (typeof raw === "boolean") {
3071
+ errors.push(`input "${def.key}" must be a number, got ${JSON.stringify(raw)}`);
3072
+ break;
3073
+ }
3074
+ const n = typeof raw === "number" ? raw : Number(String(raw).trim());
3075
+ if (!Number.isFinite(n)) {
3076
+ errors.push(`input "${def.key}" must be a finite number, got ${JSON.stringify(raw)}`);
3077
+ } else {
3078
+ values[def.key] = n;
3079
+ }
3080
+ break;
3081
+ }
3082
+ case "select": {
3083
+ const allowed = (def.options ?? []).map((o) => o.value);
3084
+ if (allowed.length === 0) {
3085
+ errors.push(`input "${def.key}" is a select but declares no options`);
3086
+ } else if (!allowed.includes(String(raw))) {
3087
+ errors.push(`input "${def.key}" must be one of: ${allowed.join(", ")}`);
3088
+ } else {
3089
+ values[def.key] = String(raw);
3090
+ }
3091
+ break;
3092
+ }
3093
+ default:
3094
+ values[def.key] = String(raw);
3095
+ }
3096
+ }
3097
+ return { values, errors };
3098
+ }
3099
+ function resolveWorkflowId(args) {
3100
+ const id = args.workflow_id ?? args.id;
3101
+ if (!id) return { error: "provide workflow_id" };
3102
+ if (args.workflow_id && args.id && args.workflow_id !== args.id) {
3103
+ return { error: "workflow_id and id disagree \u2014 pass only workflow_id" };
3104
+ }
3105
+ return { id };
3106
+ }
2830
3107
  function registerWorkflowTools(server) {
2831
3108
  server.tool(
2832
3109
  "list_workflows",
@@ -2862,6 +3139,15 @@ function registerWorkflowTools(server) {
2862
3139
  if (args.nodes && args.edges) {
2863
3140
  nodes = args.nodes;
2864
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
+ }
2865
3151
  } else {
2866
3152
  const trigger = args.trigger ?? { triggerType: "manual" };
2867
3153
  const actions = args.actions ?? [];
@@ -2888,7 +3174,8 @@ function registerWorkflowTools(server) {
2888
3174
  "update_workflow",
2889
3175
  "Update a workflow's properties",
2890
3176
  {
2891
- id: V.id.describe("Workflow ID"),
3177
+ workflow_id: V.id.optional().describe("Workflow ID (from list_workflows)"),
3178
+ id: V.id.optional().describe("Deprecated alias for workflow_id"),
2892
3179
  name: V.title.optional(),
2893
3180
  nodes: z5.array(nodeSchema).optional(),
2894
3181
  edges: z5.array(edgeSchema).optional(),
@@ -2898,23 +3185,38 @@ function registerWorkflowTools(server) {
2898
3185
  stagger_delay_ms: z5.number().optional()
2899
3186
  },
2900
3187
  async (args) => {
3188
+ const resolved = resolveWorkflowId(args);
3189
+ if ("error" in resolved) {
3190
+ return { content: [{ type: "text", text: `Error: ${resolved.error}` }], isError: true };
3191
+ }
2901
3192
  const workflows = dbListWorkflows();
2902
- const workflow = workflows.find((w) => w.id === args.id);
3193
+ const workflow = workflows.find((w) => w.id === resolved.id);
2903
3194
  if (!workflow) {
2904
3195
  return {
2905
- content: [{ type: "text", text: `Error: workflow "${args.id}" not found` }],
3196
+ content: [{ type: "text", text: `Error: workflow "${resolved.id}" not found` }],
2906
3197
  isError: true
2907
3198
  };
2908
3199
  }
2909
3200
  const updates = {};
2910
3201
  if (args.name !== void 0) updates.name = args.name;
2911
- 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
+ }
2912
3214
  if (args.edges !== void 0) updates.edges = args.edges;
2913
3215
  if (args.icon !== void 0) updates.icon = args.icon;
2914
3216
  if (args.icon_color !== void 0) updates.iconColor = args.icon_color;
2915
3217
  if (args.enabled !== void 0) updates.enabled = args.enabled;
2916
3218
  if (args.stagger_delay_ms !== void 0) updates.staggerDelayMs = args.stagger_delay_ms;
2917
- dbUpdateWorkflow(args.id, updates);
3219
+ dbUpdateWorkflow(resolved.id, updates);
2918
3220
  dbSignalChange();
2919
3221
  return {
2920
3222
  content: [{ type: "text", text: JSON.stringify({ ...workflow, ...updates }, null, 2) }]
@@ -2924,17 +3226,24 @@ function registerWorkflowTools(server) {
2924
3226
  server.tool(
2925
3227
  "delete_workflow",
2926
3228
  "Delete a workflow",
2927
- { id: V.id.describe("Workflow ID") },
3229
+ {
3230
+ workflow_id: V.id.optional().describe("Workflow ID (from list_workflows)"),
3231
+ id: V.id.optional().describe("Deprecated alias for workflow_id")
3232
+ },
2928
3233
  async (args) => {
3234
+ const resolved = resolveWorkflowId(args);
3235
+ if ("error" in resolved) {
3236
+ return { content: [{ type: "text", text: `Error: ${resolved.error}` }], isError: true };
3237
+ }
2929
3238
  const workflows = dbListWorkflows();
2930
- const workflow = workflows.find((w) => w.id === args.id);
3239
+ const workflow = workflows.find((w) => w.id === resolved.id);
2931
3240
  if (!workflow) {
2932
3241
  return {
2933
- content: [{ type: "text", text: `Error: workflow "${args.id}" not found` }],
3242
+ content: [{ type: "text", text: `Error: workflow "${resolved.id}" not found` }],
2934
3243
  isError: true
2935
3244
  };
2936
3245
  }
2937
- dbDeleteWorkflow(args.id);
3246
+ dbDeleteWorkflow(resolved.id);
2938
3247
  dbSignalChange();
2939
3248
  return { content: [{ type: "text", text: `Deleted workflow: ${workflow.name}` }] };
2940
3249
  }
@@ -3006,6 +3315,221 @@ function registerWorkflowTools(server) {
3006
3315
  }
3007
3316
  }
3008
3317
  );
3318
+ server.tool(
3319
+ "execute_workflow",
3320
+ "Run a workflow now, as if triggered manually. Supply values for any parameters the workflow declares (see the trigger node's inputs); declared defaults fill in anything omitted. Requires the Vorn app to be running. Returns as soon as the run is queued \u2014 poll list_workflow_runs for the outcome.",
3321
+ {
3322
+ workflow_id: V.id.describe("Workflow ID (from list_workflows)"),
3323
+ inputs: z5.record(z5.string(), z5.union([z5.string(), z5.number(), z5.boolean()])).optional().describe("Values for the declared parameters, keyed by input key ({{inputs.<key>}})")
3324
+ },
3325
+ async (args) => {
3326
+ const workflow = dbListWorkflows().find((w) => w.id === args.workflow_id);
3327
+ if (!workflow) {
3328
+ return {
3329
+ content: [{ type: "text", text: `Error: workflow "${args.workflow_id}" not found` }],
3330
+ isError: true
3331
+ };
3332
+ }
3333
+ const trigger = workflow.nodes.find((n) => n.type === "trigger")?.config;
3334
+ const defs = trigger?.triggerType === "manual" ? trigger.inputs ?? [] : [];
3335
+ const supplied = args.inputs ?? {};
3336
+ let inputs;
3337
+ if (defs.length > 0) {
3338
+ const { values, errors } = resolveWorkflowInputs(defs, supplied);
3339
+ if (errors.length > 0) {
3340
+ return {
3341
+ content: [
3342
+ { type: "text", text: `Error: invalid inputs
3343
+ - ${errors.join("\n - ")}` }
3344
+ ],
3345
+ isError: true
3346
+ };
3347
+ }
3348
+ inputs = values;
3349
+ } else if (Object.keys(supplied).length > 0) {
3350
+ inputs = supplied;
3351
+ }
3352
+ try {
3353
+ await rpcCall("workflow:runManual", { workflowId: args.workflow_id, inputs });
3354
+ } catch (err) {
3355
+ return {
3356
+ content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : err}` }],
3357
+ isError: true
3358
+ };
3359
+ }
3360
+ const disabled = workflow.enabled === false ? " (workflow is disabled; manual runs still execute)" : "";
3361
+ const shown = inputs ? `
3362
+ inputs: ${JSON.stringify(inputs, null, 2)}` : "\nno inputs";
3363
+ return {
3364
+ content: [
3365
+ {
3366
+ type: "text",
3367
+ text: `Queued "${workflow.name}"${disabled}${shown}
3368
+
3369
+ Run history: list_workflow_runs with workflow_id ${args.workflow_id}`
3370
+ }
3371
+ ]
3372
+ };
3373
+ }
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
+ );
3009
3533
  }
3010
3534
 
3011
3535
  // src/tools/config.ts
@@ -3371,7 +3895,7 @@ console.warn = (...args) => _origError("[mcp:warn]", ...args);
3371
3895
  console.error = (...args) => _origError("[mcp:error]", ...args);
3372
3896
  async function main() {
3373
3897
  configManager.init();
3374
- const version = true ? "0.5.3" : createRequire(import.meta.url)("../package.json").version;
3898
+ const version = true ? "0.5.5" : createRequire(import.meta.url)("../package.json").version;
3375
3899
  const server = createMcpServer(version);
3376
3900
  const transport = new StdioServerTransport();
3377
3901
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vornrun/mcp",
3
- "version": "0.5.3",
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.3",
42
- "@vornrun/shared": "0.5.3",
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"