@vornrun/mcp 0.5.2 → 0.5.4
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 +473 -35
- 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 = {
|
|
@@ -107,6 +87,15 @@ var DEFAULT_WORKSPACE = {
|
|
|
107
87
|
function isTerminalTaskStatus(status) {
|
|
108
88
|
return status === "done" || status === "cancelled";
|
|
109
89
|
}
|
|
90
|
+
var SDK_FILTER_KEYS = {
|
|
91
|
+
connectorId: "sdkConnectorId",
|
|
92
|
+
version: "sdkVersion",
|
|
93
|
+
icon: "sdkIcon"
|
|
94
|
+
};
|
|
95
|
+
function connectionConnectorId(connection) {
|
|
96
|
+
const packaged = connection.filters?.[SDK_FILTER_KEYS.connectorId];
|
|
97
|
+
return typeof packaged === "string" && packaged !== "" ? packaged : connection.connectorId;
|
|
98
|
+
}
|
|
110
99
|
|
|
111
100
|
// ../server/src/default-workflows.ts
|
|
112
101
|
var DEFAULT_TASK_WORKFLOW_ID = "system:default-task-workflow";
|
|
@@ -985,6 +974,15 @@ function loadDefaults(d) {
|
|
|
985
974
|
hasSeenOnboarding: map.hasSeenOnboarding
|
|
986
975
|
},
|
|
987
976
|
...map.reopenSessions !== void 0 && { reopenSessions: map.reopenSessions },
|
|
977
|
+
// Saving iterates over every key in defaults, but loading is this explicit
|
|
978
|
+
// list — so a key missing here round-trips to nothing and its feature is
|
|
979
|
+
// silently inert.
|
|
980
|
+
// Array-checked rather than cast: the value came from JSON.parse of a row a
|
|
981
|
+
// user can edit, and broadcasting a non-array under a string[] type would
|
|
982
|
+
// break any consumer that trusts the declaration.
|
|
983
|
+
...Array.isArray(map.envPassthrough) && {
|
|
984
|
+
envPassthrough: map.envPassthrough.filter((k) => typeof k === "string")
|
|
985
|
+
},
|
|
988
986
|
// Terminal block rendering. Default on; the key only appears once the
|
|
989
987
|
// user has toggled it, so absence means "not yet decided", not "off".
|
|
990
988
|
domBlockRendering: map.domBlockRendering ?? true,
|
|
@@ -2308,7 +2306,7 @@ function readPort() {
|
|
|
2308
2306
|
return discoverAndHeal();
|
|
2309
2307
|
}
|
|
2310
2308
|
}
|
|
2311
|
-
async function rpcCall(method, params) {
|
|
2309
|
+
async function rpcCall(method, params, timeoutMs = TIMEOUT_MS) {
|
|
2312
2310
|
const result = readPort();
|
|
2313
2311
|
if (!result.port) {
|
|
2314
2312
|
throw new Error(result.reason === "invalid" ? PORT_FILE_INVALID_MSG : PORT_FILE_MISSING_MSG);
|
|
@@ -2318,8 +2316,8 @@ async function rpcCall(method, params) {
|
|
|
2318
2316
|
const id = ++rpcId;
|
|
2319
2317
|
const timer = setTimeout(() => {
|
|
2320
2318
|
ws.close();
|
|
2321
|
-
reject(new Error(`RPC call "${method}" timed out after ${
|
|
2322
|
-
},
|
|
2319
|
+
reject(new Error(`RPC call "${method}" timed out after ${timeoutMs}ms`));
|
|
2320
|
+
}, timeoutMs);
|
|
2323
2321
|
ws.on("open", () => {
|
|
2324
2322
|
ws.send(JSON.stringify({ jsonrpc: "2.0", id, method, params }));
|
|
2325
2323
|
});
|
|
@@ -2739,10 +2737,68 @@ var launchAgentConfigSchema = z5.object({
|
|
|
2739
2737
|
message: "outputSchema requires headless: true",
|
|
2740
2738
|
path: ["outputSchema"]
|
|
2741
2739
|
});
|
|
2740
|
+
var workflowInputDefSchema = z5.object({
|
|
2741
|
+
key: z5.string().regex(
|
|
2742
|
+
/^[A-Za-z_][A-Za-z0-9_]*$/,
|
|
2743
|
+
"key must be a valid identifier \u2014 it becomes {{inputs.<key>}}"
|
|
2744
|
+
).max(100),
|
|
2745
|
+
label: V.shortText,
|
|
2746
|
+
type: z5.enum(["text", "textarea", "number", "select", "boolean", "project", "branch"]),
|
|
2747
|
+
required: z5.boolean().optional(),
|
|
2748
|
+
defaultValue: V.shortText.optional(),
|
|
2749
|
+
options: z5.array(z5.object({ value: V.shortText, label: V.shortText })).optional(),
|
|
2750
|
+
placeholder: V.shortText.optional(),
|
|
2751
|
+
description: V.shortText.optional()
|
|
2752
|
+
}).superRefine((def, ctx) => {
|
|
2753
|
+
const options = def.options ?? [];
|
|
2754
|
+
if (def.type === "select" && options.length === 0) {
|
|
2755
|
+
ctx.addIssue({
|
|
2756
|
+
code: "custom",
|
|
2757
|
+
path: ["options"],
|
|
2758
|
+
message: `select input "${def.key}" declares no options, so the run dialog could offer nothing`
|
|
2759
|
+
});
|
|
2760
|
+
}
|
|
2761
|
+
if (def.defaultValue === void 0) return;
|
|
2762
|
+
if (def.type === "number" && !Number.isFinite(Number(def.defaultValue))) {
|
|
2763
|
+
ctx.addIssue({
|
|
2764
|
+
code: "custom",
|
|
2765
|
+
path: ["defaultValue"],
|
|
2766
|
+
message: `default "${def.defaultValue}" for number input "${def.key}" is not a finite number`
|
|
2767
|
+
});
|
|
2768
|
+
}
|
|
2769
|
+
if (def.type === "boolean" && !["true", "false"].includes(def.defaultValue)) {
|
|
2770
|
+
ctx.addIssue({
|
|
2771
|
+
code: "custom",
|
|
2772
|
+
path: ["defaultValue"],
|
|
2773
|
+
message: `default "${def.defaultValue}" for boolean input "${def.key}" must be "true" or "false"`
|
|
2774
|
+
});
|
|
2775
|
+
}
|
|
2776
|
+
if (def.type === "select" && options.length > 0 && !options.some((o) => o.value === def.defaultValue)) {
|
|
2777
|
+
ctx.addIssue({
|
|
2778
|
+
code: "custom",
|
|
2779
|
+
path: ["defaultValue"],
|
|
2780
|
+
message: `default "${def.defaultValue}" for select input "${def.key}" is not one of its options`
|
|
2781
|
+
});
|
|
2782
|
+
}
|
|
2783
|
+
});
|
|
2784
|
+
var workflowInputsSchema = z5.array(workflowInputDefSchema).superRefine((inputs, ctx) => {
|
|
2785
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2786
|
+
inputs.forEach((def, index) => {
|
|
2787
|
+
if (seen.has(def.key)) {
|
|
2788
|
+
ctx.addIssue({
|
|
2789
|
+
code: "custom",
|
|
2790
|
+
path: [index, "key"],
|
|
2791
|
+
message: `duplicate input key "${def.key}" \u2014 only one value can survive under {{inputs.${def.key}}}`
|
|
2792
|
+
});
|
|
2793
|
+
}
|
|
2794
|
+
seen.add(def.key);
|
|
2795
|
+
});
|
|
2796
|
+
});
|
|
2742
2797
|
var triggerConfigSchema = z5.union([
|
|
2743
2798
|
z5.object({
|
|
2744
2799
|
triggerType: z5.literal("manual"),
|
|
2745
|
-
contextual: z5.boolean().optional()
|
|
2800
|
+
contextual: z5.boolean().optional(),
|
|
2801
|
+
inputs: workflowInputsSchema.optional()
|
|
2746
2802
|
}),
|
|
2747
2803
|
z5.object({ triggerType: z5.literal("once"), runAt: V.shortText }),
|
|
2748
2804
|
z5.object({
|
|
@@ -2818,6 +2874,75 @@ function buildGraphFromFlat(trigger, actions) {
|
|
|
2818
2874
|
}
|
|
2819
2875
|
return { nodes, edges };
|
|
2820
2876
|
}
|
|
2877
|
+
function resolveWorkflowInputs(defs, supplied) {
|
|
2878
|
+
const errors = [];
|
|
2879
|
+
const known = new Set(defs.map((d) => d.key));
|
|
2880
|
+
for (const key of Object.keys(supplied)) {
|
|
2881
|
+
if (!known.has(key)) {
|
|
2882
|
+
errors.push(
|
|
2883
|
+
`unknown input "${key}" \u2014 this workflow declares: ${Array.from(known).join(", ") || "(none)"}`
|
|
2884
|
+
);
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
const values = {};
|
|
2888
|
+
for (const def of defs) {
|
|
2889
|
+
const provided = Object.prototype.hasOwnProperty.call(supplied, def.key);
|
|
2890
|
+
const raw = provided ? supplied[def.key] : def.defaultValue;
|
|
2891
|
+
if (def.type === "boolean") {
|
|
2892
|
+
if (raw === void 0) {
|
|
2893
|
+
values[def.key] = false;
|
|
2894
|
+
} else if (typeof raw === "boolean") {
|
|
2895
|
+
values[def.key] = raw;
|
|
2896
|
+
} else if (raw === "true" || raw === "false") {
|
|
2897
|
+
values[def.key] = raw === "true";
|
|
2898
|
+
} else {
|
|
2899
|
+
errors.push(`input "${def.key}" must be a boolean, got ${JSON.stringify(raw)}`);
|
|
2900
|
+
}
|
|
2901
|
+
continue;
|
|
2902
|
+
}
|
|
2903
|
+
if (raw === void 0 || typeof raw === "string" && raw.trim() === "") {
|
|
2904
|
+
if (def.required) errors.push(`missing required input "${def.key}" (${def.label})`);
|
|
2905
|
+
continue;
|
|
2906
|
+
}
|
|
2907
|
+
switch (def.type) {
|
|
2908
|
+
case "number": {
|
|
2909
|
+
if (typeof raw === "boolean") {
|
|
2910
|
+
errors.push(`input "${def.key}" must be a number, got ${JSON.stringify(raw)}`);
|
|
2911
|
+
break;
|
|
2912
|
+
}
|
|
2913
|
+
const n = typeof raw === "number" ? raw : Number(String(raw).trim());
|
|
2914
|
+
if (!Number.isFinite(n)) {
|
|
2915
|
+
errors.push(`input "${def.key}" must be a finite number, got ${JSON.stringify(raw)}`);
|
|
2916
|
+
} else {
|
|
2917
|
+
values[def.key] = n;
|
|
2918
|
+
}
|
|
2919
|
+
break;
|
|
2920
|
+
}
|
|
2921
|
+
case "select": {
|
|
2922
|
+
const allowed = (def.options ?? []).map((o) => o.value);
|
|
2923
|
+
if (allowed.length === 0) {
|
|
2924
|
+
errors.push(`input "${def.key}" is a select but declares no options`);
|
|
2925
|
+
} else if (!allowed.includes(String(raw))) {
|
|
2926
|
+
errors.push(`input "${def.key}" must be one of: ${allowed.join(", ")}`);
|
|
2927
|
+
} else {
|
|
2928
|
+
values[def.key] = String(raw);
|
|
2929
|
+
}
|
|
2930
|
+
break;
|
|
2931
|
+
}
|
|
2932
|
+
default:
|
|
2933
|
+
values[def.key] = String(raw);
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
return { values, errors };
|
|
2937
|
+
}
|
|
2938
|
+
function resolveWorkflowId(args) {
|
|
2939
|
+
const id = args.workflow_id ?? args.id;
|
|
2940
|
+
if (!id) return { error: "provide workflow_id" };
|
|
2941
|
+
if (args.workflow_id && args.id && args.workflow_id !== args.id) {
|
|
2942
|
+
return { error: "workflow_id and id disagree \u2014 pass only workflow_id" };
|
|
2943
|
+
}
|
|
2944
|
+
return { id };
|
|
2945
|
+
}
|
|
2821
2946
|
function registerWorkflowTools(server) {
|
|
2822
2947
|
server.tool(
|
|
2823
2948
|
"list_workflows",
|
|
@@ -2879,7 +3004,8 @@ function registerWorkflowTools(server) {
|
|
|
2879
3004
|
"update_workflow",
|
|
2880
3005
|
"Update a workflow's properties",
|
|
2881
3006
|
{
|
|
2882
|
-
|
|
3007
|
+
workflow_id: V.id.optional().describe("Workflow ID (from list_workflows)"),
|
|
3008
|
+
id: V.id.optional().describe("Deprecated alias for workflow_id"),
|
|
2883
3009
|
name: V.title.optional(),
|
|
2884
3010
|
nodes: z5.array(nodeSchema).optional(),
|
|
2885
3011
|
edges: z5.array(edgeSchema).optional(),
|
|
@@ -2889,11 +3015,15 @@ function registerWorkflowTools(server) {
|
|
|
2889
3015
|
stagger_delay_ms: z5.number().optional()
|
|
2890
3016
|
},
|
|
2891
3017
|
async (args) => {
|
|
3018
|
+
const resolved = resolveWorkflowId(args);
|
|
3019
|
+
if ("error" in resolved) {
|
|
3020
|
+
return { content: [{ type: "text", text: `Error: ${resolved.error}` }], isError: true };
|
|
3021
|
+
}
|
|
2892
3022
|
const workflows = dbListWorkflows();
|
|
2893
|
-
const workflow = workflows.find((w) => w.id ===
|
|
3023
|
+
const workflow = workflows.find((w) => w.id === resolved.id);
|
|
2894
3024
|
if (!workflow) {
|
|
2895
3025
|
return {
|
|
2896
|
-
content: [{ type: "text", text: `Error: workflow "${
|
|
3026
|
+
content: [{ type: "text", text: `Error: workflow "${resolved.id}" not found` }],
|
|
2897
3027
|
isError: true
|
|
2898
3028
|
};
|
|
2899
3029
|
}
|
|
@@ -2905,7 +3035,7 @@ function registerWorkflowTools(server) {
|
|
|
2905
3035
|
if (args.icon_color !== void 0) updates.iconColor = args.icon_color;
|
|
2906
3036
|
if (args.enabled !== void 0) updates.enabled = args.enabled;
|
|
2907
3037
|
if (args.stagger_delay_ms !== void 0) updates.staggerDelayMs = args.stagger_delay_ms;
|
|
2908
|
-
dbUpdateWorkflow(
|
|
3038
|
+
dbUpdateWorkflow(resolved.id, updates);
|
|
2909
3039
|
dbSignalChange();
|
|
2910
3040
|
return {
|
|
2911
3041
|
content: [{ type: "text", text: JSON.stringify({ ...workflow, ...updates }, null, 2) }]
|
|
@@ -2915,17 +3045,24 @@ function registerWorkflowTools(server) {
|
|
|
2915
3045
|
server.tool(
|
|
2916
3046
|
"delete_workflow",
|
|
2917
3047
|
"Delete a workflow",
|
|
2918
|
-
{
|
|
3048
|
+
{
|
|
3049
|
+
workflow_id: V.id.optional().describe("Workflow ID (from list_workflows)"),
|
|
3050
|
+
id: V.id.optional().describe("Deprecated alias for workflow_id")
|
|
3051
|
+
},
|
|
2919
3052
|
async (args) => {
|
|
3053
|
+
const resolved = resolveWorkflowId(args);
|
|
3054
|
+
if ("error" in resolved) {
|
|
3055
|
+
return { content: [{ type: "text", text: `Error: ${resolved.error}` }], isError: true };
|
|
3056
|
+
}
|
|
2920
3057
|
const workflows = dbListWorkflows();
|
|
2921
|
-
const workflow = workflows.find((w) => w.id ===
|
|
3058
|
+
const workflow = workflows.find((w) => w.id === resolved.id);
|
|
2922
3059
|
if (!workflow) {
|
|
2923
3060
|
return {
|
|
2924
|
-
content: [{ type: "text", text: `Error: workflow "${
|
|
3061
|
+
content: [{ type: "text", text: `Error: workflow "${resolved.id}" not found` }],
|
|
2925
3062
|
isError: true
|
|
2926
3063
|
};
|
|
2927
3064
|
}
|
|
2928
|
-
dbDeleteWorkflow(
|
|
3065
|
+
dbDeleteWorkflow(resolved.id);
|
|
2929
3066
|
dbSignalChange();
|
|
2930
3067
|
return { content: [{ type: "text", text: `Deleted workflow: ${workflow.name}` }] };
|
|
2931
3068
|
}
|
|
@@ -2997,6 +3134,63 @@ function registerWorkflowTools(server) {
|
|
|
2997
3134
|
}
|
|
2998
3135
|
}
|
|
2999
3136
|
);
|
|
3137
|
+
server.tool(
|
|
3138
|
+
"execute_workflow",
|
|
3139
|
+
"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.",
|
|
3140
|
+
{
|
|
3141
|
+
workflow_id: V.id.describe("Workflow ID (from list_workflows)"),
|
|
3142
|
+
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>}})")
|
|
3143
|
+
},
|
|
3144
|
+
async (args) => {
|
|
3145
|
+
const workflow = dbListWorkflows().find((w) => w.id === args.workflow_id);
|
|
3146
|
+
if (!workflow) {
|
|
3147
|
+
return {
|
|
3148
|
+
content: [{ type: "text", text: `Error: workflow "${args.workflow_id}" not found` }],
|
|
3149
|
+
isError: true
|
|
3150
|
+
};
|
|
3151
|
+
}
|
|
3152
|
+
const trigger = workflow.nodes.find((n) => n.type === "trigger")?.config;
|
|
3153
|
+
const defs = trigger?.triggerType === "manual" ? trigger.inputs ?? [] : [];
|
|
3154
|
+
const supplied = args.inputs ?? {};
|
|
3155
|
+
let inputs;
|
|
3156
|
+
if (defs.length > 0) {
|
|
3157
|
+
const { values, errors } = resolveWorkflowInputs(defs, supplied);
|
|
3158
|
+
if (errors.length > 0) {
|
|
3159
|
+
return {
|
|
3160
|
+
content: [
|
|
3161
|
+
{ type: "text", text: `Error: invalid inputs
|
|
3162
|
+
- ${errors.join("\n - ")}` }
|
|
3163
|
+
],
|
|
3164
|
+
isError: true
|
|
3165
|
+
};
|
|
3166
|
+
}
|
|
3167
|
+
inputs = values;
|
|
3168
|
+
} else if (Object.keys(supplied).length > 0) {
|
|
3169
|
+
inputs = supplied;
|
|
3170
|
+
}
|
|
3171
|
+
try {
|
|
3172
|
+
await rpcCall("workflow:runManual", { workflowId: args.workflow_id, inputs });
|
|
3173
|
+
} catch (err) {
|
|
3174
|
+
return {
|
|
3175
|
+
content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : err}` }],
|
|
3176
|
+
isError: true
|
|
3177
|
+
};
|
|
3178
|
+
}
|
|
3179
|
+
const disabled = workflow.enabled === false ? " (workflow is disabled; manual runs still execute)" : "";
|
|
3180
|
+
const shown = inputs ? `
|
|
3181
|
+
inputs: ${JSON.stringify(inputs, null, 2)}` : "\nno inputs";
|
|
3182
|
+
return {
|
|
3183
|
+
content: [
|
|
3184
|
+
{
|
|
3185
|
+
type: "text",
|
|
3186
|
+
text: `Queued "${workflow.name}"${disabled}${shown}
|
|
3187
|
+
|
|
3188
|
+
Run history: list_workflow_runs with workflow_id ${args.workflow_id}`
|
|
3189
|
+
}
|
|
3190
|
+
]
|
|
3191
|
+
};
|
|
3192
|
+
}
|
|
3193
|
+
);
|
|
3000
3194
|
}
|
|
3001
3195
|
|
|
3002
3196
|
// src/tools/config.ts
|
|
@@ -3097,6 +3291,249 @@ function registerWorkspaceTools(server) {
|
|
|
3097
3291
|
);
|
|
3098
3292
|
}
|
|
3099
3293
|
|
|
3294
|
+
// src/tools/connectors.ts
|
|
3295
|
+
import { z as z7 } from "zod";
|
|
3296
|
+
var PROBE_TIMEOUT_MS = 12e4;
|
|
3297
|
+
var json = (value) => ({
|
|
3298
|
+
content: [{ type: "text", text: JSON.stringify(value, null, 2) }]
|
|
3299
|
+
});
|
|
3300
|
+
var failure = (message) => ({
|
|
3301
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
3302
|
+
isError: true
|
|
3303
|
+
});
|
|
3304
|
+
function registerConnectorTools(server) {
|
|
3305
|
+
server.tool(
|
|
3306
|
+
"list_connectors",
|
|
3307
|
+
"List every connector: the ones built into Vorn, the ones installable from a package, and how many connections each already has. Use this before creating a workflow that calls a connector action, or to find the id of a connector to install.",
|
|
3308
|
+
{
|
|
3309
|
+
installable_only: z7.boolean().optional().describe("Only connectors that are not set up yet")
|
|
3310
|
+
},
|
|
3311
|
+
async (args) => {
|
|
3312
|
+
const [builtIns, catalog, connections, statuses] = await Promise.all([
|
|
3313
|
+
rpcCall("connector:list"),
|
|
3314
|
+
rpcCall("connector:catalog"),
|
|
3315
|
+
rpcCall("connection:list", { connectorId: void 0 }),
|
|
3316
|
+
rpcCall("connector:status")
|
|
3317
|
+
]);
|
|
3318
|
+
const countFor = (id) => connections.filter((conn) => connectionConnectorId(conn) === id).length;
|
|
3319
|
+
const statusFor = (id) => statuses.find((s) => s.connectorId === id);
|
|
3320
|
+
const entries = [
|
|
3321
|
+
...builtIns.map((c) => ({
|
|
3322
|
+
id: c.id,
|
|
3323
|
+
name: c.name,
|
|
3324
|
+
source: "built-in",
|
|
3325
|
+
capabilities: c.capabilities,
|
|
3326
|
+
connections: countFor(c.id),
|
|
3327
|
+
// Only meaningful for connectors that authenticate up front; the
|
|
3328
|
+
// rest report nothing rather than a misleading "not authed".
|
|
3329
|
+
...statusFor(c.id) && {
|
|
3330
|
+
authenticated: statusFor(c.id).authed,
|
|
3331
|
+
...statusFor(c.id).message && { authMessage: statusFor(c.id).message }
|
|
3332
|
+
}
|
|
3333
|
+
})),
|
|
3334
|
+
...catalog.map((entry) => ({
|
|
3335
|
+
id: entry.id,
|
|
3336
|
+
name: entry.name,
|
|
3337
|
+
source: "package",
|
|
3338
|
+
description: entry.description,
|
|
3339
|
+
package: entry.packageName,
|
|
3340
|
+
capabilities: entry.capabilities,
|
|
3341
|
+
connections: countFor(entry.id),
|
|
3342
|
+
...entry.auth && { auth: entry.auth }
|
|
3343
|
+
}))
|
|
3344
|
+
];
|
|
3345
|
+
return json(args.installable_only ? entries.filter((e) => e.connections === 0) : entries);
|
|
3346
|
+
}
|
|
3347
|
+
);
|
|
3348
|
+
server.tool(
|
|
3349
|
+
"list_connections",
|
|
3350
|
+
"List configured connector connections, including when each last synced and the error from its last failure. Use this to diagnose a connector that is not producing tasks.",
|
|
3351
|
+
{
|
|
3352
|
+
connector_id: V.id.optional().describe("Only connections for this connector"),
|
|
3353
|
+
failing_only: z7.boolean().optional().describe("Only connections whose last sync failed")
|
|
3354
|
+
},
|
|
3355
|
+
async (args) => {
|
|
3356
|
+
const connections = await rpcCall("connection:list", {
|
|
3357
|
+
connectorId: void 0
|
|
3358
|
+
});
|
|
3359
|
+
const visible = connections.filter((conn) => !args.connector_id || connectionConnectorId(conn) === args.connector_id).filter((conn) => !args.failing_only || !!conn.lastSyncError);
|
|
3360
|
+
return json(
|
|
3361
|
+
visible.map((conn) => ({
|
|
3362
|
+
id: conn.id,
|
|
3363
|
+
name: conn.name,
|
|
3364
|
+
connectorId: connectionConnectorId(conn),
|
|
3365
|
+
project: conn.executionProject,
|
|
3366
|
+
syncIntervalMinutes: conn.syncIntervalMinutes,
|
|
3367
|
+
lastSyncAt: conn.lastSyncAt,
|
|
3368
|
+
lastSyncError: conn.lastSyncError,
|
|
3369
|
+
// Deliberately not the whole `filters` blob: it holds encrypted
|
|
3370
|
+
// credentials, and an agent has no use for ciphertext.
|
|
3371
|
+
config: publicFilters(conn)
|
|
3372
|
+
}))
|
|
3373
|
+
);
|
|
3374
|
+
}
|
|
3375
|
+
);
|
|
3376
|
+
server.tool(
|
|
3377
|
+
"list_connector_actions",
|
|
3378
|
+
"List the actions a connection can execute, with their input schemas. Call this before run_connector_action or before adding a callConnectorAction node to a workflow.",
|
|
3379
|
+
{ connection_id: V.id.describe("Connection ID") },
|
|
3380
|
+
async (args) => {
|
|
3381
|
+
const actions = await rpcCall(
|
|
3382
|
+
"connection:listActions",
|
|
3383
|
+
args.connection_id
|
|
3384
|
+
);
|
|
3385
|
+
if (actions.length === 0) {
|
|
3386
|
+
return failure(
|
|
3387
|
+
`No actions for connection "${args.connection_id}". Either the connection does not exist, or its connector exposes no actions yet \u2014 for an MCP connection, tool discovery may still be running.`
|
|
3388
|
+
);
|
|
3389
|
+
}
|
|
3390
|
+
return json(actions);
|
|
3391
|
+
}
|
|
3392
|
+
);
|
|
3393
|
+
server.tool(
|
|
3394
|
+
"inspect_connector_package",
|
|
3395
|
+
"Start a connector package and read what it offers \u2014 its triggers, actions and required environment variables \u2014 without installing it. Use this to review a connector before install_connector, or to check a local build.",
|
|
3396
|
+
{
|
|
3397
|
+
package: V.shortText.describe(
|
|
3398
|
+
'npm package name, or a command to run a local build (e.g. "node /path/to/dist/index.js")'
|
|
3399
|
+
)
|
|
3400
|
+
},
|
|
3401
|
+
async (args) => {
|
|
3402
|
+
const result = await probe(args.package);
|
|
3403
|
+
if (!result.ok) return failure(result.error);
|
|
3404
|
+
return json(result.manifest);
|
|
3405
|
+
}
|
|
3406
|
+
);
|
|
3407
|
+
server.tool(
|
|
3408
|
+
"install_connector",
|
|
3409
|
+
"Install a connector from the catalog or from an npm package, creating a connection ready to poll. Call list_connectors for catalog ids and inspect_connector_package to see which environment variables are needed. Secrets cannot be set this way \u2014 see the error it returns if the connector requires one.",
|
|
3410
|
+
{
|
|
3411
|
+
connector_id: V.id.optional().describe("Catalog connector id (from list_connectors). Use this or package."),
|
|
3412
|
+
package: V.shortText.optional().describe("npm package name or launch command"),
|
|
3413
|
+
name: V.title.optional().describe("Connection name (defaults to the connector name)"),
|
|
3414
|
+
project: V.name.optional().describe("Vorn project tasks should be created in"),
|
|
3415
|
+
trigger: V.shortText.optional().describe("Trigger type to configure (defaults to the first the connector offers)"),
|
|
3416
|
+
env: z7.record(z7.string(), z7.string()).optional().describe("Non-secret environment variables the connector needs"),
|
|
3417
|
+
sync_interval_minutes: z7.number().int().min(1).max(1440).optional()
|
|
3418
|
+
},
|
|
3419
|
+
async (args) => {
|
|
3420
|
+
const catalog = await rpcCall("connector:catalog");
|
|
3421
|
+
const entry = args.connector_id ? catalog.find((c) => c.id === args.connector_id) : void 0;
|
|
3422
|
+
if (args.connector_id && !entry) {
|
|
3423
|
+
return failure(
|
|
3424
|
+
`No connector "${args.connector_id}" in the catalog. Known: ${catalog.map((c) => c.id).join(", ") || "(none)"}. To install something not in the catalog, pass \`package\` instead.`
|
|
3425
|
+
);
|
|
3426
|
+
}
|
|
3427
|
+
const target = entry ? entry.launch : args.package;
|
|
3428
|
+
if (!target) return failure("Provide either connector_id or package.");
|
|
3429
|
+
const result = await probe(target);
|
|
3430
|
+
if (!result.ok) return failure(result.error);
|
|
3431
|
+
const manifest = result.manifest;
|
|
3432
|
+
const supplied = args.env ?? {};
|
|
3433
|
+
const unknown = Object.keys(supplied).filter(
|
|
3434
|
+
(name) => !manifest.env.some((e) => e.name === name)
|
|
3435
|
+
);
|
|
3436
|
+
if (unknown.length > 0) {
|
|
3437
|
+
return failure(
|
|
3438
|
+
`${manifest.name} does not use ${unknown.join(", ")}. It accepts: ${manifest.env.map((e) => e.name).join(", ") || "(none)"}.`
|
|
3439
|
+
);
|
|
3440
|
+
}
|
|
3441
|
+
const secrets = manifest.env.filter((e) => e.secret && (e.required || supplied[e.name]));
|
|
3442
|
+
if (secrets.length > 0) {
|
|
3443
|
+
return failure(
|
|
3444
|
+
`${manifest.name} uses the secret ${plural(secrets.length, "value")} ${secrets.map((e) => e.name).join(", ")}, which this tool cannot accept: it runs outside the desktop process, where encryption lives, so it could only store them unprotected. They must be entered by a person in Settings > Connectors to reach the OS keychain. Everything else about the connector is ready to install.`
|
|
3445
|
+
);
|
|
3446
|
+
}
|
|
3447
|
+
const missing = manifest.env.filter((e) => e.required && !supplied[e.name]?.trim());
|
|
3448
|
+
if (missing.length > 0) {
|
|
3449
|
+
return failure(
|
|
3450
|
+
`${manifest.name} needs ${missing.map((e) => describeEnv(e)).join(", ")}. Pass them in \`env\`.`
|
|
3451
|
+
);
|
|
3452
|
+
}
|
|
3453
|
+
const trigger = args.trigger ? manifest.triggers.find((t) => t.type === args.trigger) : manifest.triggers[0];
|
|
3454
|
+
if (args.trigger && !trigger) {
|
|
3455
|
+
return failure(
|
|
3456
|
+
`${manifest.name} has no trigger "${args.trigger}". It offers: ${manifest.triggers.map((t) => t.type).join(", ") || "(none)"}.`
|
|
3457
|
+
);
|
|
3458
|
+
}
|
|
3459
|
+
const launch = typeof target === "string" ? parseLaunch(target) : target;
|
|
3460
|
+
const connection = await rpcCall("connection:create", {
|
|
3461
|
+
connectorId: "mcp",
|
|
3462
|
+
name: args.name ?? (trigger ? `${manifest.name}: ${trigger.label}` : manifest.name),
|
|
3463
|
+
filters: {
|
|
3464
|
+
command: launch.command,
|
|
3465
|
+
args: JSON.stringify(launch.args),
|
|
3466
|
+
env: JSON.stringify(supplied),
|
|
3467
|
+
[SDK_FILTER_KEYS.connectorId]: manifest.id,
|
|
3468
|
+
[SDK_FILTER_KEYS.version]: manifest.version,
|
|
3469
|
+
...manifest.icon && { [SDK_FILTER_KEYS.icon]: JSON.stringify(manifest.icon) },
|
|
3470
|
+
...trigger?.filters ?? {}
|
|
3471
|
+
},
|
|
3472
|
+
syncIntervalMinutes: args.sync_interval_minutes ?? 5,
|
|
3473
|
+
statusMapping: {},
|
|
3474
|
+
...args.project && { executionProject: args.project }
|
|
3475
|
+
});
|
|
3476
|
+
return json({
|
|
3477
|
+
installed: manifest.name,
|
|
3478
|
+
connectionId: connection.id,
|
|
3479
|
+
trigger: trigger?.type,
|
|
3480
|
+
note: "Poll it now with backfill_connection, or reference it from a workflow."
|
|
3481
|
+
});
|
|
3482
|
+
}
|
|
3483
|
+
);
|
|
3484
|
+
server.tool(
|
|
3485
|
+
"run_connector_action",
|
|
3486
|
+
"Execute one action on a connection \u2014 create an issue, run a query, close a work item. Call list_connector_actions first for the action name and its arguments.",
|
|
3487
|
+
{
|
|
3488
|
+
connection_id: V.id.describe("Connection ID"),
|
|
3489
|
+
action: V.shortText.describe("Action name from list_connector_actions"),
|
|
3490
|
+
args: z7.record(z7.string(), z7.unknown()).optional().describe("Action arguments")
|
|
3491
|
+
},
|
|
3492
|
+
async (args) => {
|
|
3493
|
+
const result = await rpcCall("connection:executeAction", {
|
|
3494
|
+
connectionId: args.connection_id,
|
|
3495
|
+
action: args.action,
|
|
3496
|
+
args: args.args ?? {}
|
|
3497
|
+
});
|
|
3498
|
+
if (!result.success) return failure(result.error ?? "Action failed");
|
|
3499
|
+
return json(result);
|
|
3500
|
+
}
|
|
3501
|
+
);
|
|
3502
|
+
server.tool(
|
|
3503
|
+
"backfill_connection",
|
|
3504
|
+
"Pull items from a connection now and turn them into tasks, without waiting for its poll interval. Use this to verify a connection works after installing it.",
|
|
3505
|
+
{ connection_id: V.id.describe("Connection ID") },
|
|
3506
|
+
async (args) => {
|
|
3507
|
+
const result = await rpcCall(
|
|
3508
|
+
"connection:backfill",
|
|
3509
|
+
{ connectionId: args.connection_id },
|
|
3510
|
+
PROBE_TIMEOUT_MS
|
|
3511
|
+
);
|
|
3512
|
+
if (result.error) return failure(result.error);
|
|
3513
|
+
return json(result);
|
|
3514
|
+
}
|
|
3515
|
+
);
|
|
3516
|
+
}
|
|
3517
|
+
async function probe(target) {
|
|
3518
|
+
const launch = typeof target === "string" ? parseLaunch(target) : target;
|
|
3519
|
+
return rpcCall("connector:probeSdk", launch, PROBE_TIMEOUT_MS);
|
|
3520
|
+
}
|
|
3521
|
+
function parseLaunch(spec) {
|
|
3522
|
+
const parts = spec.trim().split(/\s+/);
|
|
3523
|
+
if (parts.length === 1) return { command: "npx", args: ["-y", parts[0]] };
|
|
3524
|
+
return { command: parts[0], args: parts.slice(1) };
|
|
3525
|
+
}
|
|
3526
|
+
function publicFilters(conn) {
|
|
3527
|
+
const hidden = /* @__PURE__ */ new Set(["secretEnv", "discoveredTools"]);
|
|
3528
|
+
return Object.fromEntries(Object.entries(conn.filters ?? {}).filter(([key]) => !hidden.has(key)));
|
|
3529
|
+
}
|
|
3530
|
+
function describeEnv(entry) {
|
|
3531
|
+
return entry.description ? `${entry.name} (${entry.description})` : entry.name;
|
|
3532
|
+
}
|
|
3533
|
+
function plural(count, word) {
|
|
3534
|
+
return count === 1 ? word : `${word}s`;
|
|
3535
|
+
}
|
|
3536
|
+
|
|
3100
3537
|
// src/server.ts
|
|
3101
3538
|
function createMcpServer(version) {
|
|
3102
3539
|
const server = new McpServer({ name: "vorn", version }, { capabilities: { tools: {} } });
|
|
@@ -3106,6 +3543,7 @@ function createMcpServer(version) {
|
|
|
3106
3543
|
registerSessionTools(server);
|
|
3107
3544
|
registerWorkflowTools(server);
|
|
3108
3545
|
registerWorkspaceTools(server);
|
|
3546
|
+
registerConnectorTools(server);
|
|
3109
3547
|
return server;
|
|
3110
3548
|
}
|
|
3111
3549
|
|
|
@@ -3118,7 +3556,7 @@ console.warn = (...args) => _origError("[mcp:warn]", ...args);
|
|
|
3118
3556
|
console.error = (...args) => _origError("[mcp:error]", ...args);
|
|
3119
3557
|
async function main() {
|
|
3120
3558
|
configManager.init();
|
|
3121
|
-
const version = true ? "0.5.
|
|
3559
|
+
const version = true ? "0.5.4" : createRequire(import.meta.url)("../package.json").version;
|
|
3122
3560
|
const server = createMcpServer(version);
|
|
3123
3561
|
const transport = new StdioServerTransport();
|
|
3124
3562
|
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.4",
|
|
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.4",
|
|
42
|
+
"@vornrun/shared": "0.5.4",
|
|
43
43
|
"tsup": "^8.5.1",
|
|
44
44
|
"tsx": "^4.23.1",
|
|
45
45
|
"typescript": "^6.0.3"
|