@vornrun/mcp 0.7.0-beta.1 → 0.7.0-beta.11

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 +251 -92
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -50,8 +50,32 @@ import fs from "fs";
50
50
  import path from "path";
51
51
 
52
52
  // ../shared/src/protocol.ts
53
+ var BOOTSTRAP_ENV_VAR = "SECRET_VORN_BOOTSTRAP_TOKEN";
53
54
  var LOCAL_TOKEN_FILENAME = "local-token";
54
55
 
56
+ // ../shared/src/types.ts
57
+ var DEFAULT_WORKSPACE = {
58
+ id: "personal",
59
+ name: "Personal",
60
+ icon: "User",
61
+ iconColor: "#6b7280",
62
+ order: 0
63
+ };
64
+ function isTerminalTaskStatus(status) {
65
+ return status === "done" || status === "cancelled";
66
+ }
67
+ var SDK_FILTER_KEYS = {
68
+ connectorId: "sdkConnectorId",
69
+ version: "sdkVersion",
70
+ icon: "sdkIcon",
71
+ implicit: "implicit"
72
+ };
73
+ var NEVER_BORROWED_ENV = { keys: ["CLAUDECODE"], prefixes: ["CLAUDE_CODE_"] };
74
+ function connectionConnectorId(connection2) {
75
+ const packaged = connection2.filters?.[SDK_FILTER_KEYS.connectorId];
76
+ return typeof packaged === "string" && packaged !== "" ? packaged : connection2.connectorId;
77
+ }
78
+
55
79
  // ../server/src/process-utils.ts
56
80
  function getDefaultShell(configured) {
57
81
  const chosen = configured?.trim();
@@ -76,30 +100,10 @@ function findWindowsShell() {
76
100
  if (fs.existsSync(windowsPowerShell)) return windowsPowerShell;
77
101
  return process.env.COMSPEC || "cmd.exe";
78
102
  }
79
- var STRIP_ENV_KEYS = ["CLAUDECODE"];
103
+ var STRIP_ENV_KEYS = NEVER_BORROWED_ENV.keys;
104
+ var STRIP_ENV_PREFIXES = [...NEVER_BORROWED_ENV.prefixes, BOOTSTRAP_ENV_VAR];
80
105
  var STRIP_ENV_KEYS_UPPER = STRIP_ENV_KEYS.map((k) => k.toUpperCase());
81
106
 
82
- // ../shared/src/types.ts
83
- var DEFAULT_WORKSPACE = {
84
- id: "personal",
85
- name: "Personal",
86
- icon: "User",
87
- iconColor: "#6b7280",
88
- order: 0
89
- };
90
- function isTerminalTaskStatus(status) {
91
- return status === "done" || status === "cancelled";
92
- }
93
- var SDK_FILTER_KEYS = {
94
- connectorId: "sdkConnectorId",
95
- version: "sdkVersion",
96
- icon: "sdkIcon"
97
- };
98
- function connectionConnectorId(connection2) {
99
- const packaged = connection2.filters?.[SDK_FILTER_KEYS.connectorId];
100
- return typeof packaged === "string" && packaged !== "" ? packaged : connection2.connectorId;
101
- }
102
-
103
107
  // ../server/src/default-workflows.ts
104
108
  var DEFAULT_TASK_WORKFLOW_ID = "system:default-task-workflow";
105
109
  function buildDefaultTaskWorkflow() {
@@ -888,6 +892,51 @@ function migrateSchema(d) {
888
892
  })();
889
893
  logger_default.info("[database] migrated schema to version 15 (config row revisions)");
890
894
  }
895
+ if (version < 16) {
896
+ d.transaction(() => {
897
+ const cols = d.prepare("PRAGMA table_info(sessions)").all();
898
+ if (!cols.some((c) => c.name === "shell_cwd")) {
899
+ d.exec("ALTER TABLE sessions ADD COLUMN shell_cwd TEXT");
900
+ }
901
+ d.prepare(
902
+ "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '16')"
903
+ ).run();
904
+ })();
905
+ logger_default.info("[database] migrated schema to version 16 (shell working directory)");
906
+ }
907
+ if (version < 17) {
908
+ d.transaction(() => {
909
+ const derived = `(
910
+ SELECT json_extract(sc.filters, '$.sdkConnectorId')
911
+ FROM task_source_links tsl
912
+ JOIN source_connections sc ON sc.id = tsl.connection_id
913
+ WHERE tsl.task_id = tasks.id
914
+ )`;
915
+ d.exec(`
916
+ UPDATE tasks
917
+ SET source_connector_id = ${derived}
918
+ WHERE source_connector_id = 'mcp' AND ${derived} IS NOT NULL
919
+ `);
920
+ d.exec(`
921
+ UPDATE task_source_links
922
+ SET connector_id = (
923
+ SELECT json_extract(sc.filters, '$.sdkConnectorId')
924
+ FROM source_connections sc
925
+ WHERE sc.id = task_source_links.connection_id
926
+ )
927
+ WHERE connector_id = 'mcp'
928
+ AND (
929
+ SELECT json_extract(sc.filters, '$.sdkConnectorId')
930
+ FROM source_connections sc
931
+ WHERE sc.id = task_source_links.connection_id
932
+ ) IS NOT NULL
933
+ `);
934
+ d.prepare(
935
+ "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '17')"
936
+ ).run();
937
+ })();
938
+ logger_default.info("[database] migrated schema to version 17 (packaged connector task ids)");
939
+ }
891
940
  }
892
941
  var REVISIONED_TABLES = [
893
942
  "projects",
@@ -930,7 +979,8 @@ function verifySchema(d) {
930
979
  ddl: "ALTER TABLE sessions ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0"
931
980
  },
932
981
  { column: "worktree_name", ddl: "ALTER TABLE sessions ADD COLUMN worktree_name TEXT" },
933
- { column: "agent_session_id", ddl: "ALTER TABLE sessions ADD COLUMN agent_session_id TEXT" }
982
+ { column: "agent_session_id", ddl: "ALTER TABLE sessions ADD COLUMN agent_session_id TEXT" },
983
+ { column: "shell_cwd", ddl: "ALTER TABLE sessions ADD COLUMN shell_cwd TEXT" }
934
984
  ],
935
985
  agent_commands: [
936
986
  {
@@ -1078,7 +1128,13 @@ function loadDefaults(d) {
1078
1128
  ...map.hasSeenOnboarding !== void 0 && {
1079
1129
  hasSeenOnboarding: map.hasSeenOnboarding
1080
1130
  },
1081
- ...map.reopenSessions !== void 0 && { reopenSessions: map.reopenSessions },
1131
+ // Default on, and that changed meaning rather than merely flipping. It used
1132
+ // to decide whether every saved session was relaunched at start-up, which
1133
+ // spends tokens and starts processes -- worth asking about, so it was off.
1134
+ // Bringing a pane back no longer does either: it shows the last screen its
1135
+ // terminal drew and waits. There is nothing to ask, and leaving it off made
1136
+ // the whole thing invisible unless somebody went looking for a toggle.
1137
+ reopenSessions: map.reopenSessions ?? true,
1082
1138
  // Saving iterates over every key in defaults, but loading is this explicit
1083
1139
  // list — so a key missing here round-trips to nothing and its feature is
1084
1140
  // silently inert.
@@ -1092,6 +1148,10 @@ function loadDefaults(d) {
1092
1148
  // user has toggled it, so absence means "not yet decided", not "off".
1093
1149
  domBlockRendering: map.domBlockRendering ?? true,
1094
1150
  minimalShellPrompt: map.minimalShellPrompt ?? true,
1151
+ // Sessions outlive the window. Default on, same reasoning as above: the key
1152
+ // only appears once the user has turned it off, so absence is "not yet
1153
+ // decided". Read by the main process at quit, not by the renderer.
1154
+ keepSessionsRunning: map.keepSessionsRunning ?? true,
1095
1155
  ...map.widgetEnabled !== void 0 && { widgetEnabled: map.widgetEnabled },
1096
1156
  ...map.taskViewMode !== void 0 && {
1097
1157
  taskViewMode: map.taskViewMode
@@ -1592,7 +1652,7 @@ var configManager = new ConfigManager();
1592
1652
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1593
1653
 
1594
1654
  // src/tools/tasks.ts
1595
- import crypto from "crypto";
1655
+ import crypto2 from "crypto";
1596
1656
  import path4 from "path";
1597
1657
  import { z as z2 } from "zod";
1598
1658
 
@@ -1989,7 +2049,7 @@ function registerTaskTools(server) {
1989
2049
  const now = (/* @__PURE__ */ new Date()).toISOString();
1990
2050
  const status = args.status ?? "todo";
1991
2051
  const task = {
1992
- id: crypto.randomUUID(),
2052
+ id: crypto2.randomUUID(),
1993
2053
  projectName: args.project_name,
1994
2054
  title: args.title,
1995
2055
  description: args.description ?? "",
@@ -2689,41 +2749,63 @@ function registerSessionTools(server) {
2689
2749
  }
2690
2750
 
2691
2751
  // src/tools/workflows.ts
2692
- import crypto2 from "crypto";
2752
+ import crypto3 from "crypto";
2693
2753
  import { z as z5 } from "zod";
2694
2754
 
2695
- // src/workflow-portability.ts
2755
+ // ../shared/src/workflow-portability.ts
2696
2756
  var PROJECT_PATH_TOKEN = "{{project.path}}";
2697
2757
  var PROJECT_NAME_TOKEN = "{{project.name}}";
2698
2758
  var PORTABLE_FORMAT_VERSION = 1;
2759
+ var HTTP_PROFILE_CONNECTOR = "http";
2699
2760
  function importedWorkflowId(bundle, slug) {
2700
2761
  return `import:${bundle}:${slug}`;
2701
2762
  }
2763
+ function importedWorkflowIdFor(bundle, slug, name, existing) {
2764
+ for (let attempt = 1; ; attempt++) {
2765
+ const id = importedWorkflowId(bundle, attempt === 1 ? slug : `${slug}-${attempt}`);
2766
+ const held = existing.find((workflow) => workflow.id === id);
2767
+ if (!held || held.name === name) return id;
2768
+ }
2769
+ }
2702
2770
  function slugify(name) {
2703
2771
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "workflow";
2704
2772
  }
2705
- function portabilityBlockers(workflow) {
2706
- const blockers = [];
2707
- for (const node of workflow.nodes) {
2708
- const config = node.config;
2709
- if (node.type === "trigger" && config.triggerType === "connectorPoll") {
2710
- blockers.push(`the trigger polls a connector connection, which exists only on this machine`);
2711
- }
2712
- if (node.type === "callConnectorAction") {
2713
- blockers.push(`step "${node.label}" calls a connector action bound to a local connection`);
2714
- }
2773
+ function connectorOf(connection2) {
2774
+ return connectionConnectorId({
2775
+ connectorId: connection2.connectorId,
2776
+ filters: connection2.filters ?? {}
2777
+ });
2778
+ }
2779
+ function boundConnectionKey(node, config) {
2780
+ if (node.type === "trigger" && config.triggerType === "connectorPoll") return "connectionId";
2781
+ if (node.type === "callConnectorAction") return "connectionId";
2782
+ if (node.type === "httpRequest") return "profileConnectionId";
2783
+ if (node.type === "script") return "secretsFrom";
2784
+ return null;
2785
+ }
2786
+ var OPTIONAL_CONNECTION_KEYS = /* @__PURE__ */ new Set(["profileConnectionId", "secretsFrom"]);
2787
+ function resolveRequirement(requirement, connections) {
2788
+ const candidates = connections.filter(
2789
+ (connection2) => requirement.kind === "httpProfile" ? connectorOf(connection2) === HTTP_PROFILE_CONNECTOR : requirement.connectorId !== "" && connectorOf(connection2) === requirement.connectorId
2790
+ );
2791
+ if (candidates.length === 0) return void 0;
2792
+ if (requirement.name !== "") {
2793
+ const named = candidates.filter((connection2) => connection2.name === requirement.name);
2794
+ if (named.length === 1) return named[0].id;
2715
2795
  }
2716
- return blockers;
2796
+ return candidates.length === 1 ? candidates[0].id : void 0;
2717
2797
  }
2718
- function toPortable(workflow, projectPath) {
2798
+ function toPortable(workflow, projectPath, connections = []) {
2719
2799
  const slug = slugify(workflow.name);
2800
+ const requires = [];
2720
2801
  const nodes = workflow.nodes.map((node) => {
2721
2802
  const config = { ...node.config };
2803
+ if (node.type === "trigger" && config.triggerType === "webhook") config.token = "";
2722
2804
  if (node.type === "launchAgent" || node.type === "script") {
2723
- for (const key of ["projectPath", "cwd", "existingWorktreePath"]) {
2724
- const value = config[key];
2805
+ for (const key2 of ["projectPath", "cwd", "existingWorktreePath"]) {
2806
+ const value = config[key2];
2725
2807
  if (typeof value === "string" && value) {
2726
- config[key] = replacePath(value, projectPath);
2808
+ config[key2] = replacePath(value, projectPath);
2727
2809
  }
2728
2810
  }
2729
2811
  if (typeof config.projectName === "string" && config.projectName) {
@@ -2731,6 +2813,26 @@ function toPortable(workflow, projectPath) {
2731
2813
  }
2732
2814
  delete config.remoteHostId;
2733
2815
  }
2816
+ const key = boundConnectionKey(node, config);
2817
+ const bound = key === null ? "" : config[key];
2818
+ const unbound = key !== null && !OPTIONAL_CONNECTION_KEYS.has(key) && bound === "";
2819
+ if (key !== null && (typeof bound === "string" && bound !== "" || unbound)) {
2820
+ const source2 = connections.find((connection2) => connection2.id === bound);
2821
+ const event = config.event;
2822
+ const declared = config.connectorId;
2823
+ requires.push(
2824
+ key === "profileConnectionId" ? { kind: "httpProfile", nodeId: node.id, name: source2?.name ?? "" } : {
2825
+ kind: "connection",
2826
+ nodeId: node.id,
2827
+ connectorId: source2 ? connectorOf(source2) : typeof declared === "string" ? declared : "",
2828
+ name: source2?.name ?? "",
2829
+ ...typeof event === "string" && event !== "" && { event },
2830
+ ...key === "secretsFrom" && { key }
2831
+ }
2832
+ );
2833
+ if (OPTIONAL_CONNECTION_KEYS.has(key)) delete config[key];
2834
+ else config[key] = "";
2835
+ }
2734
2836
  return { ...node, config };
2735
2837
  });
2736
2838
  return {
@@ -2740,6 +2842,7 @@ function toPortable(workflow, projectPath) {
2740
2842
  ...workflow.icon && { icon: workflow.icon },
2741
2843
  ...workflow.iconColor && { iconColor: workflow.iconColor },
2742
2844
  ...workflow.staggerDelayMs !== void 0 && { staggerDelayMs: workflow.staggerDelayMs },
2845
+ ...requires.length > 0 && { requires },
2743
2846
  nodes,
2744
2847
  edges: workflow.edges
2745
2848
  };
@@ -2750,16 +2853,40 @@ function normalizeForCompare(p) {
2750
2853
  function replacePath(value, projectPath) {
2751
2854
  const v = normalizeForCompare(value);
2752
2855
  const root = normalizeForCompare(projectPath);
2856
+ if (root === "") return value;
2753
2857
  if (v === root) return PROJECT_PATH_TOKEN;
2754
2858
  if (v.startsWith(`${root}/`)) return `${PROJECT_PATH_TOKEN}/${v.slice(root.length + 1)}`;
2755
2859
  return value;
2756
2860
  }
2757
- function fromPortable(portable, bundle, project) {
2861
+ function unresolvedRequirements(portable, connections) {
2862
+ const present = new Set(portable.nodes.map((node) => node.id));
2863
+ return (portable.requires ?? []).filter(
2864
+ (requirement) => present.has(requirement.nodeId) && (bindsOnlyByHand(requirement) || resolveRequirement(requirement, connections) === void 0)
2865
+ );
2866
+ }
2867
+ function bindsOnlyByHand(requirement) {
2868
+ return requirement.kind === "connection" && requirement.key === "secretsFrom";
2869
+ }
2870
+ function fromPortable(portable, bundle, project, connections = [], mintToken = () => crypto.randomUUID()) {
2871
+ const bindings = /* @__PURE__ */ new Map();
2872
+ for (const requirement of portable.requires ?? []) {
2873
+ bindings.set(requirement.nodeId, [...bindings.get(requirement.nodeId) ?? [], requirement]);
2874
+ }
2758
2875
  const nodes = portable.nodes.map((node) => {
2759
2876
  const config = { ...node.config };
2760
- for (const [key, value] of Object.entries(config)) {
2877
+ const key = boundConnectionKey(node, config);
2878
+ if (key !== null && OPTIONAL_CONNECTION_KEYS.has(key)) delete config[key];
2879
+ for (const [key2, value] of Object.entries(config)) {
2761
2880
  if (typeof value !== "string") continue;
2762
- config[key] = value.split(PROJECT_PATH_TOKEN).join(project.path.replace(/[/\\]+$/, "")).split(PROJECT_NAME_TOKEN).join(project.name);
2881
+ config[key2] = value.split(PROJECT_PATH_TOKEN).join(project.path.replace(/[/\\]+$/, "")).split(PROJECT_NAME_TOKEN).join(project.name);
2882
+ }
2883
+ for (const requirement of bindings.get(node.id) ?? []) {
2884
+ if (bindsOnlyByHand(requirement) || key === null) continue;
2885
+ const resolved = resolveRequirement(requirement, connections);
2886
+ if (resolved !== void 0) config[key] = resolved;
2887
+ }
2888
+ if (node.type === "trigger" && config.triggerType === "webhook" && !config.token) {
2889
+ config.token = mintToken();
2763
2890
  }
2764
2891
  return { ...node, config };
2765
2892
  });
@@ -2768,7 +2895,9 @@ function fromPortable(portable, bundle, project) {
2768
2895
  name: portable.name,
2769
2896
  icon: portable.icon ?? "Zap",
2770
2897
  iconColor: portable.iconColor ?? "#6366f1",
2771
- enabled: true,
2898
+ // A file cannot ask to be running: a dropped cron workflow would start
2899
+ // firing before anyone had read it. Callers restore what they had.
2900
+ enabled: false,
2772
2901
  ...portable.staggerDelayMs !== void 0 && { staggerDelayMs: portable.staggerDelayMs },
2773
2902
  nodes,
2774
2903
  edges: portable.edges
@@ -2891,6 +3020,18 @@ var triggerConfigSchema = z5.union([
2891
3020
  projectFilter: V.name.optional(),
2892
3021
  fromStatus: z5.enum(["todo", "in_progress", "in_review", "done", "cancelled"]).optional(),
2893
3022
  toStatus: z5.enum(["todo", "in_progress", "in_review", "done", "cancelled"]).optional()
3023
+ }),
3024
+ z5.object({
3025
+ triggerType: z5.literal("connectorPoll"),
3026
+ connectionId: V.id,
3027
+ event: V.shortText,
3028
+ cron: V.shortText,
3029
+ timezone: V.shortText.optional()
3030
+ }),
3031
+ z5.object({
3032
+ triggerType: z5.literal("webhook"),
3033
+ method: z5.enum(["POST", "GET"]),
3034
+ token: V.shortText
2894
3035
  })
2895
3036
  ]);
2896
3037
  var nodeSchema = z5.object({
@@ -2905,6 +3046,7 @@ var nodeSchema = z5.object({
2905
3046
  "approval",
2906
3047
  "createTaskFromItem",
2907
3048
  "callConnectorAction",
3049
+ "httpRequest",
2908
3050
  "loop"
2909
3051
  ]),
2910
3052
  label: V.shortText,
@@ -2966,7 +3108,7 @@ function buildGraphFromFlat(trigger, actions) {
2966
3108
  const nodes = [];
2967
3109
  const edges = [];
2968
3110
  const triggerNode = {
2969
- id: crypto2.randomUUID(),
3111
+ id: crypto3.randomUUID(),
2970
3112
  type: "trigger",
2971
3113
  label: trigger.triggerType === "manual" ? "Manual Trigger" : trigger.triggerType === "once" ? "Schedule (Once)" : trigger.triggerType === "recurring" ? "Schedule (Recurring)" : trigger.triggerType === "taskCreated" ? "When Task Created" : trigger.triggerType === "taskStatusChanged" ? "When Task Status Changes" : "Trigger",
2972
3114
  config: trigger,
@@ -2977,7 +3119,7 @@ function buildGraphFromFlat(trigger, actions) {
2977
3119
  const NODE_GAP = 140;
2978
3120
  for (let i = 0; i < actions.length; i++) {
2979
3121
  const action = actions[i];
2980
- const nodeId = crypto2.randomUUID();
3122
+ const nodeId = crypto3.randomUUID();
2981
3123
  nodes.push({
2982
3124
  id: nodeId,
2983
3125
  type: "launchAgent",
@@ -2986,7 +3128,7 @@ function buildGraphFromFlat(trigger, actions) {
2986
3128
  position: { x: 0, y: (i + 1) * NODE_GAP }
2987
3129
  });
2988
3130
  edges.push({
2989
- id: crypto2.randomUUID(),
3131
+ id: crypto3.randomUUID(),
2990
3132
  source: prevId,
2991
3133
  target: nodeId
2992
3134
  });
@@ -3063,6 +3205,13 @@ function resolveWorkflowId(args) {
3063
3205
  }
3064
3206
  return { id };
3065
3207
  }
3208
+ async function listPortableConnections() {
3209
+ try {
3210
+ return await rpcCall("connection:list", { connectorId: void 0 });
3211
+ } catch {
3212
+ return [];
3213
+ }
3214
+ }
3066
3215
  function registerWorkflowTools(server) {
3067
3216
  server.tool(
3068
3217
  "list_workflows",
@@ -3115,7 +3264,7 @@ function registerWorkflowTools(server) {
3115
3264
  edges = graph.edges;
3116
3265
  }
3117
3266
  const workflow = {
3118
- id: crypto2.randomUUID(),
3267
+ id: crypto3.randomUUID(),
3119
3268
  name: args.name,
3120
3269
  icon: args.icon ?? "Zap",
3121
3270
  iconColor: args.icon_color ?? "#6366f1",
@@ -3385,7 +3534,7 @@ Run history: list_workflow_runs with workflow_id ${args.workflow_id}`
3385
3534
  );
3386
3535
  server.tool(
3387
3536
  "export_workflow",
3388
- "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.",
3537
+ "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. Connections are dropped too and recorded as requirements the importing machine rebinds by connector and name.",
3389
3538
  {
3390
3539
  workflow_id: V.id.optional().describe("Workflow ID (from list_workflows)"),
3391
3540
  id: V.id.optional().describe("Deprecated alias for workflow_id")
@@ -3402,18 +3551,6 @@ Run history: list_workflow_runs with workflow_id ${args.workflow_id}`
3402
3551
  isError: true
3403
3552
  };
3404
3553
  }
3405
- const blockers = portabilityBlockers(workflow);
3406
- if (blockers.length > 0) {
3407
- return {
3408
- content: [
3409
- {
3410
- type: "text",
3411
- text: `Error: "${workflow.name}" cannot be exported portably because ` + blockers.join("; ") + ". Rebuild those steps without the connection, or keep this workflow local."
3412
- }
3413
- ],
3414
- isError: true
3415
- };
3416
- }
3417
3554
  const projects = await dbListProjects();
3418
3555
  const projectName = workflow.nodes.map((n) => n.config.projectName).find((name) => typeof name === "string" && name.length > 0);
3419
3556
  const project = projects.find((p) => p.name === projectName);
@@ -3428,15 +3565,20 @@ Run history: list_workflow_runs with workflow_id ${args.workflow_id}`
3428
3565
  isError: true
3429
3566
  };
3430
3567
  }
3431
- const portable = toPortable(workflow, project.path);
3568
+ const portable = toPortable(workflow, project.path, await listPortableConnections());
3432
3569
  const residual = residualAbsolutePaths(portable);
3570
+ const unnamed = (portable.requires ?? []).filter(
3571
+ (requirement) => requirement.kind === "connection" && requirement.connectorId === ""
3572
+ );
3433
3573
  return {
3434
3574
  content: [
3435
3575
  {
3436
3576
  type: "text",
3437
3577
  text: JSON.stringify(portable, null, 2) + (residual.length > 0 ? `
3438
3578
 
3439
- Warning: these still hold a machine-specific path and will not travel: ${residual.join(", ")}` : "")
3579
+ Warning: these still hold a machine-specific path and will not travel: ${residual.join(", ")}` : "") + (unnamed.length > 0 ? `
3580
+
3581
+ Warning: ${unnamed.length} step(s) point at a connection this install could not name, so an import cannot rebind them automatically.` : "")
3440
3582
  }
3441
3583
  ]
3442
3584
  };
@@ -3444,7 +3586,7 @@ Warning: these still hold a machine-specific path and will not travel: ${residua
3444
3586
  );
3445
3587
  server.tool(
3446
3588
  "import_workflow",
3447
- "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.",
3589
+ "Import a workflow exported by export_workflow, resolving {{project.path}} and {{project.name}} against a registered project. Recorded connection requirements are rebound when this machine has one unambiguous match, and reported as still to connect otherwise. 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.",
3448
3590
  {
3449
3591
  workflow: z5.string().max(5e5).describe("The exported workflow JSON"),
3450
3592
  project_name: V.name.describe("Registered project to resolve paths against"),
@@ -3504,38 +3646,38 @@ Warning: these still hold a machine-specific path and will not travel: ${residua
3504
3646
  };
3505
3647
  }
3506
3648
  const bundle = args.bundle ?? slugify(project.name);
3507
- const definition = fromPortable(
3508
- { ...parsed, slug: parsed.slug ?? slugify(parsed.name) },
3649
+ const portable = { ...parsed, slug: parsed.slug ?? slugify(parsed.name) };
3650
+ const connections = await listPortableConnections();
3651
+ const resolved = fromPortable(
3652
+ portable,
3509
3653
  bundle,
3510
3654
  {
3511
3655
  name: project.name,
3512
3656
  path: project.path
3513
- }
3657
+ },
3658
+ connections
3514
3659
  );
3515
- const blockers = portabilityBlockers(definition);
3516
- if (blockers.length > 0) {
3517
- return {
3518
- content: [
3519
- {
3520
- type: "text",
3521
- text: `Error: this workflow cannot be imported because ${blockers.join("; ")}.`
3522
- }
3523
- ],
3524
- isError: true
3525
- };
3526
- }
3527
- const existing = (await dbListWorkflows()).find((w) => w.id === definition.id);
3660
+ const unresolved = unresolvedRequirements(portable, connections);
3661
+ const known = await dbListWorkflows();
3662
+ const id = importedWorkflowIdFor(bundle, portable.slug, portable.name, known);
3663
+ const existing = known.find((w) => w.id === id);
3664
+ const definition = { ...resolved, id, enabled: existing ? existing.enabled : false };
3528
3665
  if (existing) {
3529
3666
  await dbUpdateWorkflow(definition.id, definition);
3530
3667
  } else {
3531
3668
  await dbInsertWorkflow(definition);
3532
3669
  }
3533
3670
  dbSignalChange();
3671
+ const pending = unresolved.map(
3672
+ (requirement) => requirement.kind === "httpProfile" ? `${requirement.nodeId} needs an HTTP profile${requirement.name ? ` like "${requirement.name}"` : ""}` : `${requirement.nodeId} needs a ${requirement.connectorId || "connector"} connection${requirement.name ? ` like "${requirement.name}"` : ""}`
3673
+ ).join("; ");
3534
3674
  return {
3535
3675
  content: [
3536
3676
  {
3537
3677
  type: "text",
3538
- text: `${existing ? "Updated" : "Imported"} "${definition.name}" as ${definition.id}, resolved against ${project.path}`
3678
+ text: `${existing ? "Updated" : "Imported"} "${definition.name}" as ${definition.id}, resolved against ${project.path}` + (existing || definition.enabled ? "" : ". It is disabled; enable it when ready") + (pending ? `
3679
+
3680
+ Still to connect: ${pending}` : "")
3539
3681
  }
3540
3682
  ]
3541
3683
  };
@@ -3556,7 +3698,7 @@ function registerConfigTools(server) {
3556
3698
  }
3557
3699
 
3558
3700
  // src/tools/workspaces.ts
3559
- import crypto3 from "crypto";
3701
+ import crypto4 from "crypto";
3560
3702
  import { z as z6 } from "zod";
3561
3703
  function registerWorkspaceTools(server) {
3562
3704
  server.tool("list_workspaces", "List all workspaces", async () => {
@@ -3575,7 +3717,7 @@ function registerWorkspaceTools(server) {
3575
3717
  const existing = await dbListWorkspaces();
3576
3718
  const maxOrder = existing.reduce((max, w) => Math.max(max, w.order), 0);
3577
3719
  const workspace = {
3578
- id: crypto3.randomUUID(),
3720
+ id: crypto4.randomUUID(),
3579
3721
  name: args.name,
3580
3722
  order: maxOrder + 1,
3581
3723
  ...args.icon && { icon: args.icon },
@@ -3766,10 +3908,13 @@ function registerConnectorTools(server) {
3766
3908
  );
3767
3909
  server.tool(
3768
3910
  "install_connector",
3769
- "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.",
3911
+ "Install a connector from a pack file, 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.",
3770
3912
  {
3771
3913
  connector_id: V.id.optional().describe("Catalog connector id (from list_connectors). Use this or package."),
3772
3914
  package: V.shortText.optional().describe("npm package name or launch command"),
3915
+ pack_path: V.shortText.optional().describe(
3916
+ "Path to a .vorn.tgz pack to install first. It is verified and copied to disk, and the connection then launches those files rather than resolving a package."
3917
+ ),
3773
3918
  name: V.title.optional().describe("Connection name (defaults to the connector name)"),
3774
3919
  project: V.name.optional().describe("Vorn project tasks should be created in"),
3775
3920
  trigger: V.shortText.optional().describe("Trigger type to configure (defaults to the first the connector offers)"),
@@ -3784,8 +3929,17 @@ function registerConnectorTools(server) {
3784
3929
  `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.`
3785
3930
  );
3786
3931
  }
3787
- const target = entry ? entry.launch : args.package;
3788
- if (!target) return failure("Provide either connector_id or package.");
3932
+ let installed;
3933
+ if (args.pack_path) {
3934
+ const outcome = await rpcCall("connector:installPack", {
3935
+ kind: "file",
3936
+ path: args.pack_path
3937
+ });
3938
+ if (!outcome.ok) return failure(`The pack was refused: ${outcome.error}`);
3939
+ installed = outcome.pack;
3940
+ }
3941
+ const target = installed ? packLaunch(installed) : entry?.launch ?? args.package;
3942
+ if (!target) return failure("Provide either connector_id, package, or pack_path.");
3789
3943
  const result = await probe(target);
3790
3944
  if (!result.ok) return failure(result.error);
3791
3945
  const manifest = result.manifest;
@@ -3837,6 +3991,7 @@ function registerConnectorTools(server) {
3837
3991
  installed: manifest.name,
3838
3992
  connectionId: connection2.id,
3839
3993
  trigger: trigger?.type,
3994
+ ...installed && { version: installed.version, path: installed.path },
3840
3995
  note: "Poll it now with backfill_connection, or reference it from a workflow."
3841
3996
  });
3842
3997
  }
@@ -3874,6 +4029,9 @@ function registerConnectorTools(server) {
3874
4029
  }
3875
4030
  );
3876
4031
  }
4032
+ function packLaunch(pack) {
4033
+ return { command: "node", args: [`${pack.path}/index.js`] };
4034
+ }
3877
4035
  async function probe(target) {
3878
4036
  const launch = typeof target === "string" ? parseLaunch(target) : target;
3879
4037
  return rpcCall("connector:probeSdk", launch, PROBE_TIMEOUT_MS);
@@ -3895,7 +4053,7 @@ function plural(count, word) {
3895
4053
  }
3896
4054
 
3897
4055
  // src/tools/browser.ts
3898
- import crypto4 from "crypto";
4056
+ import crypto5 from "crypto";
3899
4057
  import { z as z8 } from "zod";
3900
4058
  function sessionId(env = process.env) {
3901
4059
  const id = env.VORN_SESSION_ID;
@@ -3922,7 +4080,7 @@ function source(label) {
3922
4080
  return label.includes("WEB PAGE") || label.includes("BROWSER") ? "page" : "device";
3923
4081
  }
3924
4082
  function pageResult(data, label = "WEB PAGE CONTENT") {
3925
- const nonce = crypto4.randomUUID();
4083
+ const nonce = crypto5.randomUUID();
3926
4084
  return {
3927
4085
  content: [
3928
4086
  {
@@ -4177,6 +4335,7 @@ function registerDeviceTools(server) {
4177
4335
  sessionId: id,
4178
4336
  udid: args.udid
4179
4337
  });
4338
+ if (!r.ok) throw new Error(r.message);
4180
4339
  return {
4181
4340
  content: [{ type: "text", text: `Claimed ${r.name} (${r.udid}).` }]
4182
4341
  };
@@ -4370,7 +4529,7 @@ console.warn = (...args) => _origError("[mcp:warn]", ...args);
4370
4529
  console.error = (...args) => _origError("[mcp:error]", ...args);
4371
4530
  async function main() {
4372
4531
  configManager.init();
4373
- const version = true ? "0.7.0-beta.1" : createRequire(import.meta.url)("../package.json").version;
4532
+ const version = true ? "0.7.0-beta.11" : createRequire(import.meta.url)("../package.json").version;
4374
4533
  const server = createMcpServer(version);
4375
4534
  const transport = new StdioServerTransport();
4376
4535
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vornrun/mcp",
3
- "version": "0.7.0-beta.1",
3
+ "version": "0.7.0-beta.11",
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.7.0-beta.1",
42
- "@vornrun/shared": "0.7.0-beta.1",
41
+ "@vornrun/server": "0.7.0-beta.11",
42
+ "@vornrun/shared": "0.7.0-beta.11",
43
43
  "tsup": "^8.5.1",
44
44
  "tsx": "^4.23.1",
45
45
  "typescript": "^6.0.3"