@hoststack.dev/mcp 0.16.0 → 0.17.0

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.
@@ -8,7 +8,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
8
  import { HostStack } from "@hoststack.dev/sdk";
9
9
 
10
10
  // src/version.ts
11
- var MCP_VERSION = true ? "0.16.0" : "0.0.0-dev";
11
+ var MCP_VERSION = true ? "0.17.0" : "0.0.0-dev";
12
12
  var USER_AGENT = `hoststack-mcp/${MCP_VERSION}`;
13
13
 
14
14
  // src/api-client.ts
@@ -620,7 +620,105 @@ defineTool({
620
620
  });
621
621
 
622
622
  // src/tools/databases.ts
623
+ import { z as z6 } from "zod";
624
+
625
+ // src/tools/machines.ts
623
626
  import { z as z5 } from "zod";
627
+ var machineInput = z5.union([z5.number().int().positive(), z5.string()]).optional().describe(
628
+ `Run this on one of the team's OWN enrolled machines instead of HostStack compute \u2014 the machine's name (e.g. "desktop") or its numeric id. List them with list_machines. Omit for HostStack compute, which is the default and the right answer unless the user asked for their own hardware.`
629
+ );
630
+ async function resolveMachineId(ctx, teamId, machine) {
631
+ const { machines } = await ctx.hoststack.machines.list(teamId);
632
+ if (machines.length === 0) {
633
+ throw new Error(
634
+ "This team has no enrolled machines. Enrol one from the dashboard (Machines \u2192 Add machine) or with `hoststack machines add`, then run the install command it prints on that machine."
635
+ );
636
+ }
637
+ const known = machines.map((m) => `"${m.name}" (id ${m.id})`).join(", ");
638
+ if (typeof machine === "number") {
639
+ const byId = machines.find((m) => m.id === machine);
640
+ if (!byId) throw new Error(`No machine with id ${machine}. This team has: ${known}.`);
641
+ return byId.id;
642
+ }
643
+ const needle = machine.trim().toLowerCase();
644
+ const matches = machines.filter(
645
+ (m) => m.name.toLowerCase() === needle || m.hostname.toLowerCase() === needle
646
+ );
647
+ if (matches.length === 0) {
648
+ throw new Error(`No machine named "${machine}". This team has: ${known}.`);
649
+ }
650
+ if (matches.length > 1) {
651
+ throw new Error(
652
+ `More than one machine is named "${machine}" (ids ${matches.map((m) => m.id).join(", ")}). Pass the numeric id.`
653
+ );
654
+ }
655
+ return matches[0].id;
656
+ }
657
+ function describeMachine(m) {
658
+ if (!m.enrolled) return `${m.name}: registered but never paired`;
659
+ if (m.status !== "active") return `${m.name}: offline`;
660
+ if (m.agentBuild === "from-source") return `${m.name}: online, agent running from source`;
661
+ if (m.agentBuild === "unknown") return `${m.name}: online, agent too old to report its build`;
662
+ return `${m.name}: online`;
663
+ }
664
+ defineTool({
665
+ name: "list_machines",
666
+ category: "machines",
667
+ description: [
668
+ "List the team's OWN enrolled machines \u2014 hardware they supply (a spare desktop, a home server, a VPS they already pay for) that HostStack runs work on. These are NOT HostStack's servers; you cannot enrol, resize or reboot them from here.",
669
+ "",
670
+ "When to use: before creating a service, database or dev box that the user wants on their own hardware (pass the machine name to create_service / create_database / create_standalone_dev_environment), or to explain why something pinned to a machine is down \u2014 a machine that is switched off takes everything on it with it.",
671
+ "",
672
+ "Inputs: none.",
673
+ "",
674
+ "Returns: { items: Machine[] } \u2014 each entry has id (pass this as `machine`), name, status (active|offline|provisioning), enrolled, agentBuild (current|behind|from-source|unknown), agentVersion, lastHeartbeatAt, totalMemoryMb, totalCpuCores, and workloads { devBoxes, services, databases }.",
675
+ "",
676
+ "Notes: a machine is only reachable while its owner has it switched on, and a database pinned to one is reachable ONLY from that same machine \u2014 put the app that queries it there too. Enrolment needs a person at the machine to run an installer, so there is no tool for it: point the user at the dashboard (Machines \u2192 Add machine) or `hoststack machines add`.",
677
+ "",
678
+ 'Example: list_machines() \u2192 { items: [{ id: 12452, name: "desktop", status: "active", workloads: { devBoxes: 8, services: 0, databases: 2 } }] }'
679
+ ].join("\n"),
680
+ input: {},
681
+ handler: async (_args, ctx) => {
682
+ const teamId = await ctx.resolveTeamId();
683
+ const response = await ctx.hoststack.machines.list(teamId);
684
+ const data = shapeList(response, "machines", shape);
685
+ const summary = response.machines.length === 0 ? "This team has no enrolled machines. Everything runs on HostStack compute." : `${response.machines.length} machine${response.machines.length === 1 ? "" : "s"} \u2014 ${response.machines.map(describeMachine).join("; ")}.`;
686
+ return respond({ summary, data });
687
+ }
688
+ });
689
+ defineTool({
690
+ name: "get_machine",
691
+ category: "machines",
692
+ description: [
693
+ "One of the team's own enrolled machines, with what is actually running on it \u2014 rather than the counts list_machines carries.",
694
+ "",
695
+ 'When to use: before advising the user to switch a machine off, move work off it, or remove it \u2014 `running` is the list of things that go away with the machine. Also the fastest answer to "what is on my desktop?".',
696
+ "",
697
+ "Inputs:",
698
+ ' - machine: the machine name (e.g. "desktop") or its numeric id.',
699
+ "",
700
+ 'Returns: { machine: Machine, running: Workload[] } \u2014 each workload has kind ("dev_box" | "service" | "database"), name, status and projectId. The kinds differ in consequence: a dev box waits for its machine to come back, a service is down while it is off, and a database takes down everything reading from it.',
701
+ "",
702
+ 'Example: get_machine({ machine: "desktop" }) \u2192 { machine: { name: "desktop", status: "active" }, running: [{ kind: "database", name: "postgres-58" }] }'
703
+ ].join("\n"),
704
+ input: {
705
+ machine: z5.union([z5.number().int().positive(), z5.string()]).describe('Machine name (e.g. "desktop") or numeric id. List them with list_machines.')
706
+ },
707
+ handler: async (args2, ctx) => {
708
+ const teamId = await ctx.resolveTeamId();
709
+ const machineId = await resolveMachineId(ctx, teamId, args2.machine);
710
+ const response = await ctx.hoststack.machines.get(teamId, machineId);
711
+ const data = {
712
+ machine: shape(response.machine),
713
+ running: shapeList({ running: response.running }, "running", shape).items
714
+ };
715
+ const running = response.running ?? [];
716
+ const summary = running.length === 0 ? `${describeMachine(response.machine)}. Nothing is running on it.` : `${describeMachine(response.machine)}. Running ${running.length} thing${running.length === 1 ? "" : "s"}: ${running.map((w) => `${w.name} (${w.kind})`).join(", ")}.`;
717
+ return respond({ summary, data });
718
+ }
719
+ });
720
+
721
+ // src/tools/databases.ts
624
722
  var DATABASE_VERSIONS = {
625
723
  postgres: { default: "18", supported: ["18", "17", "16", "15"] },
626
724
  redis: { default: "8", supported: ["8", "7", "6"] },
@@ -657,6 +755,7 @@ defineTool({
657
755
  " - environment_id (optional): bind to a specific environment; defaults to the project Production env.",
658
756
  " - postgis (optional): provision the PostGIS image variant so `CREATE EXTENSION postgis` works. Postgres only.",
659
757
  " - pgvector (optional): provision the pgvector image variant so `CREATE EXTENSION vector` works. Postgres only, mutually exclusive with postgis.",
758
+ " - machine (optional): put it on one of the team's OWN enrolled machines (name or id, see list_machines) instead of HostStack compute. It costs nothing there and is still backed up off-site, but it is reachable ONLY from that same machine \u2014 the service that queries it has to run there too, and linking it to a service anywhere else is refused. No HA and no external access on a machine you own.",
660
759
  "",
661
760
  'Returns: { database: Database } \u2014 note BOTH `id` (numeric \u2014 this is what link_resource_to_service wants) and `publicId` ("db_\u2026" \u2014 what the other database tools want).',
662
761
  "",
@@ -665,16 +764,17 @@ defineTool({
665
764
  'Example: create_database({ project_id: "prj_abc", name: "app-db", engine: "postgres" }) \u2192 { database: { id: 42, publicId: "db_\u2026", status: "creating" } }'
666
765
  ].join("\n"),
667
766
  input: {
668
- project_id: z5.union([z5.number().int().positive(), z5.string()]).describe('Target project \u2014 numeric id or publicId ("prj_\u2026").'),
669
- name: z5.string().min(1).max(100).describe("Database name (1\u2013100 chars)."),
670
- engine: z5.enum(DB_ENGINES).describe("Database engine."),
671
- version: z5.string().max(20).optional().describe(`Engine version. ${VERSION_HELP}. Omit for the engine default.`),
672
- plan: z5.enum(["micro", "starter", "standard", "pro"]).optional().describe('Plan tier (memory/CPU). Default "starter".'),
673
- environment_id: z5.union([z5.number().int().positive(), z5.string()]).optional().describe("Environment to bind to. Defaults to the project Production env."),
674
- postgis: z5.boolean().optional().describe("Postgres only \u2014 enable the PostGIS extension image."),
675
- pgvector: z5.boolean().optional().describe(
767
+ project_id: z6.union([z6.number().int().positive(), z6.string()]).describe('Target project \u2014 numeric id or publicId ("prj_\u2026").'),
768
+ name: z6.string().min(1).max(100).describe("Database name (1\u2013100 chars)."),
769
+ engine: z6.enum(DB_ENGINES).describe("Database engine."),
770
+ version: z6.string().max(20).optional().describe(`Engine version. ${VERSION_HELP}. Omit for the engine default.`),
771
+ plan: z6.enum(["micro", "starter", "standard", "pro"]).optional().describe('Plan tier (memory/CPU). Default "starter".'),
772
+ environment_id: z6.union([z6.number().int().positive(), z6.string()]).optional().describe("Environment to bind to. Defaults to the project Production env."),
773
+ postgis: z6.boolean().optional().describe("Postgres only \u2014 enable the PostGIS extension image."),
774
+ pgvector: z6.boolean().optional().describe(
676
775
  "Postgres only \u2014 enable the pgvector extension image. Exclusive with postgis."
677
- )
776
+ ),
777
+ machine: machineInput
678
778
  },
679
779
  handler: async (args2, ctx) => {
680
780
  const teamId = await ctx.resolveTeamId();
@@ -697,6 +797,9 @@ defineTool({
697
797
  }
698
798
  if (args2.postgis !== void 0) input.postgis = args2.postgis;
699
799
  if (args2.pgvector !== void 0) input.pgvector = args2.pgvector;
800
+ if (args2.machine !== void 0) {
801
+ input.machineId = await resolveMachineId(ctx, teamId, args2.machine);
802
+ }
700
803
  const response = await ctx.hoststack.databases.create(teamId, input);
701
804
  const data = { database: shapeDatabase(response.database) };
702
805
  const db = response.database;
@@ -724,7 +827,7 @@ defineTool({
724
827
  'Example: delete_database({ database_id: "db_abc" }) \u2192 { ok: true }'
725
828
  ].join("\n"),
726
829
  input: {
727
- database_id: z5.string().describe("Database publicId (e.g. db_abc) to permanently delete.")
830
+ database_id: z6.string().describe("Database publicId (e.g. db_abc) to permanently delete.")
728
831
  },
729
832
  handler: async (args2, ctx) => {
730
833
  const teamId = await ctx.resolveTeamId();
@@ -750,7 +853,7 @@ defineTool({
750
853
  'Example: suspend_database({ database_id: "db_abc" }) \u2192 { ok: true }'
751
854
  ].join("\n"),
752
855
  input: {
753
- database_id: z5.string().describe("Database publicId (e.g. db_abc) to suspend.")
856
+ database_id: z6.string().describe("Database publicId (e.g. db_abc) to suspend.")
754
857
  },
755
858
  handler: async (args2, ctx) => {
756
859
  const teamId = await ctx.resolveTeamId();
@@ -776,7 +879,7 @@ defineTool({
776
879
  'Example: resume_database({ database_id: "db_abc" }) \u2192 { ok: true }'
777
880
  ].join("\n"),
778
881
  input: {
779
- database_id: z5.string().describe("Database publicId (e.g. db_abc) to resume.")
882
+ database_id: z6.string().describe("Database publicId (e.g. db_abc) to resume.")
780
883
  },
781
884
  handler: async (args2, ctx) => {
782
885
  const teamId = await ctx.resolveTeamId();
@@ -802,7 +905,7 @@ defineTool({
802
905
  'Example: list_databases({ project_id: 12 }) \u2192 { items: [{ publicId: "db_\u2026", type: "postgres", status: "running", \u2026 }] }'
803
906
  ].join("\n"),
804
907
  input: {
805
- project_id: z5.number().int().positive().describe("Numeric project ID (from list_projects).")
908
+ project_id: z6.number().int().positive().describe("Numeric project ID (from list_projects).")
806
909
  },
807
910
  handler: async (args2, ctx) => {
808
911
  const teamId = await ctx.resolveTeamId();
@@ -831,10 +934,10 @@ defineTool({
831
934
  'Example: update_database({ database_id: "db_xyz", disk_size_gb: 50 }) \u2192 { database: { diskSizeGb: 50, \u2026 } }'
832
935
  ].join("\n"),
833
936
  input: {
834
- database_id: z5.string().describe("Database publicId (e.g. db_xyz)."),
835
- name: z5.string().min(1).max(100).optional().describe("New database name."),
836
- plan: z5.enum(["free", "micro", "starter", "standard", "pro"]).optional().describe("Plan tier (memory/CPU)."),
837
- disk_size_gb: z5.number().int().min(1).max(1024).optional().describe("New disk size in GB. Must be \u2265 current.")
937
+ database_id: z6.string().describe("Database publicId (e.g. db_xyz)."),
938
+ name: z6.string().min(1).max(100).optional().describe("New database name."),
939
+ plan: z6.enum(["free", "micro", "starter", "standard", "pro"]).optional().describe("Plan tier (memory/CPU)."),
940
+ disk_size_gb: z6.number().int().min(1).max(1024).optional().describe("New disk size in GB. Must be \u2265 current.")
838
941
  },
839
942
  handler: async (args2, ctx) => {
840
943
  const teamId = await ctx.resolveTeamId();
@@ -867,7 +970,7 @@ defineTool({
867
970
  'Example: get_database({ database_id: "db_xyz" }) \u2192 { database: { type: "postgres", version: "16", status: "running", \u2026 } }'
868
971
  ].join("\n"),
869
972
  input: {
870
- database_id: z5.string().describe("Database publicId (e.g. db_xyz).")
973
+ database_id: z6.string().describe("Database publicId (e.g. db_xyz).")
871
974
  },
872
975
  handler: async (args2, ctx) => {
873
976
  const teamId = await ctx.resolveTeamId();
@@ -905,8 +1008,8 @@ defineTool({
905
1008
  ` - query_database({ database_id: "db_abc", sql: "SELECT id, email FROM users WHERE created_at > now() - interval '1 day' LIMIT 50" })`
906
1009
  ].join("\n"),
907
1010
  input: {
908
- database_id: z5.string().describe("Database publicId (e.g. db_abc)."),
909
- sql: z5.string().min(1).max(16384).describe(
1011
+ database_id: z6.string().describe("Database publicId (e.g. db_abc)."),
1012
+ sql: z6.string().min(1).max(16384).describe(
910
1013
  "A single READ-ONLY SQL statement. No trailing `;`, no embedded `;`, no psql meta-commands."
911
1014
  )
912
1015
  },
@@ -935,7 +1038,7 @@ defineTool({
935
1038
  'Example: upgrade_database_to_ha({ database_id: "db_abc" }) \u2192 { ok: true }'
936
1039
  ].join("\n"),
937
1040
  input: {
938
- database_id: z5.string().describe("Database publicId (e.g. db_abc) to upgrade to HA.")
1041
+ database_id: z6.string().describe("Database publicId (e.g. db_abc) to upgrade to HA.")
939
1042
  },
940
1043
  handler: async (args2, ctx) => {
941
1044
  const teamId = await ctx.resolveTeamId();
@@ -964,8 +1067,8 @@ defineTool({
964
1067
  'Example: upgrade_database_version({ database_id: "db_abc", version: "18" }) \u2192 { ok: true }'
965
1068
  ].join("\n"),
966
1069
  input: {
967
- database_id: z5.string().describe("Database publicId (e.g. db_abc) to upgrade."),
968
- version: z5.string().describe('Target engine version, e.g. "18" for postgres or "8" for redis.')
1070
+ database_id: z6.string().describe("Database publicId (e.g. db_abc) to upgrade."),
1071
+ version: z6.string().describe('Target engine version, e.g. "18" for postgres or "8" for redis.')
969
1072
  },
970
1073
  handler: async (args2, ctx) => {
971
1074
  const teamId = await ctx.resolveTeamId();
@@ -993,7 +1096,7 @@ defineTool({
993
1096
  'Example: restart_database({ database_id: "db_abc" }) \u2192 { ok: true }'
994
1097
  ].join("\n"),
995
1098
  input: {
996
- database_id: z5.string().describe("Database publicId (e.g. db_abc) to restart.")
1099
+ database_id: z6.string().describe("Database publicId (e.g. db_abc) to restart.")
997
1100
  },
998
1101
  handler: async (args2, ctx) => {
999
1102
  const teamId = await ctx.resolveTeamId();
@@ -1019,7 +1122,7 @@ defineTool({
1019
1122
  'Example: get_database_cluster({ database_id: "db_abc" }) \u2192 { members: [...5 live rows], failovers: [] }'
1020
1123
  ].join("\n"),
1021
1124
  input: {
1022
- database_id: z5.string().describe("Database publicId for the HA cluster.")
1125
+ database_id: z6.string().describe("Database publicId for the HA cluster.")
1023
1126
  },
1024
1127
  handler: async (args2, ctx) => {
1025
1128
  const teamId = await ctx.resolveTeamId();
@@ -1031,7 +1134,7 @@ defineTool({
1031
1134
  });
1032
1135
 
1033
1136
  // src/tools/deploys.ts
1034
- import { z as z6 } from "zod";
1137
+ import { z as z7 } from "zod";
1035
1138
  defineTool({
1036
1139
  name: "list_deploys",
1037
1140
  category: "deploys",
@@ -1048,7 +1151,7 @@ defineTool({
1048
1151
  'Example: list_deploys({ service_id: "svc_abc" }) \u2192 { items: [{ publicId: "dpl_\u2026", status: "live", commitMessage: "Fix login", \u2026 }, \u2026] }'
1049
1152
  ].join("\n"),
1050
1153
  input: {
1051
- service_id: z6.string().describe("Service publicId (e.g. svc_abc123).")
1154
+ service_id: z7.string().describe("Service publicId (e.g. svc_abc123).")
1052
1155
  },
1053
1156
  handler: async (args2, ctx) => {
1054
1157
  const teamId = await ctx.resolveTeamId();
@@ -1075,8 +1178,8 @@ defineTool({
1075
1178
  'Example: get_deploy({ service_id: "svc_abc", deploy_id: "dpl_xyz" }) \u2192 { deploy: { status: "live", \u2026 } }'
1076
1179
  ].join("\n"),
1077
1180
  input: {
1078
- service_id: z6.string().describe("Service publicId."),
1079
- deploy_id: z6.string().describe("Deploy publicId.")
1181
+ service_id: z7.string().describe("Service publicId."),
1182
+ deploy_id: z7.string().describe("Deploy publicId.")
1080
1183
  },
1081
1184
  handler: async (args2, ctx) => {
1082
1185
  const teamId = await ctx.resolveTeamId();
@@ -1104,9 +1207,9 @@ defineTool({
1104
1207
  'Example: trigger_deploy({ service_id: "svc_abc" }) \u2192 { deploy: { publicId: "dpl_\u2026", status: "pending", \u2026 } }'
1105
1208
  ].join("\n"),
1106
1209
  input: {
1107
- service_id: z6.string().describe("Service publicId."),
1108
- commit_hash: z6.string().max(40).optional().describe("Specific commit to build (default: branch HEAD)."),
1109
- branch: z6.string().max(200).optional().describe("Branch to build (default: service configured branch).")
1210
+ service_id: z7.string().describe("Service publicId."),
1211
+ commit_hash: z7.string().max(40).optional().describe("Specific commit to build (default: branch HEAD)."),
1212
+ branch: z7.string().max(200).optional().describe("Branch to build (default: service configured branch).")
1110
1213
  },
1111
1214
  handler: async (args2, ctx) => {
1112
1215
  const teamId = await ctx.resolveTeamId();
@@ -1139,8 +1242,8 @@ defineTool({
1139
1242
  'Example: cancel_deploy({ service_id: "svc_abc", deploy_id: "dpl_xyz" }) \u2192 { ok: true }'
1140
1243
  ].join("\n"),
1141
1244
  input: {
1142
- service_id: z6.string().describe("Service publicId."),
1143
- deploy_id: z6.string().describe("Deploy publicId.")
1245
+ service_id: z7.string().describe("Service publicId."),
1246
+ deploy_id: z7.string().describe("Deploy publicId.")
1144
1247
  },
1145
1248
  handler: async (args2, ctx) => {
1146
1249
  const teamId = await ctx.resolveTeamId();
@@ -1168,10 +1271,10 @@ defineTool({
1168
1271
  'Example: get_deploy_logs({ service_id: "svc_abc", deploy_id: "dpl_xyz" }) \u2192 { logs: "Building\u2026\\nStep 1/8\u2026\\n\u2026", lineCount: 42, nextAfterId: 1234 }'
1169
1272
  ].join("\n"),
1170
1273
  input: {
1171
- service_id: z6.string().describe("Service publicId."),
1172
- deploy_id: z6.string().describe("Deploy publicId."),
1173
- after_id: z6.number().int().optional().describe("Pagination cursor from a previous response\u2019s nextAfterId."),
1174
- limit: z6.number().int().min(1).max(5e3).optional().describe("Max log entries to fetch (default 500, hard cap 5000).")
1274
+ service_id: z7.string().describe("Service publicId."),
1275
+ deploy_id: z7.string().describe("Deploy publicId."),
1276
+ after_id: z7.number().int().optional().describe("Pagination cursor from a previous response\u2019s nextAfterId."),
1277
+ limit: z7.number().int().min(1).max(5e3).optional().describe("Max log entries to fetch (default 500, hard cap 5000).")
1175
1278
  },
1176
1279
  handler: async (args2, ctx) => {
1177
1280
  const teamId = await ctx.resolveTeamId();
@@ -1211,10 +1314,10 @@ defineTool({
1211
1314
  'Example: diagnose_deploy({ service_id: "svc_abc", deploy_id: "dpl_xyz" }) \u2192 all three sections in one call.'
1212
1315
  ].join("\n"),
1213
1316
  input: {
1214
- service_id: z6.string().describe("Service publicId."),
1215
- deploy_id: z6.string().describe("Deploy publicId."),
1216
- build_log_lines: z6.number().int().min(1).max(2e3).optional().describe("Trailing build log lines. Default 200, max 2000."),
1217
- runtime_log_lines: z6.number().int().min(0).max(1e3).optional().describe(
1317
+ service_id: z7.string().describe("Service publicId."),
1318
+ deploy_id: z7.string().describe("Deploy publicId."),
1319
+ build_log_lines: z7.number().int().min(1).max(2e3).optional().describe("Trailing build log lines. Default 200, max 2000."),
1320
+ runtime_log_lines: z7.number().int().min(0).max(1e3).optional().describe(
1218
1321
  "Trailing runtime log lines since deploy start. Default 100, max 1000. 0 = skip."
1219
1322
  )
1220
1323
  },
@@ -1275,7 +1378,7 @@ defineTool({
1275
1378
  });
1276
1379
 
1277
1380
  // src/tools/dns-records.ts
1278
- import { z as z7 } from "zod";
1381
+ import { z as z8 } from "zod";
1279
1382
  var DNS_RECORD_TYPES = [
1280
1383
  "A",
1281
1384
  "AAAA",
@@ -1290,7 +1393,7 @@ var DNS_RECORD_TYPES = [
1290
1393
  async function resolveZonePublicId(hoststack, teamId, input) {
1291
1394
  if (input.zone_id) {
1292
1395
  const { zones: zones2 } = await hoststack.dns.listZones(teamId);
1293
- const match = zones2.find((z16) => z16.publicId === input.zone_id);
1396
+ const match = zones2.find((z17) => z17.publicId === input.zone_id);
1294
1397
  if (!match) {
1295
1398
  throw new Error(`Zone ${input.zone_id} not found on this team.`);
1296
1399
  }
@@ -1304,7 +1407,7 @@ async function resolveZonePublicId(hoststack, teamId, input) {
1304
1407
  const labels = fqdn.split(".");
1305
1408
  for (let i = 0; i < labels.length - 1; i++) {
1306
1409
  const candidate = labels.slice(i).join(".");
1307
- const match = zones.find((z16) => z16.domainName.toLowerCase() === candidate);
1410
+ const match = zones.find((z17) => z17.domainName.toLowerCase() === candidate);
1308
1411
  if (match && match.status !== "deleting") {
1309
1412
  return { publicId: match.publicId, domainName: match.domainName };
1310
1413
  }
@@ -1349,8 +1452,8 @@ defineTool({
1349
1452
  'Example: list_dns_records({ domain: "micci.dk" }) \u2192 { zone: { domainName: "micci.dk", \u2026 }, items: [{ type: "TXT", name: "@", value: "google-site-verification=\u2026", ttl: 300, \u2026 }] }'
1350
1453
  ].join("\n"),
1351
1454
  input: {
1352
- zone_id: z7.string().optional().describe('Zone publicId (e.g. "dnz_abc").'),
1353
- domain: z7.string().optional().describe("Apex or subdomain \u2014 resolves to the longest-matching hosted zone.")
1455
+ zone_id: z8.string().optional().describe('Zone publicId (e.g. "dnz_abc").'),
1456
+ domain: z8.string().optional().describe("Apex or subdomain \u2014 resolves to the longest-matching hosted zone.")
1354
1457
  },
1355
1458
  handler: async (args2, ctx) => {
1356
1459
  const teamId = await ctx.resolveTeamId();
@@ -1383,7 +1486,7 @@ defineTool({
1383
1486
  'Example: get_dns_record({ record_id: "dnr_abc" }) \u2192 { record: { type: "TXT", name: "@", value: "\u2026", status: "active", \u2026 } }'
1384
1487
  ].join("\n"),
1385
1488
  input: {
1386
- record_id: z7.string().describe("Record publicId.")
1489
+ record_id: z8.string().describe("Record publicId.")
1387
1490
  },
1388
1491
  handler: async (args2, ctx) => {
1389
1492
  const teamId = await ctx.resolveTeamId();
@@ -1429,13 +1532,13 @@ defineTool({
1429
1532
  'Example: create_dns_record({ domain: "micci.dk", type: "TXT", name: "@", content: "google-site-verification=C0V0scK48g2\u2026", ttl: 300 })'
1430
1533
  ].join("\n"),
1431
1534
  input: {
1432
- zone_id: z7.string().optional().describe("Zone publicId."),
1433
- domain: z7.string().optional().describe("Apex or subdomain to resolve a hosted zone."),
1434
- type: z7.enum(DNS_RECORD_TYPES).describe("DNS record type."),
1435
- name: z7.string().min(1).max(253).describe('"@" for apex, or a bare label. Do NOT include the zone suffix.'),
1436
- content: z7.string().min(1).max(2e3).describe("Record value; TXT values are unquoted."),
1437
- ttl: z7.number().int().min(60).max(86400).optional().describe("TTL in seconds (default 3600)."),
1438
- priority: z7.number().int().min(0).max(65535).optional().describe("Required for MX / SRV.")
1535
+ zone_id: z8.string().optional().describe("Zone publicId."),
1536
+ domain: z8.string().optional().describe("Apex or subdomain to resolve a hosted zone."),
1537
+ type: z8.enum(DNS_RECORD_TYPES).describe("DNS record type."),
1538
+ name: z8.string().min(1).max(253).describe('"@" for apex, or a bare label. Do NOT include the zone suffix.'),
1539
+ content: z8.string().min(1).max(2e3).describe("Record value; TXT values are unquoted."),
1540
+ ttl: z8.number().int().min(60).max(86400).optional().describe("TTL in seconds (default 3600)."),
1541
+ priority: z8.number().int().min(0).max(65535).optional().describe("Required for MX / SRV.")
1439
1542
  },
1440
1543
  handler: async (args2, ctx) => {
1441
1544
  if ((args2.type === "MX" || args2.type === "SRV") && args2.priority === void 0) {
@@ -1481,12 +1584,12 @@ defineTool({
1481
1584
  'Example: update_dns_record({ record_id: "dnr_abc", type: "A", name: "api", content: "203.0.113.42", ttl: 300 })'
1482
1585
  ].join("\n"),
1483
1586
  input: {
1484
- record_id: z7.string().describe("Record publicId."),
1485
- type: z7.enum(DNS_RECORD_TYPES).describe("DNS record type."),
1486
- name: z7.string().min(1).max(253).describe('"@" for apex, or a bare label. Do NOT include the zone suffix.'),
1487
- content: z7.string().min(1).max(2e3).describe("Record value; TXT unquoted."),
1488
- ttl: z7.number().int().min(60).max(86400).optional().describe("TTL in seconds (default 3600)."),
1489
- priority: z7.number().int().min(0).max(65535).optional().describe("Required for MX / SRV.")
1587
+ record_id: z8.string().describe("Record publicId."),
1588
+ type: z8.enum(DNS_RECORD_TYPES).describe("DNS record type."),
1589
+ name: z8.string().min(1).max(253).describe('"@" for apex, or a bare label. Do NOT include the zone suffix.'),
1590
+ content: z8.string().min(1).max(2e3).describe("Record value; TXT unquoted."),
1591
+ ttl: z8.number().int().min(60).max(86400).optional().describe("TTL in seconds (default 3600)."),
1592
+ priority: z8.number().int().min(0).max(65535).optional().describe("Required for MX / SRV.")
1490
1593
  },
1491
1594
  handler: async (args2, ctx) => {
1492
1595
  if ((args2.type === "MX" || args2.type === "SRV") && args2.priority === void 0) {
@@ -1540,7 +1643,7 @@ defineTool({
1540
1643
  'Example: delete_dns_record({ record_id: "dnr_abc" }) \u2192 { ok: true }'
1541
1644
  ].join("\n"),
1542
1645
  input: {
1543
- record_id: z7.string().describe("Record publicId.")
1646
+ record_id: z8.string().describe("Record publicId.")
1544
1647
  },
1545
1648
  handler: async (args2, ctx) => {
1546
1649
  const teamId = await ctx.resolveTeamId();
@@ -1573,7 +1676,7 @@ defineTool({
1573
1676
  'Example: resync_dns_record({ record_id: "dnr_abc" }) \u2192 { record: { status: "active", ... } }'
1574
1677
  ].join("\n"),
1575
1678
  input: {
1576
- record_id: z7.string().describe("Record publicId.")
1679
+ record_id: z8.string().describe("Record publicId.")
1577
1680
  },
1578
1681
  handler: async (args2, ctx) => {
1579
1682
  const teamId = await ctx.resolveTeamId();
@@ -1611,7 +1714,7 @@ async function findRecordZone(hoststack, teamId, recordPublicId) {
1611
1714
  }
1612
1715
 
1613
1716
  // src/tools/domains.ts
1614
- import { z as z8 } from "zod";
1717
+ import { z as z9 } from "zod";
1615
1718
  defineTool({
1616
1719
  name: "list_domains",
1617
1720
  category: "domains",
@@ -1651,9 +1754,9 @@ defineTool({
1651
1754
  'Example: add_domain({ hostname: "api.example.com", service_id: "svc_abc" }) \u2192 { domain: { hostname: "api.example.com", dnsTargets: [...], verified: false, \u2026 } }'
1652
1755
  ].join("\n"),
1653
1756
  input: {
1654
- hostname: z8.string().min(3).max(253).describe("Fully-qualified hostname (e.g. api.example.com)."),
1655
- service_id: z8.string().describe("Service publicId to bind to."),
1656
- path_prefix: z8.string().optional().describe('Optional URL prefix for path-based routing (e.g. "/api").')
1757
+ hostname: z9.string().min(3).max(253).describe("Fully-qualified hostname (e.g. api.example.com)."),
1758
+ service_id: z9.string().describe("Service publicId to bind to."),
1759
+ path_prefix: z9.string().optional().describe('Optional URL prefix for path-based routing (e.g. "/api").')
1657
1760
  },
1658
1761
  handler: async (args2, ctx) => {
1659
1762
  const teamId = await ctx.resolveTeamId();
@@ -1687,7 +1790,7 @@ defineTool({
1687
1790
  'Example: verify_domain({ domain_id: "dom_xyz" }) \u2192 { ok: true }'
1688
1791
  ].join("\n"),
1689
1792
  input: {
1690
- domain_id: z8.string().describe("Domain publicId.")
1793
+ domain_id: z9.string().describe("Domain publicId.")
1691
1794
  },
1692
1795
  handler: async (args2, ctx) => {
1693
1796
  const teamId = await ctx.resolveTeamId();
@@ -1714,7 +1817,7 @@ defineTool({
1714
1817
  'Example: remove_domain({ domain_id: "dom_xyz" }) \u2192 { ok: true }'
1715
1818
  ].join("\n"),
1716
1819
  input: {
1717
- domain_id: z8.string().describe("Domain publicId.")
1820
+ domain_id: z9.string().describe("Domain publicId.")
1718
1821
  },
1719
1822
  handler: async (args2, ctx) => {
1720
1823
  const teamId = await ctx.resolveTeamId();
@@ -1724,7 +1827,7 @@ defineTool({
1724
1827
  });
1725
1828
 
1726
1829
  // src/tools/env-vars.ts
1727
- import { z as z9 } from "zod";
1830
+ import { z as z10 } from "zod";
1728
1831
  defineTool({
1729
1832
  name: "list_env_vars",
1730
1833
  category: "env-vars",
@@ -1741,7 +1844,7 @@ defineTool({
1741
1844
  'Example: list_env_vars({ service_id: "svc_abc" }) \u2192 { items: [{ key: "DATABASE_URL", value: "\u2022\u2022\u2022\u2022\u2022\u2022", isSecret: true }, { key: "PORT", value: "3000", isSecret: false }] }'
1742
1845
  ].join("\n"),
1743
1846
  input: {
1744
- service_id: z9.string().describe("Service publicId.")
1847
+ service_id: z10.string().describe("Service publicId.")
1745
1848
  },
1746
1849
  handler: async (args2, ctx) => {
1747
1850
  const teamId = await ctx.resolveTeamId();
@@ -1771,11 +1874,11 @@ defineTool({
1771
1874
  'Example: set_env_var({ service_id: "svc_abc", key: "DATABASE_URL", value: "postgres://\u2026", is_secret: true }) \u2192 { envVar: { key: "DATABASE_URL", value: "\u2022\u2022\u2022\u2022\u2022\u2022", \u2026 }, action: "updated" }'
1772
1875
  ].join("\n"),
1773
1876
  input: {
1774
- service_id: z9.string().describe("Service publicId."),
1775
- key: z9.string().min(1).max(128).describe("Env-var key."),
1776
- value: z9.string().describe("New value."),
1777
- is_secret: z9.boolean().optional().describe("Mark as secret (encrypted, masked on read). Default true."),
1778
- target: z9.enum(["build", "runtime", "both"]).optional().describe("Injection target: build, runtime, or both. Default both.")
1877
+ service_id: z10.string().describe("Service publicId."),
1878
+ key: z10.string().min(1).max(128).describe("Env-var key."),
1879
+ value: z10.string().describe("New value."),
1880
+ is_secret: z10.boolean().optional().describe("Mark as secret (encrypted, masked on read). Default true."),
1881
+ target: z10.enum(["build", "runtime", "both"]).optional().describe("Injection target: build, runtime, or both. Default both.")
1779
1882
  },
1780
1883
  handler: async (args2, ctx) => {
1781
1884
  const teamId = await ctx.resolveTeamId();
@@ -1827,8 +1930,8 @@ defineTool({
1827
1930
  'Example: delete_env_var({ service_id: "svc_abc", key: "OLD_FLAG" }) \u2192 { ok: true }'
1828
1931
  ].join("\n"),
1829
1932
  input: {
1830
- service_id: z9.string().describe("Service publicId."),
1831
- key: z9.string().min(1).max(128).describe("Env-var key to delete.")
1933
+ service_id: z10.string().describe("Service publicId."),
1934
+ key: z10.string().min(1).max(128).describe("Env-var key to delete.")
1832
1935
  },
1833
1936
  handler: async (args2, ctx) => {
1834
1937
  const teamId = await ctx.resolveTeamId();
@@ -1864,13 +1967,13 @@ defineTool({
1864
1967
  'Example: bulk_set_env_vars({ service_id: "svc_abc", env_vars: [{ key: "PORT", value: "3000", is_secret: false }, { key: "DATABASE_URL", value: "\u2026", is_secret: true }] }) \u2192 { ok: true }'
1865
1968
  ].join("\n"),
1866
1969
  input: {
1867
- service_id: z9.string().describe("Service publicId."),
1868
- env_vars: z9.array(
1869
- z9.object({
1870
- key: z9.string().min(1).max(128),
1871
- value: z9.string(),
1872
- is_secret: z9.boolean().optional(),
1873
- target: z9.enum(["build", "runtime", "both"]).optional()
1970
+ service_id: z10.string().describe("Service publicId."),
1971
+ env_vars: z10.array(
1972
+ z10.object({
1973
+ key: z10.string().min(1).max(128),
1974
+ value: z10.string(),
1975
+ is_secret: z10.boolean().optional(),
1976
+ target: z10.enum(["build", "runtime", "both"]).optional()
1874
1977
  })
1875
1978
  ).max(500).describe("Array of env-var rows. Hard cap 500.")
1876
1979
  },
@@ -1896,7 +1999,7 @@ defineTool({
1896
1999
  });
1897
2000
 
1898
2001
  // src/tools/environments.ts
1899
- import { z as z10 } from "zod";
2002
+ import { z as z11 } from "zod";
1900
2003
  defineTool({
1901
2004
  name: "list_environments",
1902
2005
  category: "environments",
@@ -1917,7 +2020,7 @@ defineTool({
1917
2020
  'Example: list_environments({ project_id: "prj_abc" }) \u2192 { items: [{ name: "Production", type: "production", isDefault: true }, { name: "Staging", type: "staging" }] }'
1918
2021
  ].join("\n"),
1919
2022
  input: {
1920
- project_id: z10.string().describe("Project publicId.")
2023
+ project_id: z11.string().describe("Project publicId.")
1921
2024
  },
1922
2025
  handler: async (args2, ctx) => {
1923
2026
  const teamId = await ctx.resolveTeamId();
@@ -1951,13 +2054,13 @@ defineTool({
1951
2054
  'Example: create_environment({ project_id: "prj_abc", name: "Staging", type: "staging" }) \u2192 { environment: { id: 7, publicId: "env_\u2026", name: "Staging", type: "staging" } }'
1952
2055
  ].join("\n"),
1953
2056
  input: {
1954
- project_id: z10.string().describe("Project publicId."),
1955
- name: z10.string().min(1).max(64).describe("Environment name (1\u201364 chars)."),
1956
- type: z10.enum(["production", "staging", "development", "preview"]).describe(
2057
+ project_id: z11.string().describe("Project publicId."),
2058
+ name: z11.string().min(1).max(64).describe("Environment name (1\u201364 chars)."),
2059
+ type: z11.enum(["production", "staging", "development", "preview"]).describe(
1957
2060
  // §2.1: "development" here is a project deployment env, NOT a cloud Dev Box.
1958
2061
  'Environment type \u2014 drives the hostname suffix. type="development" creates a PROJECT deployment environment (a -dev hostname suffix), NOT an agentic Dev Box \u2014 for a cloud dev box use create_dev_environment / create_standalone_dev_environment / spin_up_dev_environment.'
1959
2062
  ),
1960
- is_protected: z10.boolean().optional().describe("Require admin role for destructive actions. Default false.")
2063
+ is_protected: z11.boolean().optional().describe("Require admin role for destructive actions. Default false.")
1961
2064
  },
1962
2065
  handler: async (args2, ctx) => {
1963
2066
  const teamId = await ctx.resolveTeamId();
@@ -1991,8 +2094,8 @@ defineTool({
1991
2094
  'Example: delete_environment({ project_id: "prj_abc", environment_id: "env_xyz" }) \u2192 { success: true }'
1992
2095
  ].join("\n"),
1993
2096
  input: {
1994
- project_id: z10.string().describe("Project publicId."),
1995
- environment_id: z10.union([z10.string(), z10.number()]).describe("Environment publicId or numeric id.")
2097
+ project_id: z11.string().describe("Project publicId."),
2098
+ environment_id: z11.union([z11.string(), z11.number()]).describe("Environment publicId or numeric id.")
1996
2099
  },
1997
2100
  handler: async (args2, ctx) => {
1998
2101
  const teamId = await ctx.resolveTeamId();
@@ -2023,9 +2126,9 @@ defineTool({
2023
2126
  'Example: promote_deploy({ service_id: "svc_staging", deploy_id: "dpl_built", target_environment_id: "env_prod" }) \u2192 { deploy: { id: 99, status: "pending", trigger: "rollback", dockerImageId: "sha256:\u2026" } }'
2024
2127
  ].join("\n"),
2025
2128
  input: {
2026
- service_id: z10.string().describe("Source service publicId."),
2027
- deploy_id: z10.string().describe("Source deploy publicId (must have a built image)."),
2028
- target_environment_id: z10.union([z10.string(), z10.number()]).describe("Target environment publicId or numeric id.")
2129
+ service_id: z11.string().describe("Source service publicId."),
2130
+ deploy_id: z11.string().describe("Source deploy publicId (must have a built image)."),
2131
+ target_environment_id: z11.union([z11.string(), z11.number()]).describe("Target environment publicId or numeric id.")
2029
2132
  },
2030
2133
  handler: async (args2, ctx) => {
2031
2134
  const teamId = await ctx.resolveTeamId();
@@ -2154,7 +2257,7 @@ defineTool({
2154
2257
  });
2155
2258
 
2156
2259
  // src/tools/notifications.ts
2157
- import { z as z11 } from "zod";
2260
+ import { z as z12 } from "zod";
2158
2261
  var NOTIFICATION_EVENTS = [
2159
2262
  "deploy.started",
2160
2263
  "deploy.succeeded",
@@ -2216,10 +2319,10 @@ defineTool({
2216
2319
  "Example: create_notification_channel({ type: 'slack', name: 'eng-alerts', webhook_url: 'https://hooks.slack.com/\u2026', events: ['deploy.failed', 'git.auth_failed', 'service.restart_failed'] })"
2217
2320
  ].join("\n"),
2218
2321
  input: {
2219
- type: z11.enum(["slack", "discord", "email"]).describe("Channel type."),
2220
- name: z11.string().min(1).max(128).describe("Human-readable label."),
2221
- webhook_url: z11.string().max(500).describe("Slack/Discord webhook URL or email address (when type=email)."),
2222
- events: z11.array(z11.enum(NOTIFICATION_EVENTS)).describe(
2322
+ type: z12.enum(["slack", "discord", "email"]).describe("Channel type."),
2323
+ name: z12.string().min(1).max(128).describe("Human-readable label."),
2324
+ webhook_url: z12.string().max(500).describe("Slack/Discord webhook URL or email address (when type=email)."),
2325
+ events: z12.array(z12.enum(NOTIFICATION_EVENTS)).describe(
2223
2326
  "List of events the channel subscribes to. Empty list = subscribe to nothing."
2224
2327
  )
2225
2328
  },
@@ -2257,10 +2360,10 @@ defineTool({
2257
2360
  "Example: update_notification_channel({ channel_id: 3, events: ['deploy.failed', 'service.restart_failed', 'git.auth_failed'] })"
2258
2361
  ].join("\n"),
2259
2362
  input: {
2260
- channel_id: z11.number().int().positive().describe("Numeric channel id from list_notification_channels."),
2261
- name: z11.string().min(1).max(128).optional().describe("New label."),
2262
- active: z11.boolean().optional().describe("false silences without deleting."),
2263
- events: z11.array(z11.enum(NOTIFICATION_EVENTS)).optional().describe("Replaces the full subscription list.")
2363
+ channel_id: z12.number().int().positive().describe("Numeric channel id from list_notification_channels."),
2364
+ name: z12.string().min(1).max(128).optional().describe("New label."),
2365
+ active: z12.boolean().optional().describe("false silences without deleting."),
2366
+ events: z12.array(z12.enum(NOTIFICATION_EVENTS)).optional().describe("Replaces the full subscription list.")
2264
2367
  },
2265
2368
  handler: async (args2, ctx) => {
2266
2369
  const teamId = await ctx.resolveTeamId();
@@ -2299,7 +2402,7 @@ defineTool({
2299
2402
  "Example: delete_notification_channel({ channel_id: 3 }) \u2192 { ok: true }"
2300
2403
  ].join("\n"),
2301
2404
  input: {
2302
- channel_id: z11.number().int().positive().describe("Numeric channel id.")
2405
+ channel_id: z12.number().int().positive().describe("Numeric channel id.")
2303
2406
  },
2304
2407
  handler: async (args2, ctx) => {
2305
2408
  const teamId = await ctx.resolveTeamId();
@@ -2326,7 +2429,7 @@ defineTool({
2326
2429
  "Example: test_notification_channel({ channel_id: 3 }) \u2192 { success: true }"
2327
2430
  ].join("\n"),
2328
2431
  input: {
2329
- channel_id: z11.number().int().positive().describe("Numeric channel id.")
2432
+ channel_id: z12.number().int().positive().describe("Numeric channel id.")
2330
2433
  },
2331
2434
  handler: async (args2, ctx) => {
2332
2435
  const teamId = await ctx.resolveTeamId();
@@ -2341,7 +2444,7 @@ defineTool({
2341
2444
  });
2342
2445
 
2343
2446
  // src/tools/projects.ts
2344
- import { z as z12 } from "zod";
2447
+ import { z as z13 } from "zod";
2345
2448
  var AVAILABLE_REGION_IDS = ["eu-central-1"];
2346
2449
  defineTool({
2347
2450
  name: "list_projects",
@@ -2382,9 +2485,9 @@ defineTool({
2382
2485
  'Example: create_project({ name: "billing-api", description: "Stripe webhooks", region: "eu-central-1" }) \u2192 { project: { id: 12, publicId: "prj_\u2026", \u2026 } }'
2383
2486
  ].join("\n"),
2384
2487
  input: {
2385
- name: z12.string().min(1).max(60).describe("Project name (1\u201360 chars)."),
2386
- description: z12.string().max(500).optional().describe("Short description (\u2264500 chars)."),
2387
- region: z12.enum(AVAILABLE_REGION_IDS).optional().describe("Region: eu-central-1 (Falkenstein) \u2014 currently the only available region.")
2488
+ name: z13.string().min(1).max(60).describe("Project name (1\u201360 chars)."),
2489
+ description: z13.string().max(500).optional().describe("Short description (\u2264500 chars)."),
2490
+ region: z13.enum(AVAILABLE_REGION_IDS).optional().describe("Region: eu-central-1 (Falkenstein) \u2014 currently the only available region.")
2388
2491
  },
2389
2492
  handler: async (args2, ctx) => {
2390
2493
  const teamId = await ctx.resolveTeamId();
@@ -2417,9 +2520,9 @@ defineTool({
2417
2520
  'Example: update_project({ project_id: "prj_abc", name: "billing-prod" }) \u2192 { project: { name: "billing-prod", \u2026 } }'
2418
2521
  ].join("\n"),
2419
2522
  input: {
2420
- project_id: z12.string().describe("Project publicId."),
2421
- name: z12.string().min(1).max(60).optional().describe("New name (1\u201360 chars)."),
2422
- description: z12.string().max(500).optional().describe("New description (\u2264500 chars).")
2523
+ project_id: z13.string().describe("Project publicId."),
2524
+ name: z13.string().min(1).max(60).optional().describe("New name (1\u201360 chars)."),
2525
+ description: z13.string().max(500).optional().describe("New description (\u2264500 chars).")
2423
2526
  },
2424
2527
  handler: async (args2, ctx) => {
2425
2528
  if (args2.name === void 0 && args2.description === void 0) {
@@ -2453,7 +2556,7 @@ defineTool({
2453
2556
  'Example: get_project({ project_id: "prj_abc" }) \u2192 { project: { id: 12, name: "billing", \u2026 } }'
2454
2557
  ].join("\n"),
2455
2558
  input: {
2456
- project_id: z12.string().describe("Project publicId (e.g. prj_abc123).")
2559
+ project_id: z13.string().describe("Project publicId (e.g. prj_abc123).")
2457
2560
  },
2458
2561
  handler: async (args2, ctx) => {
2459
2562
  const teamId = await ctx.resolveTeamId();
@@ -2465,7 +2568,7 @@ defineTool({
2465
2568
  });
2466
2569
 
2467
2570
  // src/tools/resource-links.ts
2468
- import { z as z13 } from "zod";
2571
+ import { z as z14 } from "zod";
2469
2572
  var RESOURCE_LINK_TYPES = [
2470
2573
  "database",
2471
2574
  "object_storage",
@@ -2526,7 +2629,7 @@ defineTool({
2526
2629
  'Example: list_service_resources({ service_id: "svc_abc" }) \u2192 { items: [{ id: 7, resourceType: "database", resourceId: 42, alias: "APP_DB" }] }'
2527
2630
  ].join("\n"),
2528
2631
  input: {
2529
- service_id: z13.union([z13.number().int().positive(), z13.string()]).describe('Service \u2014 publicId ("svc_\u2026") or numeric id.')
2632
+ service_id: z14.union([z14.number().int().positive(), z14.string()]).describe('Service \u2014 publicId ("svc_\u2026") or numeric id.')
2530
2633
  },
2531
2634
  handler: async (args2, ctx) => {
2532
2635
  const teamId = await ctx.resolveTeamId();
@@ -2563,10 +2666,10 @@ defineTool({
2563
2666
  'Example: link_resource_to_service({ service_id: "svc_abc", resource_type: "database", resource_id: 42, alias: "APP_DB" }) \u2192 { link: { id: 7, alias: "APP_DB" } }'
2564
2667
  ].join("\n"),
2565
2668
  input: {
2566
- service_id: z13.union([z13.number().int().positive(), z13.string()]).describe('Consuming service \u2014 publicId ("svc_\u2026") or numeric id.'),
2567
- resource_type: z13.enum(RESOURCE_LINK_TYPES).describe("Kind of resource being linked."),
2568
- resource_id: z13.number().int().positive().describe("NUMERIC id of the resource (e.g. database.id) \u2014 not the publicId."),
2569
- alias: z13.string().min(1).max(48).regex(
2669
+ service_id: z14.union([z14.number().int().positive(), z14.string()]).describe('Consuming service \u2014 publicId ("svc_\u2026") or numeric id.'),
2670
+ resource_type: z14.enum(RESOURCE_LINK_TYPES).describe("Kind of resource being linked."),
2671
+ resource_id: z14.number().int().positive().describe("NUMERIC id of the resource (e.g. database.id) \u2014 not the publicId."),
2672
+ alias: z14.string().min(1).max(48).regex(
2570
2673
  /^[A-Z][A-Z0-9_]*$/,
2571
2674
  "Alias must be uppercase letters, digits and underscores, starting with a letter."
2572
2675
  ).describe('Uppercase env-var prefix, e.g. "APP_DB". Unique within the service.')
@@ -2602,8 +2705,8 @@ defineTool({
2602
2705
  'Example: unlink_resource_from_service({ service_id: "svc_abc", link_id: 7 }) \u2192 { ok: true }'
2603
2706
  ].join("\n"),
2604
2707
  input: {
2605
- service_id: z13.union([z13.number().int().positive(), z13.string()]).describe('Service \u2014 publicId ("svc_\u2026") or numeric id.'),
2606
- link_id: z13.number().int().positive().describe("Numeric linkId from list_service_resources (the link's own `id`).")
2708
+ service_id: z14.union([z14.number().int().positive(), z14.string()]).describe('Service \u2014 publicId ("svc_\u2026") or numeric id.'),
2709
+ link_id: z14.number().int().positive().describe("Numeric linkId from list_service_resources (the link's own `id`).")
2607
2710
  },
2608
2711
  handler: async (args2, ctx) => {
2609
2712
  const teamId = await ctx.resolveTeamId();
@@ -2615,9 +2718,177 @@ defineTool({
2615
2718
  });
2616
2719
 
2617
2720
  // src/tools/services.ts
2618
- import { z as z14 } from "zod";
2721
+ import { z as z15 } from "zod";
2722
+
2723
+ // src/lib/app-templates.ts
2724
+ var MCP_APP_TEMPLATES = [
2725
+ {
2726
+ id: "node-express",
2727
+ name: "Node.js Express",
2728
+ description: "HTTP server with Express.js",
2729
+ type: "web_service",
2730
+ runtime: "node",
2731
+ installCommand: "npm install",
2732
+ startCommand: "node index.js"
2733
+ },
2734
+ {
2735
+ id: "python-fastapi",
2736
+ name: "Python FastAPI",
2737
+ description: "Modern Python API framework",
2738
+ type: "web_service",
2739
+ runtime: "python",
2740
+ installCommand: "pip install -r requirements.txt",
2741
+ startCommand: "uvicorn main:app --host 0.0.0.0 --port $PORT"
2742
+ },
2743
+ {
2744
+ id: "go-api",
2745
+ name: "Go HTTP Server",
2746
+ description: "Lightweight Go web service",
2747
+ type: "web_service",
2748
+ runtime: "go",
2749
+ installCommand: "go mod download",
2750
+ buildCommand: "go build -o server .",
2751
+ startCommand: "./server"
2752
+ },
2753
+ {
2754
+ id: "bun-hono",
2755
+ name: "Bun + Hono",
2756
+ description: "Fast TypeScript API with Bun runtime",
2757
+ type: "web_service",
2758
+ runtime: "bun",
2759
+ installCommand: "bun install",
2760
+ startCommand: "bun run src/index.ts"
2761
+ },
2762
+ {
2763
+ id: "nextjs-ssr",
2764
+ name: "Next.js",
2765
+ description: "Server-rendered Next.js started with next start",
2766
+ type: "web_service",
2767
+ runtime: "node",
2768
+ installCommand: "npm install",
2769
+ buildCommand: "npm run build",
2770
+ startCommand: "npm run start"
2771
+ },
2772
+ {
2773
+ id: "react-router-ssr",
2774
+ name: "React Router / Remix",
2775
+ description: "Server build served by react-router-serve or remix-serve",
2776
+ type: "web_service",
2777
+ runtime: "node",
2778
+ installCommand: "npm install",
2779
+ buildCommand: "npm run build",
2780
+ startCommand: "npm run start"
2781
+ },
2782
+ {
2783
+ id: "sveltekit-node",
2784
+ name: "SvelteKit",
2785
+ description: "SvelteKit server build produced by adapter-node",
2786
+ type: "web_service",
2787
+ runtime: "node",
2788
+ installCommand: "npm install",
2789
+ buildCommand: "npm run build",
2790
+ startCommand: "node build"
2791
+ },
2792
+ {
2793
+ id: "nuxt",
2794
+ name: "Nuxt",
2795
+ description: "Nuxt server build running on the Nitro output",
2796
+ type: "web_service",
2797
+ runtime: "node",
2798
+ installCommand: "npm install",
2799
+ buildCommand: "npm run build",
2800
+ startCommand: "node .output/server/index.mjs"
2801
+ },
2802
+ {
2803
+ id: "django",
2804
+ name: "Django",
2805
+ description: "Django project served by Gunicorn",
2806
+ type: "web_service",
2807
+ runtime: "python",
2808
+ installCommand: "pip install -r requirements.txt",
2809
+ buildCommand: "python manage.py collectstatic --noinput || true",
2810
+ startCommand: "gunicorn --bind 0.0.0.0:$PORT --workers 2 $(ls */wsgi.py | head -n1 | cut -d/ -f1).wsgi:application"
2811
+ },
2812
+ {
2813
+ id: "flask",
2814
+ name: "Flask",
2815
+ description: "Flask app served by Gunicorn",
2816
+ type: "web_service",
2817
+ runtime: "python",
2818
+ installCommand: "pip install -r requirements.txt",
2819
+ startCommand: "gunicorn --bind 0.0.0.0:$PORT --workers 2 app:app"
2820
+ },
2821
+ {
2822
+ id: "spring-boot",
2823
+ name: "Spring Boot",
2824
+ description: "Spring Boot fat JAR built with Maven or Gradle",
2825
+ type: "web_service",
2826
+ runtime: "java",
2827
+ // No install/build on purpose: the agent inspects the repo and picks
2828
+ // Maven vs Gradle with the matching toolchain image. Sending either one
2829
+ // here would break the other half of the split.
2830
+ startCommand: "java -jar /app/app.jar --server.port=$PORT"
2831
+ },
2832
+ {
2833
+ id: "static-react",
2834
+ name: "React SPA",
2835
+ description: "Single-page React application",
2836
+ type: "static_site",
2837
+ runtime: "node",
2838
+ installCommand: "npm install",
2839
+ buildCommand: "npm run build",
2840
+ publishPath: "./dist"
2841
+ },
2842
+ {
2843
+ id: "static-nextjs",
2844
+ name: "Next.js Static",
2845
+ description: "Next.js with static export",
2846
+ type: "static_site",
2847
+ runtime: "node",
2848
+ installCommand: "npm install",
2849
+ buildCommand: "npm run build",
2850
+ publishPath: "./out"
2851
+ },
2852
+ {
2853
+ id: "astro",
2854
+ name: "Astro",
2855
+ description: "Astro site built to Astro's default static output",
2856
+ type: "static_site",
2857
+ runtime: "node",
2858
+ installCommand: "npm install",
2859
+ buildCommand: "npm run build",
2860
+ publishPath: "./dist"
2861
+ },
2862
+ {
2863
+ id: "worker-bullmq",
2864
+ name: "BullMQ Worker",
2865
+ description: "Background job processor",
2866
+ type: "worker",
2867
+ runtime: "node",
2868
+ installCommand: "npm install",
2869
+ startCommand: "node worker.js"
2870
+ },
2871
+ {
2872
+ id: "cron-cleanup",
2873
+ name: "Cleanup Cron",
2874
+ description: "Daily maintenance task",
2875
+ type: "cron_job",
2876
+ runtime: "node",
2877
+ installCommand: "npm install",
2878
+ startCommand: "node cleanup.js",
2879
+ cronSchedule: "0 3 * * *"
2880
+ }
2881
+ ];
2882
+
2883
+ // src/tools/services.ts
2619
2884
  var DEV_ENV_IMAGE = "registry.hoststack.dev/hoststack/dev-env:latest";
2620
2885
  var DEV_ENV_VOLUME = { name: "workspace", mountPath: "/workspace", sizeGb: 10 };
2886
+ var DEV_BOX_INSIDE = [
2887
+ "INSIDE the box (no Docker daemon \u2014 do not try `docker run`, and runtime `apt-get` is blocked too; it is all preinstalled):",
2888
+ ' - Databases/engines via `dev-services up`: Postgres 17 (:5432, user "dev", trust auth), Redis (:6379), Meilisearch (:7700). MariaDB/MySQL (:3306, user "dev", no password, client `mariadb`) and MongoDB (:27017, client `mongosh`) are OPT-IN \u2014 `dev-services up mysql` / `dev-services up mongodb` \u2014 so a box does not pay RAM for engines it never uses. `dev-services createdb <name>` makes a Postgres role + database in one step. Data lives on /workspace, so it survives restarts.',
2889
+ " - Language runtimes: node, bun, python and php 8.3 are built in. go, java, ruby, rust, elixir and dotnet install on demand with `dev-runtime add <name>` (persisted to /workspace). Together that covers every runtime the platform itself deploys \u2014 a WordPress/Laravel/Rails stack runs in the box. elixir needs `dev-runtime add erlang` first: it is a BEAM distribution with no OTP of its own, and Ubuntu ships one too old to use, so mise builds a current one (takes a few minutes).",
2890
+ " - `hoststack-status` prints what is running, the box dev URL and agent login state at any time."
2891
+ ].join("\n");
2621
2892
  var SERVICE_TYPES = [
2622
2893
  "web_service",
2623
2894
  "private_service",
@@ -2664,11 +2935,11 @@ defineTool({
2664
2935
  'Example: list_services({ status: "failed" }) \u2192 only services that need attention.'
2665
2936
  ].join("\n"),
2666
2937
  input: {
2667
- project_id: z14.union([z14.number().int().positive(), z14.string()]).optional().describe("Project filter \u2014 numeric id or publicId."),
2668
- environment_id: z14.union([z14.number().int().positive(), z14.string()]).optional().describe("Environment filter \u2014 numeric id or publicId."),
2669
- status: z14.enum(["active", "deploying", "suspended", "failed", "not_deployed"]).optional().describe("Filter by current runtime status."),
2670
- type: z14.enum(["web_service", "private_service", "worker", "cron_job", "static_site"]).optional().describe("Filter by service type."),
2671
- dev_environment: z14.boolean().optional().describe(
2938
+ project_id: z15.union([z15.number().int().positive(), z15.string()]).optional().describe("Project filter \u2014 numeric id or publicId."),
2939
+ environment_id: z15.union([z15.number().int().positive(), z15.string()]).optional().describe("Environment filter \u2014 numeric id or publicId."),
2940
+ status: z15.enum(["active", "deploying", "suspended", "failed", "not_deployed"]).optional().describe("Filter by current runtime status."),
2941
+ type: z15.enum(["web_service", "private_service", "worker", "cron_job", "static_site"]).optional().describe("Filter by service type."),
2942
+ dev_environment: z15.boolean().optional().describe(
2672
2943
  "Include agentic Dev Boxes in the results (excluded by default; see list_dev_environments)."
2673
2944
  )
2674
2945
  },
@@ -2729,29 +3000,31 @@ defineTool({
2729
3000
  ' - plan (optional): service size (default "micro").',
2730
3001
  " - environment_id (optional): bind to a specific environment; defaults to the project Production env.",
2731
3002
  " - auto_deploy (optional, default true): trigger the first deploy immediately when a source is present.",
3003
+ " - machine (optional): run it on one of the team's OWN enrolled machines (name or id, see list_machines) instead of HostStack compute. Pinned at creation and never moved afterwards, and the service is up only while that machine is.",
2732
3004
  "",
2733
3005
  "Returns: { service: Service, deployId: number | null }.",
2734
3006
  "",
2735
3007
  'Example: create_service({ project_id: "prj_abc", name: "api", type: "web_service", github_repo_id: 42 }) \u2192 { service: { publicId: "svc_\u2026" }, deployId: 1234 }'
2736
3008
  ].join("\n"),
2737
3009
  input: {
2738
- project_id: z14.union([z14.number().int().positive(), z14.string()]).describe("Target project \u2014 numeric id or publicId."),
2739
- name: z14.string().min(1).max(100).describe("Service name (1\u2013100 chars)."),
2740
- type: z14.enum(SERVICE_TYPES).describe("Service type."),
2741
- docker_image: z14.string().max(500).optional().describe(
3010
+ project_id: z15.union([z15.number().int().positive(), z15.string()]).describe("Target project \u2014 numeric id or publicId."),
3011
+ name: z15.string().min(1).max(100).describe("Service name (1\u2013100 chars)."),
3012
+ type: z15.enum(SERVICE_TYPES).describe("Service type."),
3013
+ docker_image: z15.string().max(500).optional().describe(
2742
3014
  "Pre-built APPLICATION image ref. Mutually exclusive with github_repo_id. Not for databases \u2014 use create_database for postgres/redis/mysql/mariadb/mongodb."
2743
3015
  ),
2744
- github_repo_id: z14.number().int().positive().optional().describe("Linked GitHub repo numeric id. Mutually exclusive with docker_image."),
2745
- branch: z14.string().max(200).optional().describe('Git branch (default "main").'),
2746
- install_command: z14.string().max(1e3).optional().describe("Install shell command."),
2747
- build_command: z14.string().max(1e3).optional().describe("Build shell command."),
2748
- start_command: z14.string().max(1e3).optional().describe("Start shell command (required for web/private services without an image)."),
2749
- cron_schedule: z14.string().max(100).optional().describe("Cron expression \u2014 required for cron_job."),
2750
- publish_path: z14.string().max(500).optional().describe("Static-site output dir."),
2751
- runtime: z14.string().max(50).optional().describe("Runtime hint (node/bun/python/\u2026)."),
2752
- plan: z14.enum(SERVICE_PLANS).optional().describe('Service size (default "micro").'),
2753
- environment_id: z14.union([z14.number().int().positive(), z14.string()]).optional().describe("Bind to a specific environment; defaults to Production."),
2754
- auto_deploy: z14.boolean().optional().describe("Trigger the first deploy immediately (default true).")
3016
+ github_repo_id: z15.number().int().positive().optional().describe("Linked GitHub repo numeric id. Mutually exclusive with docker_image."),
3017
+ branch: z15.string().max(200).optional().describe('Git branch (default "main").'),
3018
+ install_command: z15.string().max(1e3).optional().describe("Install shell command."),
3019
+ build_command: z15.string().max(1e3).optional().describe("Build shell command."),
3020
+ start_command: z15.string().max(1e3).optional().describe("Start shell command (required for web/private services without an image)."),
3021
+ cron_schedule: z15.string().max(100).optional().describe("Cron expression \u2014 required for cron_job."),
3022
+ publish_path: z15.string().max(500).optional().describe("Static-site output dir."),
3023
+ runtime: z15.string().max(50).optional().describe("Runtime hint (node/bun/python/\u2026)."),
3024
+ plan: z15.enum(SERVICE_PLANS).optional().describe('Service size (default "micro").'),
3025
+ environment_id: z15.union([z15.number().int().positive(), z15.string()]).optional().describe("Bind to a specific environment; defaults to Production."),
3026
+ auto_deploy: z15.boolean().optional().describe("Trigger the first deploy immediately (default true)."),
3027
+ machine: machineInput
2755
3028
  },
2756
3029
  handler: async (args2, ctx) => {
2757
3030
  const teamId = await ctx.resolveTeamId();
@@ -2781,6 +3054,9 @@ defineTool({
2781
3054
  teamId
2782
3055
  });
2783
3056
  }
3057
+ if (args2.machine !== void 0) {
3058
+ input.machineId = await resolveMachineId(ctx, teamId, args2.machine);
3059
+ }
2784
3060
  const response = await ctx.hoststack.services.create(teamId, input);
2785
3061
  const data = {
2786
3062
  service: shapeService(response.service),
@@ -2804,28 +3080,31 @@ defineTool({
2804
3080
  ' - project_id: numeric id or publicId ("prj_\u2026") of the target project.',
2805
3081
  ' - name (optional): service name (default "dev-environment").',
2806
3082
  ' - plan (optional): box size. Defaults to "standard" (2 GB) \u2014 the OOM-safe floor (DEV_ENV_MIN_SIZE); a coding agent + build OOMs below it. A smaller plan is clamped up to "standard".',
2807
- " - disk_gb (optional): /workspace volume size in GB (default 10, 1\u2013100).",
3083
+ " - disk_gb (optional): /workspace volume size in GB (default 10, min 10, max 10240).",
2808
3084
  " - hoststack_api_key (optional): sets HOSTSTACK_API_KEY so the hoststack MCP works inside the container.",
2809
3085
  " - poststack_api_key (optional): sets POSTSTACK_API_KEY so the poststack MCP works inside the container.",
2810
3086
  " - repo_url (optional): clone this git URL into /workspace on first boot (with optional branch).",
2811
3087
  "",
3088
+ DEV_BOX_INSIDE,
3089
+ "",
2812
3090
  "Returns: { service: Service, volumeAttached: boolean, deployId: number | null }. Open the Terminal tab on the service (dashboard or phone) once it is running; log in once with `claude /login` inside the container.",
2813
3091
  "",
2814
3092
  'Example: create_dev_environment({ project_id: "prj_abc", name: "scratch", hoststack_api_key: "hs_live_\u2026" })'
2815
3093
  ].join("\n"),
2816
3094
  input: {
2817
- project_id: z14.union([z14.number().int().positive(), z14.string()]).describe("Target project \u2014 numeric id or publicId."),
2818
- name: z14.string().min(1).max(100).optional().describe('Service name (default "dev-environment").'),
2819
- plan: z14.enum(SERVICE_PLANS).optional().describe(
3095
+ project_id: z15.union([z15.number().int().positive(), z15.string()]).describe("Target project \u2014 numeric id or publicId."),
3096
+ name: z15.string().min(1).max(100).optional().describe('Service name (default "dev-environment").'),
3097
+ plan: z15.enum(SERVICE_PLANS).optional().describe(
2820
3098
  'Box size (default "standard" \u2014 2 GB, the OOM-safe floor; a smaller plan is clamped up to "standard").'
2821
3099
  ),
2822
- disk_gb: z14.number().int().min(1).max(100).optional().describe("/workspace volume size in GB (default 10)."),
2823
- hoststack_api_key: z14.string().optional().describe("Value for HOSTSTACK_API_KEY (enables the hoststack MCP in-container)."),
2824
- poststack_api_key: z14.string().optional().describe("Value for POSTSTACK_API_KEY (enables the poststack MCP in-container)."),
2825
- repo_url: z14.string().max(500).optional().describe(
3100
+ disk_gb: z15.number().int().min(10).max(10240).optional().describe("/workspace volume size in GB (default 10, min 10, max 10240)."),
3101
+ hoststack_api_key: z15.string().optional().describe("Value for HOSTSTACK_API_KEY (enables the hoststack MCP in-container)."),
3102
+ poststack_api_key: z15.string().optional().describe("Value for POSTSTACK_API_KEY (enables the poststack MCP in-container)."),
3103
+ repo_url: z15.string().max(500).optional().describe(
2826
3104
  "Clone this git URL into /workspace on first boot (HTTPS, or SSH once a key is set)."
2827
3105
  ),
2828
- branch: z14.string().max(200).optional().describe("Branch to clone (with repo_url).")
3106
+ branch: z15.string().max(200).optional().describe("Branch to clone (with repo_url)."),
3107
+ machine: machineInput
2829
3108
  },
2830
3109
  handler: async (args2, ctx) => {
2831
3110
  const teamId = await ctx.resolveTeamId();
@@ -2844,6 +3123,9 @@ defineTool({
2844
3123
  autoDeploy: false,
2845
3124
  plan
2846
3125
  };
3126
+ if (args2.machine !== void 0) {
3127
+ createInput.machineId = await resolveMachineId(ctx, teamId, args2.machine);
3128
+ }
2847
3129
  const created = await ctx.hoststack.services.create(teamId, createInput);
2848
3130
  const service = created.service;
2849
3131
  const rollback = async () => {
@@ -2931,14 +3213,16 @@ defineTool({
2931
3213
  " - include_database_clone (optional): clone the linked database so the app runs on a copy of real data (default true).",
2932
3214
  ' - name (optional): dev box name (default "<source>-dev").',
2933
3215
  "",
3216
+ DEV_BOX_INSIDE,
3217
+ "",
2934
3218
  "Returns: { service, devUrl, deployId }. Once it is live: open the Terminal tab, run `claude`, start the dev server (it must bind $PORT), and view the app at https://<devUrl>. Tear it all down later with delete_dev_environment.",
2935
3219
  "",
2936
3220
  'Example: spin_up_dev_environment({ service_id: "svc_api" }) \u2192 a dev box running a clone of the api service (repo + env-vars + cloned DB) with a public dev URL.'
2937
3221
  ].join("\n"),
2938
3222
  input: {
2939
- service_id: z14.union([z14.number().int().positive(), z14.string()]).describe("Source service to debug \u2014 numeric id or publicId."),
2940
- include_database_clone: z14.boolean().optional().describe("Clone the linked database so the app runs on copied data (default true)."),
2941
- name: z14.string().min(1).max(100).optional().describe('Dev box name (default "<source>-dev").')
3223
+ service_id: z15.union([z15.number().int().positive(), z15.string()]).describe("Source service to debug \u2014 numeric id or publicId."),
3224
+ include_database_clone: z15.boolean().optional().describe("Clone the linked database so the app runs on copied data (default true)."),
3225
+ name: z15.string().min(1).max(100).optional().describe('Dev box name (default "<source>-dev").')
2942
3226
  },
2943
3227
  handler: async (args2, ctx) => {
2944
3228
  const teamId = await ctx.resolveTeamId();
@@ -2979,7 +3263,7 @@ defineTool({
2979
3263
  'Example: delete_dev_environment({ service_id: "svc_api_dev" }) \u2192 removes the dev box, its cloned database, and the /workspace volume.'
2980
3264
  ].join("\n"),
2981
3265
  input: {
2982
- service_id: z14.union([z14.number().int().positive(), z14.string()]).describe("The dev box to tear down \u2014 numeric id or publicId.")
3266
+ service_id: z15.union([z15.number().int().positive(), z15.string()]).describe("The dev box to tear down \u2014 numeric id or publicId.")
2983
3267
  },
2984
3268
  handler: async (args2, ctx) => {
2985
3269
  const teamId = await ctx.resolveTeamId();
@@ -3015,8 +3299,8 @@ defineTool({
3015
3299
  'Example: resize_dev_environment({ service_id: "svc_skyskraber_dev", size: "large" }) \u2192 bumps the box to the large tier, applied live.'
3016
3300
  ].join("\n"),
3017
3301
  input: {
3018
- service_id: z14.union([z14.number().int().positive(), z14.string()]).describe("The box to resize \u2014 numeric id or publicId."),
3019
- size: z14.enum(SERVICE_PLANS).describe(
3302
+ service_id: z15.union([z15.number().int().positive(), z15.string()]).describe("The box to resize \u2014 numeric id or publicId."),
3303
+ size: z15.enum(SERVICE_PLANS).describe(
3020
3304
  'Target size tier (service catalog size, e.g. "standard", "large", "xlarge").'
3021
3305
  )
3022
3306
  },
@@ -3070,35 +3354,60 @@ defineTool({
3070
3354
  name: "list_templates",
3071
3355
  category: "services",
3072
3356
  description: [
3073
- "List the canonical Dev Box (AI dev environment) preset so an agent can see, without guessing, what a `create_standalone_dev_environment` / `create_dev_environment` actually provisions: the container image, the persistent /workspace volume (mount + default size), the OOM-safe size floor, and the companion database options.",
3357
+ "List the app quickstart templates the dashboard New-Service wizard offers, plus the separate Dev Box preset. A template is a pre-filled install/build/start command set for a stack (Next.js, Django, SvelteKit, Spring Boot, \u2026) \u2014 pass its fields straight to `create_service` to get a service configured exactly like a hand-created one.",
3074
3358
  "",
3075
- "When to use: before creating a Dev Box, to confirm the image, the default /workspace size, the minimum plan, or which companion engines can be attached \u2014 instead of hand-duplicating those constants.",
3359
+ 'When to use: whenever asked to deploy a known stack ("deploy my Next.js app", "put this Django project on HostStack"), before hand-writing a start command; and before creating a Dev Box, to confirm the image, /workspace size, plan floor, and companion engines instead of duplicating those constants.',
3076
3360
  "",
3077
- 'Returns: { templates: [{ id, name, image, volume: { name, mountPath, sizeGb }, minPlan, companions }] }. Today there is one preset ("dev-environment").',
3361
+ "Returns: { templates: [{ id, name, description, type, runtime, installCommand?, buildCommand?, startCommand?, publishPath?, cronSchedule? }], devBox: { id, name, image, volume: { name, mountPath, sizeGb }, minPlan, createWith, companions, inBox } }. Every template is source-built from a Git repo \u2014 pair one with `github_repo_id`, not `docker_image`. The Dev Box is NOT in `templates`: it is created with create_standalone_dev_environment / create_dev_environment, never create_service. `companions` are SEPARATE managed databases you can attach; `inBox` is what the box already runs on its own (engines via `dev-services`, language runtimes via `dev-runtime`) \u2014 check it before concluding a box cannot run something.",
3078
3362
  "",
3079
- 'Example: list_templates() \u2192 { templates: [{ id: "dev-environment", image: "registry.hoststack.dev/hoststack/dev-env:latest", volume: { mountPath: "/workspace", sizeGb: 10 }, minPlan: "standard", companions: ["postgres","redis","meilisearch"] }] }'
3363
+ 'Example: list_templates() \u2192 { templates: [{ id: "nextjs-ssr", name: "Next.js", type: "web_service", runtime: "node", installCommand: "npm install", buildCommand: "npm run build", startCommand: "npm run start" }, \u2026], devBox: { id: "dev-environment", image: "registry.hoststack.dev/hoststack/dev-env:latest", volume: { mountPath: "/workspace", sizeGb: 10 }, minPlan: "standard", companions: ["postgres","redis","meilisearch"] } }'
3080
3364
  ].join("\n"),
3081
3365
  input: {},
3082
3366
  handler: async () => {
3083
- const templates = [
3084
- {
3085
- id: "dev-environment",
3086
- name: "AI Dev Environment",
3087
- image: DEV_ENV_IMAGE,
3088
- volume: {
3089
- name: DEV_ENV_VOLUME.name,
3090
- mountPath: DEV_ENV_VOLUME.mountPath,
3091
- sizeGb: DEV_ENV_VOLUME.sizeGb
3367
+ const devBox = {
3368
+ id: "dev-environment",
3369
+ name: "AI Dev Environment",
3370
+ image: DEV_ENV_IMAGE,
3371
+ volume: {
3372
+ name: DEV_ENV_VOLUME.name,
3373
+ mountPath: DEV_ENV_VOLUME.mountPath,
3374
+ sizeGb: DEV_ENV_VOLUME.sizeGb
3375
+ },
3376
+ /** OOM-safe plan floor — a coding agent + build needs at least this. */
3377
+ minPlan: DEV_ENV_MIN_SIZE,
3378
+ /** Create it with these, never create_service. */
3379
+ createWith: ["create_standalone_dev_environment", "create_dev_environment"],
3380
+ /** Companion engines you can attach (fresh + empty), like local `make db-up`. */
3381
+ companions: ["postgres", "redis", "meilisearch"],
3382
+ /**
3383
+ * What the box can already run without attaching anything. Reported
3384
+ * so an agent picking a template can see that "it needs MySQL" or
3385
+ * "it's a PHP app" is already satisfied, instead of concluding the
3386
+ * box is unsuitable and reaching for Docker (there is no daemon).
3387
+ */
3388
+ inBox: {
3389
+ engines: {
3390
+ default: ["postgres", "redis", "meilisearch"],
3391
+ optIn: ["mysql", "mongodb"],
3392
+ start: "dev-services up [mysql|mongodb]"
3092
3393
  },
3093
- /** OOM-safe plan floor — a coding agent + build needs at least this. */
3094
- minPlan: DEV_ENV_MIN_SIZE,
3095
- /** Companion engines you can attach (fresh + empty), like local `make db-up`. */
3096
- companions: ["postgres", "redis", "meilisearch"]
3394
+ runtimes: {
3395
+ builtin: ["node", "bun", "python", "php"],
3396
+ // erlang is listed because elixir cannot run without it —
3397
+ // see DEV_BOX_INSIDE. Ordering matters for a caller acting
3398
+ // on this: add erlang, then elixir.
3399
+ onDemand: ["go", "java", "ruby", "rust", "erlang", "elixir", "dotnet"],
3400
+ install: "dev-runtime add <name>"
3401
+ }
3097
3402
  }
3098
- ];
3403
+ };
3404
+ const templates = MCP_APP_TEMPLATES;
3405
+ const byType = /* @__PURE__ */ new Map();
3406
+ for (const t of templates) byType.set(t.type, (byType.get(t.type) ?? 0) + 1);
3407
+ const breakdown = [...byType].map(([type, n]) => `${n} ${type}`).join(", ");
3099
3408
  return respond({
3100
- summary: `${templates.length} Dev Box template${templates.length === 1 ? "" : "s"} available (image ${DEV_ENV_IMAGE}, /workspace ${DEV_ENV_VOLUME.sizeGb} GB, floor "${DEV_ENV_MIN_SIZE}").`,
3101
- data: { templates }
3409
+ summary: `${templates.length} app templates (${breakdown}) \u2014 pick one by id and pass its commands to create_service with a github_repo_id. Plus the Dev Box preset (image ${DEV_ENV_IMAGE}, /workspace ${DEV_ENV_VOLUME.sizeGb} GB, floor "${DEV_ENV_MIN_SIZE}"), which is created with create_standalone_dev_environment, not create_service.`,
3410
+ data: { templates, devBox }
3102
3411
  });
3103
3412
  }
3104
3413
  });
@@ -3110,7 +3419,9 @@ defineTool({
3110
3419
  "",
3111
3420
  "This makes a cloud Dev Box, NOT a project deploy environment (create_environment \u2192 Production / Staging / Preview). It is a workspace you code in, not a deployment target.",
3112
3421
  "",
3113
- 'Every Dev Box \u2014 with or without companions \u2014 also ships NATIVE Postgres 17, Redis and Meilisearch INSIDE the box, started with `dev-services up` (postgres :5432 as user "dev" with trust auth, redis :6379, meilisearch :7700). That is the no-Docker replacement for `make db-up` and is the right answer for local development inside the box; there is no Docker daemon available in there. The `companions` option is different \u2014 it attaches SEPARATE managed databases to the box\'s environment for when the box needs a real, persistent, backed-up datastore.',
3422
+ DEV_BOX_INSIDE,
3423
+ "",
3424
+ "Those in-box engines are the no-Docker replacement for a local `make db-up`, and are the right answer for development inside the box. The `companions` option is a DIFFERENT thing: it attaches SEPARATE managed databases to the box's environment, for when the box needs a real, persistent, backed-up datastore rather than a scratch one.",
3114
3425
  "",
3115
3426
  'When to use: "create a dev environment for <repo>", "spin me up a cloud dev box with a Postgres". Distinct from spin_up_dev_environment (which clones an EXISTING service) and create_dev_environment (a bare box in a chosen project).',
3116
3427
  "",
@@ -3121,6 +3432,7 @@ defineTool({
3121
3432
  " - clone_url (for url): an http(s) git clone URL.",
3122
3433
  " - branch (optional): branch to clone.",
3123
3434
  ' - databases (optional): companion services to attach \u2014 any of "postgres", "redis", "meilisearch".',
3435
+ " - machine (optional): run the box on one of the team's OWN enrolled machines (name or id, see list_machines) instead of HostStack compute. /workspace then lives on that machine's disk, so the box is reachable only while it is switched on, and any companion databases are created there too.",
3124
3436
  ' - plan (optional): box size. Defaults to "standard" (2 GB) \u2014 the OOM-safe floor (DEV_ENV_MIN_SIZE); a smaller plan is floored up to "standard" server-side.',
3125
3437
  " - agent_accounts (optional): bind specific saved agent logins by account id per provider. OMIT to auto-inherit the box owner's default logins (claude/codex/opencode), so `claude` is already authenticated on first boot \u2014 no manual login.",
3126
3438
  "",
@@ -3129,25 +3441,26 @@ defineTool({
3129
3441
  'Example: create_standalone_dev_environment({ name: "app-dev", source_kind: "github_repo", github_repo_id: 42, databases: ["postgres","redis"] })'
3130
3442
  ].join("\n"),
3131
3443
  input: {
3132
- name: z14.string().min(1).max(100).optional().describe(
3444
+ name: z15.string().min(1).max(100).optional().describe(
3133
3445
  'Dev Box name. Omit to have it named after the source (the repo name, or "dev-box" for a blank one), de-duplicated against existing boxes.'
3134
3446
  ),
3135
- source_kind: z14.enum(["github_repo", "url", "blank"]).describe("Where the code comes from."),
3136
- github_repo_id: z14.number().int().positive().optional().describe('Connected GitHub repo id (required when source_kind="github_repo").'),
3137
- clone_url: z14.string().url().optional().describe('http(s) git clone URL (required when source_kind="url").'),
3138
- branch: z14.string().min(1).max(255).optional().describe("Branch to clone."),
3139
- databases: z14.array(z14.enum(["postgres", "redis", "meilisearch"])).optional().describe("Companion services to attach (fresh + empty)."),
3140
- plan: z14.enum(SERVICE_PLANS).optional().describe(
3447
+ source_kind: z15.enum(["github_repo", "url", "blank"]).describe("Where the code comes from."),
3448
+ github_repo_id: z15.number().int().positive().optional().describe('Connected GitHub repo id (required when source_kind="github_repo").'),
3449
+ clone_url: z15.string().url().optional().describe('http(s) git clone URL (required when source_kind="url").'),
3450
+ branch: z15.string().min(1).max(255).optional().describe("Branch to clone."),
3451
+ databases: z15.array(z15.enum(["postgres", "redis", "meilisearch"])).optional().describe("Companion services to attach (fresh + empty)."),
3452
+ plan: z15.enum(SERVICE_PLANS).optional().describe(
3141
3453
  'Box size (default "standard" \u2014 2 GB; a smaller plan is floored to "standard").'
3142
3454
  ),
3143
- agent_accounts: z14.array(
3144
- z14.object({
3145
- provider: z14.enum(["claude", "codex", "opencode"]),
3146
- account_id: z14.number().int().positive()
3455
+ agent_accounts: z15.array(
3456
+ z15.object({
3457
+ provider: z15.enum(["claude", "codex", "opencode"]),
3458
+ account_id: z15.number().int().positive()
3147
3459
  })
3148
3460
  ).max(3).optional().describe(
3149
3461
  "Bind saved agent logins by account id per provider. Omit to inherit the box owner's default logins automatically."
3150
- )
3462
+ ),
3463
+ machine: machineInput
3151
3464
  },
3152
3465
  handler: async (args2, ctx) => {
3153
3466
  const teamId = await ctx.resolveTeamId();
@@ -3179,9 +3492,11 @@ defineTool({
3179
3492
  } else {
3180
3493
  source = { kind: "blank" };
3181
3494
  }
3495
+ const machineId = args2.machine !== void 0 ? await resolveMachineId(ctx, teamId, args2.machine) : void 0;
3182
3496
  const input = {
3183
3497
  ...args2.name ? { name: args2.name } : {},
3184
3498
  source,
3499
+ ...machineId !== void 0 ? { machineId } : {},
3185
3500
  ...args2.databases ? { databases: args2.databases } : {},
3186
3501
  ...args2.plan ? { plan: args2.plan } : {},
3187
3502
  ...args2.agent_accounts && args2.agent_accounts.length > 0 ? {
@@ -3218,7 +3533,7 @@ defineTool({
3218
3533
  'Example: get_service({ service_id: "svc_abc" }) \u2192 { service: { type: "web", status: "running", \u2026 }, config: { healthCheckGracePeriodSec: 120, \u2026 } }'
3219
3534
  ].join("\n"),
3220
3535
  input: {
3221
- service_id: z14.string().describe("Service publicId (e.g. svc_abc123).")
3536
+ service_id: z15.string().describe("Service publicId (e.g. svc_abc123).")
3222
3537
  },
3223
3538
  handler: async (args2, ctx) => {
3224
3539
  const teamId = await ctx.resolveTeamId();
@@ -3250,7 +3565,7 @@ defineTool({
3250
3565
  'Example: get_service_metrics({ service_id: "svc_abc" }) \u2192 { metrics: { cpu: 0.42, memory: 0.71, \u2026 } }'
3251
3566
  ].join("\n"),
3252
3567
  input: {
3253
- service_id: z14.string().describe("Service publicId.")
3568
+ service_id: z15.string().describe("Service publicId.")
3254
3569
  },
3255
3570
  handler: async (args2, ctx) => {
3256
3571
  const teamId = await ctx.resolveTeamId();
@@ -3279,9 +3594,9 @@ defineTool({
3279
3594
  'Example: get_service_metrics_history({ service_id: "svc_abc", from: "-1h" }) \u2192 60-ish points for the last hour.'
3280
3595
  ].join("\n"),
3281
3596
  input: {
3282
- service_id: z14.string().describe("Service publicId."),
3283
- from: z14.string().optional().describe('ISO-8601 lower bound or relative offset (e.g. "-1h", "-2d").'),
3284
- to: z14.string().optional().describe("ISO-8601 upper bound; defaults to now.")
3597
+ service_id: z15.string().describe("Service publicId."),
3598
+ from: z15.string().optional().describe('ISO-8601 lower bound or relative offset (e.g. "-1h", "-2d").'),
3599
+ to: z15.string().optional().describe("ISO-8601 upper bound; defaults to now.")
3285
3600
  },
3286
3601
  handler: async (args2, ctx) => {
3287
3602
  const teamId = await ctx.resolveTeamId();
@@ -3325,8 +3640,8 @@ defineTool({
3325
3640
  'Example: update_service({ service_id: "svc_abc", name: "api-prod" }) \u2192 { service: { name: "api-prod", \u2026 } }'
3326
3641
  ].join("\n"),
3327
3642
  input: {
3328
- service_id: z14.string().describe("Service publicId."),
3329
- name: z14.string().min(1).max(60).describe("New service name (1\u201360 chars).")
3643
+ service_id: z15.string().describe("Service publicId."),
3644
+ name: z15.string().min(1).max(60).describe("New service name (1\u201360 chars).")
3330
3645
  },
3331
3646
  handler: async (args2, ctx) => {
3332
3647
  const teamId = await ctx.resolveTeamId();
@@ -3375,40 +3690,40 @@ defineTool({
3375
3690
  'Example: update_service_config({ service_id: "svc_abc", health_check_grace_period_sec: 180 }) \u2192 { config: { healthCheckGracePeriodSec: 180, \u2026 } }'
3376
3691
  ].join("\n"),
3377
3692
  input: {
3378
- service_id: z14.string().describe("Service publicId."),
3379
- install_command: z14.string().nullable().optional().describe("Install shell command. Null clears."),
3380
- build_command: z14.string().nullable().optional().describe("Build shell command. Null clears."),
3381
- start_command: z14.string().nullable().optional().describe("Start shell command. Null clears."),
3382
- branch: z14.string().optional().describe("Git branch to track."),
3383
- root_directory: z14.string().optional().describe("Build context root."),
3384
- dockerfile_path: z14.string().nullable().optional().describe("Path to Dockerfile relative to root. Null clears."),
3385
- auto_deploy: z14.boolean().optional().describe("Auto-deploy on push."),
3386
- health_check_path: z14.string().nullable().optional().describe('HTTP health-check path (e.g. "/health"). Null = TCP-only check.'),
3387
- health_check_enabled: z14.boolean().optional().describe("Toggle health checking on/off."),
3388
- health_check_interval: z14.number().int().min(5).max(300).optional().describe("How often the check runs, in seconds (5\u2013300)."),
3389
- health_check_timeout: z14.number().int().min(1).max(60).optional().describe("Single-attempt timeout in seconds (1\u201360)."),
3390
- health_check_grace_period_sec: z14.number().int().min(1).max(1800).optional().describe(
3693
+ service_id: z15.string().describe("Service publicId."),
3694
+ install_command: z15.string().nullable().optional().describe("Install shell command. Null clears."),
3695
+ build_command: z15.string().nullable().optional().describe("Build shell command. Null clears."),
3696
+ start_command: z15.string().nullable().optional().describe("Start shell command. Null clears."),
3697
+ branch: z15.string().optional().describe("Git branch to track."),
3698
+ root_directory: z15.string().optional().describe("Build context root."),
3699
+ dockerfile_path: z15.string().nullable().optional().describe("Path to Dockerfile relative to root. Null clears."),
3700
+ auto_deploy: z15.boolean().optional().describe("Auto-deploy on push."),
3701
+ health_check_path: z15.string().nullable().optional().describe('HTTP health-check path (e.g. "/health"). Null = TCP-only check.'),
3702
+ health_check_enabled: z15.boolean().optional().describe("Toggle health checking on/off."),
3703
+ health_check_interval: z15.number().int().min(5).max(300).optional().describe("How often the check runs, in seconds (5\u2013300)."),
3704
+ health_check_timeout: z15.number().int().min(1).max(60).optional().describe("Single-attempt timeout in seconds (1\u201360)."),
3705
+ health_check_grace_period_sec: z15.number().int().min(1).max(1800).optional().describe(
3391
3706
  "Startup grace period in seconds (1\u20131800). Raise this if the app needs more time to boot before health checks start counting failures."
3392
3707
  ),
3393
- memory_mb: z14.number().int().min(128).max(16384).optional().describe("Container memory cap in MB (128\u201316384)."),
3394
- cpu_shares: z14.number().int().min(128).max(4096).optional().describe("Relative CPU weight (128\u20134096)."),
3395
- disk_size_gb: z14.number().int().min(1).max(100).optional().describe("Ephemeral disk size in GB (1\u2013100)."),
3396
- port: z14.number().int().min(1).max(65535).optional().describe("Container port the platform forwards traffic to."),
3397
- protocol: z14.enum(["http", "tcp"]).optional().describe("Traffic protocol."),
3398
- restart_policy: z14.enum(["always", "on-failure", "no"]).optional().describe("Docker restart policy."),
3399
- deploy_strategy: z14.enum(["rolling", "recreate"]).optional().describe(
3708
+ memory_mb: z15.number().int().min(128).max(16384).optional().describe("Container memory cap in MB (128\u201316384)."),
3709
+ cpu_shares: z15.number().int().min(128).max(4096).optional().describe("Relative CPU weight (128\u20134096)."),
3710
+ disk_size_gb: z15.number().int().min(1).max(100).optional().describe("Ephemeral disk size in GB (1\u2013100)."),
3711
+ port: z15.number().int().min(1).max(65535).optional().describe("Container port the platform forwards traffic to."),
3712
+ protocol: z15.enum(["http", "tcp"]).optional().describe("Traffic protocol."),
3713
+ restart_policy: z15.enum(["always", "on-failure", "no"]).optional().describe("Docker restart policy."),
3714
+ deploy_strategy: z15.enum(["rolling", "recreate"]).optional().describe(
3400
3715
  'How a deploy replaces the container. "rolling" (default) = start new, wait for healthy, switch traffic, stop old (zero downtime). "recreate" = stop old first, then start new (brief outage) \u2014 required for a container holding an exclusive lock on a mounted volume, which cannot deploy at all under rolling.'
3401
3716
  ),
3402
- pre_deploy_command: z14.string().optional().describe("Shell command run before the new release accepts traffic."),
3403
- instance_count: z14.number().int().positive().max(50).optional().describe("Pin min and max instances to this value (1\u201350)."),
3404
- min_instances: z14.number().int().min(0).max(50).optional().describe("Autoscale lower bound. Use with max_instances for a range."),
3405
- max_instances: z14.number().int().min(1).max(50).optional().describe("Autoscale upper bound. Use with min_instances for a range."),
3406
- scale_cpu_threshold: z14.number().int().min(10).max(100).optional().describe("Autoscale CPU trigger percentage (10\u2013100)."),
3407
- scale_memory_threshold: z14.number().int().min(10).max(100).optional().describe("Autoscale memory trigger percentage (10\u2013100)."),
3408
- log_filter_rules: z14.array(
3409
- z14.object({
3410
- pattern: z14.string().min(1).max(200),
3411
- action: z14.enum(["drop", "downgrade"])
3717
+ pre_deploy_command: z15.string().optional().describe("Shell command run before the new release accepts traffic."),
3718
+ instance_count: z15.number().int().positive().max(50).optional().describe("Pin min and max instances to this value (1\u201350)."),
3719
+ min_instances: z15.number().int().min(0).max(50).optional().describe("Autoscale lower bound. Use with max_instances for a range."),
3720
+ max_instances: z15.number().int().min(1).max(50).optional().describe("Autoscale upper bound. Use with min_instances for a range."),
3721
+ scale_cpu_threshold: z15.number().int().min(10).max(100).optional().describe("Autoscale CPU trigger percentage (10\u2013100)."),
3722
+ scale_memory_threshold: z15.number().int().min(10).max(100).optional().describe("Autoscale memory trigger percentage (10\u2013100)."),
3723
+ log_filter_rules: z15.array(
3724
+ z15.object({
3725
+ pattern: z15.string().min(1).max(200),
3726
+ action: z15.enum(["drop", "downgrade"])
3412
3727
  })
3413
3728
  ).max(50).optional().describe(
3414
3729
  "Runtime-log filter rules. Empty array [] clears all rules. Each pattern is case-insensitive substring match against the message."
@@ -3505,7 +3820,7 @@ defineTool({
3505
3820
  'Example: suspend_service({ service_id: "svc_dev" }) \u2192 { ok: true }'
3506
3821
  ].join("\n"),
3507
3822
  input: {
3508
- service_id: z14.string().describe("Service publicId.")
3823
+ service_id: z15.string().describe("Service publicId.")
3509
3824
  },
3510
3825
  handler: async (args2, ctx) => {
3511
3826
  const teamId = await ctx.resolveTeamId();
@@ -3529,7 +3844,7 @@ defineTool({
3529
3844
  'Example: resume_service({ service_id: "svc_dev" }) \u2192 { ok: true }'
3530
3845
  ].join("\n"),
3531
3846
  input: {
3532
- service_id: z14.string().describe("Service publicId.")
3847
+ service_id: z15.string().describe("Service publicId.")
3533
3848
  },
3534
3849
  handler: async (args2, ctx) => {
3535
3850
  const teamId = await ctx.resolveTeamId();
@@ -3555,7 +3870,7 @@ defineTool({
3555
3870
  'Example: delete_service({ service_id: "svc_abandoned" }) \u2192 { ok: true }'
3556
3871
  ].join("\n"),
3557
3872
  input: {
3558
- service_id: z14.string().describe("Service publicId.")
3873
+ service_id: z15.string().describe("Service publicId.")
3559
3874
  },
3560
3875
  handler: async (args2, ctx) => {
3561
3876
  const teamId = await ctx.resolveTeamId();
@@ -3592,16 +3907,16 @@ defineTool({
3592
3907
  ' - Just count error lines without fetching them: get_service_logs({ service_id: "svc_abc", level: "error", since: "-5m", count_only: true }) \u2192 { count: 47 }'
3593
3908
  ].join("\n"),
3594
3909
  input: {
3595
- service_id: z14.string().describe("Service publicId."),
3596
- lines: z14.number().int().positive().max(1e3).optional().describe("Tail size; default 200, hard cap 1000."),
3597
- since: z14.string().optional().describe('ISO-8601 timestamp or relative offset (e.g. "-5m", "-1h").'),
3598
- until: z14.string().optional().describe("ISO-8601 timestamp or relative offset upper bound."),
3599
- stream: z14.enum(["stdout", "stderr"]).optional().describe("Restrict to one stream."),
3600
- level: z14.enum(["stdout", "stderr", "trace", "debug", "info", "warn", "error", "fatal"]).optional().describe(
3910
+ service_id: z15.string().describe("Service publicId."),
3911
+ lines: z15.number().int().positive().max(1e3).optional().describe("Tail size; default 200, hard cap 1000."),
3912
+ since: z15.string().optional().describe('ISO-8601 timestamp or relative offset (e.g. "-5m", "-1h").'),
3913
+ until: z15.string().optional().describe("ISO-8601 timestamp or relative offset upper bound."),
3914
+ stream: z15.enum(["stdout", "stderr"]).optional().describe("Restrict to one stream."),
3915
+ level: z15.enum(["stdout", "stderr", "trace", "debug", "info", "warn", "error", "fatal"]).optional().describe(
3601
3916
  "Filter by structured JSON log level (pino/bunyan/severity). Falls back to a stream-alias hint for plain-text logs (info/debug\u2192stdout, warn/error/fatal\u2192stderr)."
3602
3917
  ),
3603
- search: z14.string().max(100).optional().describe("Case-insensitive substring filter."),
3604
- count_only: z14.boolean().optional().describe("When true, return only { count } \u2014 skips the log payload.")
3918
+ search: z15.string().max(100).optional().describe("Case-insensitive substring filter."),
3919
+ count_only: z15.boolean().optional().describe("When true, return only { count } \u2014 skips the log payload.")
3605
3920
  },
3606
3921
  handler: async (args2, ctx) => {
3607
3922
  const teamId = await ctx.resolveTeamId();
@@ -3650,14 +3965,14 @@ defineTool({
3650
3965
  'Example: get_service_logs_bulk({ service_ids: ["svc_api", "svc_worker"], level: "error", since: "-15m", count_only: true }) \u2192 { results: { svc_api: { count: 0 }, svc_worker: { count: 12 } } }.'
3651
3966
  ].join("\n"),
3652
3967
  input: {
3653
- service_ids: z14.array(z14.string()).min(1).max(10).describe("Service publicIds (1\u201310). Hard cap 10 to bound parallel work."),
3654
- lines_per_service: z14.number().int().positive().max(500).optional().describe("Tail size per service; default 100, hard cap 500."),
3655
- since: z14.string().optional().describe('ISO-8601 timestamp or relative offset (e.g. "-5m", "-1h").'),
3656
- until: z14.string().optional().describe("ISO-8601 timestamp or relative offset upper bound."),
3657
- stream: z14.enum(["stdout", "stderr"]).optional().describe("Restrict to one stream."),
3658
- level: z14.enum(["stdout", "stderr", "trace", "debug", "info", "warn", "error", "fatal"]).optional().describe("Structured log level filter (same as get_service_logs)."),
3659
- search: z14.string().max(100).optional().describe("Case-insensitive substring filter."),
3660
- count_only: z14.boolean().optional().describe("When true, return only counts per service \u2014 skips the log payload.")
3968
+ service_ids: z15.array(z15.string()).min(1).max(10).describe("Service publicIds (1\u201310). Hard cap 10 to bound parallel work."),
3969
+ lines_per_service: z15.number().int().positive().max(500).optional().describe("Tail size per service; default 100, hard cap 500."),
3970
+ since: z15.string().optional().describe('ISO-8601 timestamp or relative offset (e.g. "-5m", "-1h").'),
3971
+ until: z15.string().optional().describe("ISO-8601 timestamp or relative offset upper bound."),
3972
+ stream: z15.enum(["stdout", "stderr"]).optional().describe("Restrict to one stream."),
3973
+ level: z15.enum(["stdout", "stderr", "trace", "debug", "info", "warn", "error", "fatal"]).optional().describe("Structured log level filter (same as get_service_logs)."),
3974
+ search: z15.string().max(100).optional().describe("Case-insensitive substring filter."),
3975
+ count_only: z15.boolean().optional().describe("When true, return only counts per service \u2014 skips the log payload.")
3661
3976
  },
3662
3977
  handler: async (args2, ctx) => {
3663
3978
  const teamId = await ctx.resolveTeamId();
@@ -3703,7 +4018,7 @@ defineTool({
3703
4018
  });
3704
4019
 
3705
4020
  // src/tools/volumes.ts
3706
- import { z as z15 } from "zod";
4021
+ import { z as z16 } from "zod";
3707
4022
  var MIN_VOLUME_SIZE_GB = 10;
3708
4023
  defineTool({
3709
4024
  name: "list_volumes",
@@ -3721,7 +4036,7 @@ defineTool({
3721
4036
  'Example: list_volumes({ service_id: "svc_abc" }) \u2192 { items: [{ name: "data", mountPath: "/var/data", sizeGb: 10, status: "active" }] }'
3722
4037
  ].join("\n"),
3723
4038
  input: {
3724
- service_id: z15.string().describe("Service publicId (e.g. svc_abc123).")
4039
+ service_id: z16.string().describe("Service publicId (e.g. svc_abc123).")
3725
4040
  },
3726
4041
  handler: async (args2, ctx) => {
3727
4042
  const teamId = await ctx.resolveTeamId();
@@ -3750,15 +4065,15 @@ defineTool({
3750
4065
  'Example: create_volume({ service_id: "svc_abc", name: "data", mount_path: "/var/data", size_gb: 10 }) \u2192 { volume: { name: "data", mountPath: "/var/data", sizeGb: 10, status: "pending" } }'
3751
4066
  ].join("\n"),
3752
4067
  input: {
3753
- service_id: z15.string().describe("Service publicId."),
3754
- name: z15.string().min(1).max(64).regex(/^[a-z0-9-]+$/).describe("Volume name (lowercase alphanumeric + hyphens)."),
3755
- mount_path: z15.string().startsWith("/").max(500).describe("In-container mount path (absolute)."),
4068
+ service_id: z16.string().describe("Service publicId."),
4069
+ name: z16.string().min(1).max(64).regex(/^[a-z0-9-]+$/).describe("Volume name (lowercase alphanumeric + hyphens)."),
4070
+ mount_path: z16.string().startsWith("/").max(500).describe("In-container mount path (absolute)."),
3756
4071
  // 10 GB is the real floor: the block-storage backend rejects anything
3757
4072
  // smaller. Advertising 1 GB here (and defaulting to it) meant taking the
3758
4073
  // defaults produced a volume that provisioned with `Hetzner API error:
3759
4074
  // 422` on the NEXT deploy, with nothing tying the failure back to the
3760
4075
  // size. Reject it at the call instead.
3761
- size_gb: z15.number().int().min(MIN_VOLUME_SIZE_GB).max(100).optional().describe(
4076
+ size_gb: z16.number().int().min(MIN_VOLUME_SIZE_GB).max(100).optional().describe(
3762
4077
  `Disk size in GB (minimum ${MIN_VOLUME_SIZE_GB}, default ${MIN_VOLUME_SIZE_GB}, max 100 via MCP).`
3763
4078
  )
3764
4079
  },
@@ -3796,10 +4111,10 @@ defineTool({
3796
4111
  'Example: update_volume({ service_id: "svc_abc", volume_id: "vol_xyz", size_gb: 20 }) \u2192 { volume: { sizeGb: 20, \u2026 } }'
3797
4112
  ].join("\n"),
3798
4113
  input: {
3799
- service_id: z15.string().describe("Service publicId."),
3800
- volume_id: z15.string().describe("Volume publicId (e.g. vol_\u2026)."),
3801
- mount_path: z15.string().startsWith("/").max(500).optional().describe("New mount path."),
3802
- size_gb: z15.number().int().min(MIN_VOLUME_SIZE_GB).max(100).optional().describe(`New size in GB (minimum ${MIN_VOLUME_SIZE_GB}, grow-only).`)
4114
+ service_id: z16.string().describe("Service publicId."),
4115
+ volume_id: z16.string().describe("Volume publicId (e.g. vol_\u2026)."),
4116
+ mount_path: z16.string().startsWith("/").max(500).optional().describe("New mount path."),
4117
+ size_gb: z16.number().int().min(MIN_VOLUME_SIZE_GB).max(100).optional().describe(`New size in GB (minimum ${MIN_VOLUME_SIZE_GB}, grow-only).`)
3803
4118
  },
3804
4119
  handler: async (args2, ctx) => {
3805
4120
  const teamId = await ctx.resolveTeamId();
@@ -3837,8 +4152,8 @@ defineTool({
3837
4152
  'Example: delete_volume({ service_id: "svc_abc", volume_id: "vol_xyz" }) \u2192 { ok: true }'
3838
4153
  ].join("\n"),
3839
4154
  input: {
3840
- service_id: z15.string().describe("Service publicId."),
3841
- volume_id: z15.string().describe("Volume publicId.")
4155
+ service_id: z16.string().describe("Service publicId."),
4156
+ volume_id: z16.string().describe("Volume publicId.")
3842
4157
  },
3843
4158
  handler: async (args2, ctx) => {
3844
4159
  const teamId = await ctx.resolveTeamId();