@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.
- package/dist/index.js +560 -36
- 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
|
-
|
|
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 ===
|
|
3193
|
+
const workflow = workflows.find((w) => w.id === resolved.id);
|
|
2903
3194
|
if (!workflow) {
|
|
2904
3195
|
return {
|
|
2905
|
-
content: [{ type: "text", text: `Error: workflow "${
|
|
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)
|
|
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(
|
|
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
|
-
{
|
|
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 ===
|
|
3239
|
+
const workflow = workflows.find((w) => w.id === resolved.id);
|
|
2931
3240
|
if (!workflow) {
|
|
2932
3241
|
return {
|
|
2933
|
-
content: [{ type: "text", text: `Error: workflow "${
|
|
3242
|
+
content: [{ type: "text", text: `Error: workflow "${resolved.id}" not found` }],
|
|
2934
3243
|
isError: true
|
|
2935
3244
|
};
|
|
2936
3245
|
}
|
|
2937
|
-
dbDeleteWorkflow(
|
|
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.
|
|
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
|
+
"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.
|
|
42
|
-
"@vornrun/shared": "0.5.
|
|
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"
|