@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.
Files changed (2) hide show
  1. package/dist/index.js +473 -35
  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 = {
@@ -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 ${TIMEOUT_MS}ms`));
2322
- }, TIMEOUT_MS);
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
- id: V.id.describe("Workflow ID"),
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 === args.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 "${args.id}" not found` }],
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(args.id, updates);
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
- { id: V.id.describe("Workflow ID") },
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 === args.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 "${args.id}" not found` }],
3061
+ content: [{ type: "text", text: `Error: workflow "${resolved.id}" not found` }],
2925
3062
  isError: true
2926
3063
  };
2927
3064
  }
2928
- dbDeleteWorkflow(args.id);
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.2" : createRequire(import.meta.url)("../package.json").version;
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.2",
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.2",
42
- "@vornrun/shared": "0.5.2",
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"