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