@hoststack.dev/mcp 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -41,6 +41,14 @@ var ApiClient = class {
41
41
  });
42
42
  return this.handle(res);
43
43
  }
44
+ async put(path, body) {
45
+ const res = await fetch(`${this.baseUrl}${path}`, {
46
+ method: "PUT",
47
+ headers: this.headers,
48
+ body: JSON.stringify(body)
49
+ });
50
+ return this.handle(res);
51
+ }
44
52
  async delete(path) {
45
53
  const res = await fetch(`${this.baseUrl}${path}`, {
46
54
  method: "DELETE",
@@ -906,8 +914,297 @@ defineTool({
906
914
  }
907
915
  });
908
916
 
909
- // src/tools/domains.ts
917
+ // src/tools/dns-records.ts
910
918
  import { z as z7 } from "zod";
919
+ var DNS_RECORD_TYPES = ["A", "AAAA", "CNAME", "MX", "TXT", "NS", "SRV", "CAA", "ALIAS"];
920
+ async function resolveZonePublicId(api, teamId, input) {
921
+ if (input.zone_id) {
922
+ const { zones: zones2 } = await api.get(`/api/dns-zones/${teamId}`);
923
+ const match = zones2.find((z15) => z15.publicId === input.zone_id);
924
+ if (!match) {
925
+ throw new Error(`Zone ${input.zone_id} not found on this team.`);
926
+ }
927
+ return { publicId: match.publicId, domainName: match.domainName };
928
+ }
929
+ if (!input.domain) {
930
+ throw new Error("Provide either zone_id or domain.");
931
+ }
932
+ const { zones } = await api.get(`/api/dns-zones/${teamId}`);
933
+ const fqdn = input.domain.toLowerCase().replace(/\.$/, "");
934
+ const labels = fqdn.split(".");
935
+ for (let i = 0; i < labels.length - 1; i++) {
936
+ const candidate = labels.slice(i).join(".");
937
+ const match = zones.find((z15) => z15.domainName.toLowerCase() === candidate);
938
+ if (match && match.status !== "deleting") {
939
+ return { publicId: match.publicId, domainName: match.domainName };
940
+ }
941
+ }
942
+ throw new Error(
943
+ `No hosted zone matches ${input.domain}. Create the zone in the dashboard (Domains \u2192 DNS) first, then retry.`
944
+ );
945
+ }
946
+ defineTool({
947
+ name: "list_dns_zones",
948
+ category: "dns",
949
+ description: [
950
+ "List authoritative DNS zones the team owns on HostStack's PowerDNS infrastructure. Each zone is the apex domain (e.g. example.com) under which DNS records live.",
951
+ "",
952
+ "When to use: discover which apex domains support record management before calling create_dns_record / list_dns_records. The dashboard equivalent is Domains \u2192 DNS.",
953
+ "",
954
+ 'Returns: { items: Zone[] } \u2014 each zone exposes publicId, domainName, status ("active" | "syncing" | "failed" | "deleting"), nsRecords (the nameservers the parent registry must delegate to), provider, createdAt.',
955
+ "",
956
+ 'Example: list_dns_zones() \u2192 { items: [{ publicId: "dnz_abc", domainName: "micci.dk", status: "active", nsRecords: ["ns1.hoststack.dev","ns2.hoststack.dev"] }] }'
957
+ ].join("\n"),
958
+ input: {},
959
+ handler: async (_args, ctx) => {
960
+ const teamId = await ctx.resolveTeamId();
961
+ const response = await ctx.api.get(`/api/dns-zones/${teamId}`);
962
+ const items = Array.isArray(response.zones) ? response.zones.map(shape) : [];
963
+ const summary = items.length === 0 ? "No DNS zones hosted on this team. Create one in the dashboard (Domains \u2192 DNS)." : `Found ${items.length} hosted DNS zone${items.length === 1 ? "" : "s"}.`;
964
+ return respond({ summary, data: { items } });
965
+ }
966
+ });
967
+ defineTool({
968
+ name: "list_dns_records",
969
+ category: "dns",
970
+ description: [
971
+ "List every active record in a hosted DNS zone. Records are addressed by their type + name + value tuple; PowerDNS gathers records with the same (name, type) into an RRset on the wire.",
972
+ "",
973
+ "When to use: audit what is currently published for a zone before editing, verify a recently-created record landed, or read existing TXT verification strings.",
974
+ "",
975
+ 'Provide EITHER zone_id (the zone publicId, e.g. "dnz_abc") OR domain (an apex or subdomain \u2014 the longest-matching hosted apex wins; "app.example.com" matches a hosted "example.com" zone).',
976
+ "",
977
+ 'Returns: { zone: { publicId, domainName }, items: Record[] } \u2014 each record includes publicId, type, name ("@" for apex, or label), value, ttl, priority?, status ("active" | "syncing" | "failed"), managedBy ("user" | "hoststack" | "poststack"), createdAt.',
978
+ "",
979
+ '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 }] }'
980
+ ].join("\n"),
981
+ input: {
982
+ zone_id: z7.string().optional().describe('Zone publicId (e.g. "dnz_abc").'),
983
+ domain: z7.string().optional().describe("Apex or subdomain \u2014 resolves to the longest-matching hosted zone.")
984
+ },
985
+ handler: async (args, ctx) => {
986
+ const teamId = await ctx.resolveTeamId();
987
+ const zoneInput = {};
988
+ if (args.zone_id !== void 0) zoneInput.zone_id = args.zone_id;
989
+ if (args.domain !== void 0) zoneInput.domain = args.domain;
990
+ const zone = await resolveZonePublicId(ctx.api, teamId, zoneInput);
991
+ const response = await ctx.api.get(
992
+ `/api/dns-zones/${teamId}/${zone.publicId}/records`
993
+ );
994
+ const items = Array.isArray(response.records) ? response.records.map(shape) : [];
995
+ const summary = items.length === 0 ? `Zone ${zone.domainName} has no records yet.` : `Returned ${items.length} record${items.length === 1 ? "" : "s"} for ${zone.domainName}.`;
996
+ return respond({
997
+ summary,
998
+ data: { zone: { publicId: zone.publicId, domainName: zone.domainName }, items }
999
+ });
1000
+ }
1001
+ });
1002
+ defineTool({
1003
+ name: "get_dns_record",
1004
+ category: "dns",
1005
+ description: [
1006
+ 'Fetch a single DNS record by its publicId. Useful for confirming the record landed after create_dns_record / update_dns_record (status transitions from "syncing" to "active" once PowerDNS acknowledges).',
1007
+ "",
1008
+ "When to use: verify a record exists, inspect its current ttl/value, or check the sync status of an in-flight write.",
1009
+ "",
1010
+ "Inputs:",
1011
+ ' - record_id: record publicId (e.g. "dnr_abc"). Returned by list_dns_records and create_dns_record.',
1012
+ "",
1013
+ "Returns: { record: Record }. Mirrors the shape returned by list_dns_records.",
1014
+ "",
1015
+ 'Example: get_dns_record({ record_id: "dnr_abc" }) \u2192 { record: { type: "TXT", name: "@", value: "\u2026", status: "active", \u2026 } }'
1016
+ ].join("\n"),
1017
+ input: {
1018
+ record_id: z7.string().describe("Record publicId.")
1019
+ },
1020
+ handler: async (args, ctx) => {
1021
+ const teamId = await ctx.resolveTeamId();
1022
+ const { zones } = await ctx.api.get(`/api/dns-zones/${teamId}`);
1023
+ for (const zone of zones) {
1024
+ const { records } = await ctx.api.get(
1025
+ `/api/dns-zones/${teamId}/${zone.publicId}/records`
1026
+ );
1027
+ const match = records.find((r) => r.publicId === args.record_id);
1028
+ if (match) {
1029
+ return respond({
1030
+ summary: `Record ${match.type} ${match.name} on ${zone.domainName}.`,
1031
+ data: {
1032
+ zone: { publicId: zone.publicId, domainName: zone.domainName },
1033
+ record: shape(match)
1034
+ }
1035
+ });
1036
+ }
1037
+ }
1038
+ return respondError(`Record ${args.record_id} not found on any zone owned by this team.`);
1039
+ }
1040
+ });
1041
+ defineTool({
1042
+ name: "create_dns_record",
1043
+ category: "dns",
1044
+ description: [
1045
+ 'Create a DNS record on a hosted zone. Writes to the DB first, then mirrors to PowerDNS \u2014 record returns with status="syncing" until the provider acknowledges, then flips to "active". Re-creating the same (zone, type, name, value) tuple is idempotent and returns the existing row.',
1046
+ "",
1047
+ "When to use: add TXT verification records (Google Search Console, domain ownership challenges, DKIM), A/AAAA records pointing a subdomain at an IP, CNAME aliases, MX records, etc.",
1048
+ "",
1049
+ "Provide EITHER zone_id (zone publicId) OR domain (resolves via longest-suffix match against hosted zones).",
1050
+ "",
1051
+ "Inputs:",
1052
+ ' - type: "A" | "AAAA" | "CNAME" | "MX" | "TXT" | "NS" | "SRV" | "CAA" | "ALIAS".',
1053
+ ' - name: "@" for the zone apex, or a subdomain label ("www", "api", "_acme-challenge").',
1054
+ " - content: the literal record value. For TXT pass the unquoted string \u2014 the PowerDNS layer handles quoting.",
1055
+ " - ttl (optional): 60\u201386400 seconds, default 3600.",
1056
+ " - priority (optional, required for MX and SRV): 0\u201365535.",
1057
+ "",
1058
+ "Returns: { record: Record } \u2014 see get_dns_record for the shape.",
1059
+ "",
1060
+ 'Example: create_dns_record({ domain: "micci.dk", type: "TXT", name: "@", content: "google-site-verification=C0V0scK48g2\u2026", ttl: 300 })'
1061
+ ].join("\n"),
1062
+ input: {
1063
+ zone_id: z7.string().optional().describe("Zone publicId."),
1064
+ domain: z7.string().optional().describe("Apex or subdomain to resolve a hosted zone."),
1065
+ type: z7.enum(DNS_RECORD_TYPES).describe("DNS record type."),
1066
+ name: z7.string().min(1).max(253).describe('"@" for apex, or label (no trailing dot).'),
1067
+ content: z7.string().min(1).max(2e3).describe("Record value; TXT values are unquoted."),
1068
+ ttl: z7.number().int().min(60).max(86400).optional().describe("TTL in seconds (default 3600)."),
1069
+ priority: z7.number().int().min(0).max(65535).optional().describe("Required for MX / SRV.")
1070
+ },
1071
+ handler: async (args, ctx) => {
1072
+ if ((args.type === "MX" || args.type === "SRV") && args.priority === void 0) {
1073
+ return respondError(`Priority is required for ${args.type} records (0\u201365535).`);
1074
+ }
1075
+ const teamId = await ctx.resolveTeamId();
1076
+ const zoneInput = {};
1077
+ if (args.zone_id !== void 0) zoneInput.zone_id = args.zone_id;
1078
+ if (args.domain !== void 0) zoneInput.domain = args.domain;
1079
+ const zone = await resolveZonePublicId(ctx.api, teamId, zoneInput);
1080
+ const body = {
1081
+ type: args.type,
1082
+ name: args.name,
1083
+ value: args.content
1084
+ };
1085
+ if (args.ttl !== void 0) body.ttl = args.ttl;
1086
+ if (args.priority !== void 0) body.priority = args.priority;
1087
+ const response = await ctx.api.post(
1088
+ `/api/dns-zones/${teamId}/${zone.publicId}/records`,
1089
+ body
1090
+ );
1091
+ return respond({
1092
+ summary: `Created ${args.type} ${args.name} on ${zone.domainName}.`,
1093
+ data: {
1094
+ zone: { publicId: zone.publicId, domainName: zone.domainName },
1095
+ record: shape(response.record)
1096
+ }
1097
+ });
1098
+ }
1099
+ });
1100
+ defineTool({
1101
+ name: "update_dns_record",
1102
+ category: "dns",
1103
+ description: [
1104
+ "Update an existing DNS record by its publicId. Use this to change a TTL, swap a value, or move the record to a different (name, type) bucket. Full-record PUT semantics: every mutable field must be provided.",
1105
+ "",
1106
+ "When to use: rotate a DKIM key, repoint an A record at a new IP, bump TTL ahead of a planned change.",
1107
+ "",
1108
+ "Inputs:",
1109
+ " - record_id: record publicId.",
1110
+ " - type, name, content: the new values (the route is full-replace, not patch \u2014 fetch with get_dns_record first if you only want to tweak one field).",
1111
+ " - ttl (optional), priority (optional, required for MX / SRV).",
1112
+ "",
1113
+ "Returns: { record: Record } reflecting the new state.",
1114
+ "",
1115
+ 'Example: update_dns_record({ record_id: "dnr_abc", type: "A", name: "api", content: "203.0.113.42", ttl: 300 })'
1116
+ ].join("\n"),
1117
+ input: {
1118
+ record_id: z7.string().describe("Record publicId."),
1119
+ type: z7.enum(DNS_RECORD_TYPES).describe("DNS record type."),
1120
+ name: z7.string().min(1).max(253).describe('"@" for apex, or label.'),
1121
+ content: z7.string().min(1).max(2e3).describe("Record value; TXT unquoted."),
1122
+ ttl: z7.number().int().min(60).max(86400).optional().describe("TTL in seconds (default 3600)."),
1123
+ priority: z7.number().int().min(0).max(65535).optional().describe("Required for MX / SRV.")
1124
+ },
1125
+ handler: async (args, ctx) => {
1126
+ if ((args.type === "MX" || args.type === "SRV") && args.priority === void 0) {
1127
+ return respondError(`Priority is required for ${args.type} records (0\u201365535).`);
1128
+ }
1129
+ const teamId = await ctx.resolveTeamId();
1130
+ const target = await findRecordZone(ctx.api, teamId, args.record_id);
1131
+ if (!target) {
1132
+ return respondError(
1133
+ `Record ${args.record_id} not found on any zone owned by this team.`
1134
+ );
1135
+ }
1136
+ const body = {
1137
+ type: args.type,
1138
+ name: args.name,
1139
+ value: args.content
1140
+ };
1141
+ if (args.ttl !== void 0) body.ttl = args.ttl;
1142
+ if (args.priority !== void 0) body.priority = args.priority;
1143
+ const response = await ctx.api.put(
1144
+ `/api/dns-zones/${teamId}/${target.zone.publicId}/records/${args.record_id}`,
1145
+ body
1146
+ );
1147
+ return respond({
1148
+ summary: `Updated ${args.type} ${args.name} on ${target.zone.domainName}.`,
1149
+ data: {
1150
+ zone: {
1151
+ publicId: target.zone.publicId,
1152
+ domainName: target.zone.domainName
1153
+ },
1154
+ record: shape(response.record)
1155
+ }
1156
+ });
1157
+ }
1158
+ });
1159
+ defineTool({
1160
+ name: "delete_dns_record",
1161
+ category: "dns",
1162
+ description: [
1163
+ "Soft-delete a DNS record and remove it from PowerDNS. DESTRUCTIVE: resolvers stop returning the value as soon as the change propagates. Same code path as the dashboard delete button \u2014 the row is marked deleted and the RRset is re-pushed without it.",
1164
+ "",
1165
+ "When to use: retire a TXT verification record once the challenge has been confirmed; drop an A record for a decommissioned subdomain.",
1166
+ "",
1167
+ "Inputs:",
1168
+ " - record_id: record publicId.",
1169
+ "",
1170
+ "Returns: { ok: true }.",
1171
+ "",
1172
+ 'Example: delete_dns_record({ record_id: "dnr_abc" }) \u2192 { ok: true }'
1173
+ ].join("\n"),
1174
+ input: {
1175
+ record_id: z7.string().describe("Record publicId.")
1176
+ },
1177
+ handler: async (args, ctx) => {
1178
+ const teamId = await ctx.resolveTeamId();
1179
+ const target = await findRecordZone(ctx.api, teamId, args.record_id);
1180
+ if (!target) {
1181
+ return respondError(
1182
+ `Record ${args.record_id} not found on any zone owned by this team.`
1183
+ );
1184
+ }
1185
+ await ctx.api.delete(
1186
+ `/api/dns-zones/${teamId}/${target.zone.publicId}/records/${args.record_id}`
1187
+ );
1188
+ return respond({
1189
+ summary: `Deleted record ${args.record_id} from ${target.zone.domainName}.`,
1190
+ data: { ok: true }
1191
+ });
1192
+ }
1193
+ });
1194
+ async function findRecordZone(api, teamId, recordPublicId) {
1195
+ const { zones } = await api.get(`/api/dns-zones/${teamId}`);
1196
+ for (const zone of zones) {
1197
+ const { records } = await api.get(
1198
+ `/api/dns-zones/${teamId}/${zone.publicId}/records`
1199
+ );
1200
+ const match = records.find((r) => r.publicId === recordPublicId);
1201
+ if (match) return { zone, record: match };
1202
+ }
1203
+ return null;
1204
+ }
1205
+
1206
+ // src/tools/domains.ts
1207
+ import { z as z8 } from "zod";
911
1208
  defineTool({
912
1209
  name: "list_domains",
913
1210
  category: "domains",
@@ -946,8 +1243,8 @@ defineTool({
946
1243
  'Example: add_domain({ hostname: "api.example.com", service_id: "svc_abc" }) \u2192 { domain: { hostname: "api.example.com", dnsTargets: [...], verified: false, \u2026 } }'
947
1244
  ].join("\n"),
948
1245
  input: {
949
- hostname: z7.string().min(3).max(253).describe("Fully-qualified hostname (e.g. api.example.com)."),
950
- service_id: z7.string().optional().describe("Optional service publicId to bind to.")
1246
+ hostname: z8.string().min(3).max(253).describe("Fully-qualified hostname (e.g. api.example.com)."),
1247
+ service_id: z8.string().optional().describe("Optional service publicId to bind to.")
951
1248
  },
952
1249
  handler: async (args, ctx) => {
953
1250
  const teamId = await ctx.resolveTeamId();
@@ -977,7 +1274,7 @@ defineTool({
977
1274
  'Example: verify_domain({ domain_id: "dom_xyz" }) \u2192 { ok: true }'
978
1275
  ].join("\n"),
979
1276
  input: {
980
- domain_id: z7.string().describe("Domain publicId.")
1277
+ domain_id: z8.string().describe("Domain publicId.")
981
1278
  },
982
1279
  handler: async (args, ctx) => {
983
1280
  const teamId = await ctx.resolveTeamId();
@@ -1004,7 +1301,7 @@ defineTool({
1004
1301
  'Example: remove_domain({ domain_id: "dom_xyz" }) \u2192 { ok: true }'
1005
1302
  ].join("\n"),
1006
1303
  input: {
1007
- domain_id: z7.string().describe("Domain publicId.")
1304
+ domain_id: z8.string().describe("Domain publicId.")
1008
1305
  },
1009
1306
  handler: async (args, ctx) => {
1010
1307
  const teamId = await ctx.resolveTeamId();
@@ -1014,7 +1311,7 @@ defineTool({
1014
1311
  });
1015
1312
 
1016
1313
  // src/tools/env-vars.ts
1017
- import { z as z8 } from "zod";
1314
+ import { z as z9 } from "zod";
1018
1315
  defineTool({
1019
1316
  name: "list_env_vars",
1020
1317
  category: "env-vars",
@@ -1031,7 +1328,7 @@ defineTool({
1031
1328
  '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 }] }'
1032
1329
  ].join("\n"),
1033
1330
  input: {
1034
- service_id: z8.string().describe("Service publicId.")
1331
+ service_id: z9.string().describe("Service publicId.")
1035
1332
  },
1036
1333
  handler: async (args, ctx) => {
1037
1334
  const teamId = await ctx.resolveTeamId();
@@ -1060,10 +1357,10 @@ defineTool({
1060
1357
  '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" }'
1061
1358
  ].join("\n"),
1062
1359
  input: {
1063
- service_id: z8.string().describe("Service publicId."),
1064
- key: z8.string().min(1).max(128).describe("Env-var key."),
1065
- value: z8.string().describe("New value."),
1066
- is_secret: z8.boolean().optional().describe("Mark as secret (encrypted, masked on read). Default true.")
1360
+ service_id: z9.string().describe("Service publicId."),
1361
+ key: z9.string().min(1).max(128).describe("Env-var key."),
1362
+ value: z9.string().describe("New value."),
1363
+ is_secret: z9.boolean().optional().describe("Mark as secret (encrypted, masked on read). Default true.")
1067
1364
  },
1068
1365
  handler: async (args, ctx) => {
1069
1366
  const teamId = await ctx.resolveTeamId();
@@ -1115,8 +1412,8 @@ defineTool({
1115
1412
  'Example: delete_env_var({ service_id: "svc_abc", key: "OLD_FLAG" }) \u2192 { ok: true }'
1116
1413
  ].join("\n"),
1117
1414
  input: {
1118
- service_id: z8.string().describe("Service publicId."),
1119
- key: z8.string().min(1).max(128).describe("Env-var key to delete.")
1415
+ service_id: z9.string().describe("Service publicId."),
1416
+ key: z9.string().min(1).max(128).describe("Env-var key to delete.")
1120
1417
  },
1121
1418
  handler: async (args, ctx) => {
1122
1419
  const teamId = await ctx.resolveTeamId();
@@ -1152,12 +1449,12 @@ defineTool({
1152
1449
  '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 }'
1153
1450
  ].join("\n"),
1154
1451
  input: {
1155
- service_id: z8.string().describe("Service publicId."),
1156
- env_vars: z8.array(
1157
- z8.object({
1158
- key: z8.string().min(1).max(128),
1159
- value: z8.string(),
1160
- is_secret: z8.boolean().optional()
1452
+ service_id: z9.string().describe("Service publicId."),
1453
+ env_vars: z9.array(
1454
+ z9.object({
1455
+ key: z9.string().min(1).max(128),
1456
+ value: z9.string(),
1457
+ is_secret: z9.boolean().optional()
1161
1458
  })
1162
1459
  ).max(500).describe("Array of env-var rows. Hard cap 500.")
1163
1460
  },
@@ -1182,7 +1479,7 @@ defineTool({
1182
1479
  });
1183
1480
 
1184
1481
  // src/tools/environments.ts
1185
- import { z as z9 } from "zod";
1482
+ import { z as z10 } from "zod";
1186
1483
  defineTool({
1187
1484
  name: "list_environments",
1188
1485
  category: "environments",
@@ -1199,7 +1496,7 @@ defineTool({
1199
1496
  'Example: list_environments({ project_id: "prj_abc" }) \u2192 { items: [{ name: "Production", type: "production", isDefault: true }, { name: "Staging", type: "staging" }] }'
1200
1497
  ].join("\n"),
1201
1498
  input: {
1202
- project_id: z9.string().describe("Project publicId.")
1499
+ project_id: z10.string().describe("Project publicId.")
1203
1500
  },
1204
1501
  handler: async (args, ctx) => {
1205
1502
  const teamId = await ctx.resolveTeamId();
@@ -1228,10 +1525,10 @@ defineTool({
1228
1525
  'Example: create_environment({ project_id: "prj_abc", name: "Staging", type: "staging" }) \u2192 { environment: { id: 7, publicId: "environment_\u2026", name: "Staging", type: "staging" } }'
1229
1526
  ].join("\n"),
1230
1527
  input: {
1231
- project_id: z9.string().describe("Project publicId."),
1232
- name: z9.string().min(1).max(64).describe("Environment name (1\u201364 chars)."),
1233
- type: z9.enum(["production", "staging", "development", "preview"]).describe("Environment type \u2014 drives the hostname suffix."),
1234
- is_protected: z9.boolean().optional().describe("Require admin role for destructive actions. Default false.")
1528
+ project_id: z10.string().describe("Project publicId."),
1529
+ name: z10.string().min(1).max(64).describe("Environment name (1\u201364 chars)."),
1530
+ type: z10.enum(["production", "staging", "development", "preview"]).describe("Environment type \u2014 drives the hostname suffix."),
1531
+ is_protected: z10.boolean().optional().describe("Require admin role for destructive actions. Default false.")
1235
1532
  },
1236
1533
  handler: async (args, ctx) => {
1237
1534
  const teamId = await ctx.resolveTeamId();
@@ -1265,8 +1562,8 @@ defineTool({
1265
1562
  'Example: delete_environment({ project_id: "prj_abc", environment_id: "environment_xyz" }) \u2192 { success: true }'
1266
1563
  ].join("\n"),
1267
1564
  input: {
1268
- project_id: z9.string().describe("Project publicId."),
1269
- environment_id: z9.union([z9.string(), z9.number()]).describe("Environment publicId or numeric id.")
1565
+ project_id: z10.string().describe("Project publicId."),
1566
+ environment_id: z10.union([z10.string(), z10.number()]).describe("Environment publicId or numeric id.")
1270
1567
  },
1271
1568
  handler: async (args, ctx) => {
1272
1569
  const teamId = await ctx.resolveTeamId();
@@ -1297,9 +1594,9 @@ defineTool({
1297
1594
  'Example: promote_deploy({ service_id: "svc_staging", deploy_id: "dpl_built", target_environment_id: "environment_prod" }) \u2192 { deploy: { id: 99, status: "pending", trigger: "rollback", dockerImageId: "sha256:\u2026" } }'
1298
1595
  ].join("\n"),
1299
1596
  input: {
1300
- service_id: z9.string().describe("Source service publicId."),
1301
- deploy_id: z9.string().describe("Source deploy publicId (must have a built image)."),
1302
- target_environment_id: z9.union([z9.string(), z9.number()]).describe("Target environment publicId or numeric id.")
1597
+ service_id: z10.string().describe("Source service publicId."),
1598
+ deploy_id: z10.string().describe("Source deploy publicId (must have a built image)."),
1599
+ target_environment_id: z10.union([z10.string(), z10.number()]).describe("Target environment publicId or numeric id.")
1303
1600
  },
1304
1601
  handler: async (args, ctx) => {
1305
1602
  const teamId = await ctx.resolveTeamId();
@@ -1348,7 +1645,7 @@ defineTool({
1348
1645
  });
1349
1646
 
1350
1647
  // src/tools/notifications.ts
1351
- import { z as z10 } from "zod";
1648
+ import { z as z11 } from "zod";
1352
1649
  var NOTIFICATION_EVENTS = [
1353
1650
  "deploy.started",
1354
1651
  "deploy.succeeded",
@@ -1407,10 +1704,10 @@ defineTool({
1407
1704
  "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'] })"
1408
1705
  ].join("\n"),
1409
1706
  input: {
1410
- type: z10.enum(["slack", "discord", "email"]).describe("Channel type."),
1411
- name: z10.string().min(1).max(128).describe("Human-readable label."),
1412
- webhook_url: z10.string().max(500).describe("Slack/Discord webhook URL or email address (when type=email)."),
1413
- events: z10.array(z10.enum(NOTIFICATION_EVENTS)).describe("List of events the channel subscribes to. Empty list = subscribe to nothing.")
1707
+ type: z11.enum(["slack", "discord", "email"]).describe("Channel type."),
1708
+ name: z11.string().min(1).max(128).describe("Human-readable label."),
1709
+ webhook_url: z11.string().max(500).describe("Slack/Discord webhook URL or email address (when type=email)."),
1710
+ events: z11.array(z11.enum(NOTIFICATION_EVENTS)).describe("List of events the channel subscribes to. Empty list = subscribe to nothing.")
1414
1711
  },
1415
1712
  handler: async (args, ctx) => {
1416
1713
  const teamId = await ctx.resolveTeamId();
@@ -1446,10 +1743,10 @@ defineTool({
1446
1743
  "Example: update_notification_channel({ channel_id: 3, events: ['deploy.failed', 'service.restart_failed', 'git.auth_failed'] })"
1447
1744
  ].join("\n"),
1448
1745
  input: {
1449
- channel_id: z10.number().int().positive().describe("Numeric channel id from list_notification_channels."),
1450
- name: z10.string().min(1).max(128).optional().describe("New label."),
1451
- active: z10.boolean().optional().describe("false silences without deleting."),
1452
- events: z10.array(z10.enum(NOTIFICATION_EVENTS)).optional().describe("Replaces the full subscription list.")
1746
+ channel_id: z11.number().int().positive().describe("Numeric channel id from list_notification_channels."),
1747
+ name: z11.string().min(1).max(128).optional().describe("New label."),
1748
+ active: z11.boolean().optional().describe("false silences without deleting."),
1749
+ events: z11.array(z11.enum(NOTIFICATION_EVENTS)).optional().describe("Replaces the full subscription list.")
1453
1750
  },
1454
1751
  handler: async (args, ctx) => {
1455
1752
  const teamId = await ctx.resolveTeamId();
@@ -1488,7 +1785,7 @@ defineTool({
1488
1785
  "Example: delete_notification_channel({ channel_id: 3 }) \u2192 { ok: true }"
1489
1786
  ].join("\n"),
1490
1787
  input: {
1491
- channel_id: z10.number().int().positive().describe("Numeric channel id.")
1788
+ channel_id: z11.number().int().positive().describe("Numeric channel id.")
1492
1789
  },
1493
1790
  handler: async (args, ctx) => {
1494
1791
  const teamId = await ctx.resolveTeamId();
@@ -1515,7 +1812,7 @@ defineTool({
1515
1812
  "Example: test_notification_channel({ channel_id: 3 }) \u2192 { success: true }"
1516
1813
  ].join("\n"),
1517
1814
  input: {
1518
- channel_id: z10.number().int().positive().describe("Numeric channel id.")
1815
+ channel_id: z11.number().int().positive().describe("Numeric channel id.")
1519
1816
  },
1520
1817
  handler: async (args, ctx) => {
1521
1818
  const teamId = await ctx.resolveTeamId();
@@ -1530,7 +1827,7 @@ defineTool({
1530
1827
  });
1531
1828
 
1532
1829
  // src/tools/projects.ts
1533
- import { z as z11 } from "zod";
1830
+ import { z as z12 } from "zod";
1534
1831
  defineTool({
1535
1832
  name: "list_projects",
1536
1833
  category: "projects",
@@ -1570,9 +1867,9 @@ defineTool({
1570
1867
  'Example: create_project({ name: "billing-api", description: "Stripe webhooks", region: "fsn1" }) \u2192 { project: { id: 12, publicId: "prj_\u2026", \u2026 } }'
1571
1868
  ].join("\n"),
1572
1869
  input: {
1573
- name: z11.string().min(1).max(60).describe("Project name (1\u201360 chars)."),
1574
- description: z11.string().max(500).optional().describe("Short description (\u2264500 chars)."),
1575
- region: z11.enum(["fsn1", "nbg1", "hel1"]).optional().describe("Hetzner region: fsn1 | nbg1 | hel1.")
1870
+ name: z12.string().min(1).max(60).describe("Project name (1\u201360 chars)."),
1871
+ description: z12.string().max(500).optional().describe("Short description (\u2264500 chars)."),
1872
+ region: z12.enum(["fsn1", "nbg1", "hel1"]).optional().describe("Hetzner region: fsn1 | nbg1 | hel1.")
1576
1873
  },
1577
1874
  handler: async (args, ctx) => {
1578
1875
  const teamId = await ctx.resolveTeamId();
@@ -1603,9 +1900,9 @@ defineTool({
1603
1900
  'Example: update_project({ project_id: "prj_abc", name: "billing-prod" }) \u2192 { project: { name: "billing-prod", \u2026 } }'
1604
1901
  ].join("\n"),
1605
1902
  input: {
1606
- project_id: z11.string().describe("Project publicId."),
1607
- name: z11.string().min(1).max(60).optional().describe("New name (1\u201360 chars)."),
1608
- description: z11.string().max(500).optional().describe("New description (\u2264500 chars).")
1903
+ project_id: z12.string().describe("Project publicId."),
1904
+ name: z12.string().min(1).max(60).optional().describe("New name (1\u201360 chars)."),
1905
+ description: z12.string().max(500).optional().describe("New description (\u2264500 chars).")
1609
1906
  },
1610
1907
  handler: async (args, ctx) => {
1611
1908
  if (args.name === void 0 && args.description === void 0) {
@@ -1639,7 +1936,7 @@ defineTool({
1639
1936
  'Example: get_project({ project_id: "prj_abc" }) \u2192 { project: { id: 12, name: "billing", \u2026 } }'
1640
1937
  ].join("\n"),
1641
1938
  input: {
1642
- project_id: z11.string().describe("Project publicId (e.g. prj_abc123).")
1939
+ project_id: z12.string().describe("Project publicId (e.g. prj_abc123).")
1643
1940
  },
1644
1941
  handler: async (args, ctx) => {
1645
1942
  const teamId = await ctx.resolveTeamId();
@@ -1651,7 +1948,7 @@ defineTool({
1651
1948
  });
1652
1949
 
1653
1950
  // src/tools/services.ts
1654
- import { z as z12 } from "zod";
1951
+ import { z as z13 } from "zod";
1655
1952
  defineTool({
1656
1953
  name: "list_services",
1657
1954
  category: "services",
@@ -1671,10 +1968,10 @@ defineTool({
1671
1968
  'Example: list_services({ status: "failed" }) \u2192 only services that need attention.'
1672
1969
  ].join("\n"),
1673
1970
  input: {
1674
- project_id: z12.union([z12.number().int().positive(), z12.string()]).optional().describe("Project filter \u2014 numeric id or publicId."),
1675
- environment_id: z12.union([z12.number().int().positive(), z12.string()]).optional().describe("Environment filter \u2014 numeric id or publicId."),
1676
- status: z12.enum(["active", "deploying", "suspended", "failed", "not_deployed"]).optional().describe("Filter by current runtime status."),
1677
- type: z12.enum(["web_service", "private_service", "worker", "cron_job", "static_site"]).optional().describe("Filter by service type.")
1971
+ project_id: z13.union([z13.number().int().positive(), z13.string()]).optional().describe("Project filter \u2014 numeric id or publicId."),
1972
+ environment_id: z13.union([z13.number().int().positive(), z13.string()]).optional().describe("Environment filter \u2014 numeric id or publicId."),
1973
+ status: z13.enum(["active", "deploying", "suspended", "failed", "not_deployed"]).optional().describe("Filter by current runtime status."),
1974
+ type: z13.enum(["web_service", "private_service", "worker", "cron_job", "static_site"]).optional().describe("Filter by service type.")
1678
1975
  },
1679
1976
  handler: async (args, ctx) => {
1680
1977
  const teamId = await ctx.resolveTeamId();
@@ -1711,24 +2008,30 @@ defineTool({
1711
2008
  name: "get_service",
1712
2009
  category: "services",
1713
2010
  description: [
1714
- "Fetch a single service by ID, including its current status and configuration summary.",
2011
+ "Fetch a single service by ID with its current status AND its service_config row (resources, health-check tuning, scaling, restart policy).",
1715
2012
  "",
1716
- "When to use: drilling into a service after list_services, checking deploy/runtime status, or grabbing the repo+branch before triggering a deploy.",
2013
+ "When to use: drilling into a service after list_services, checking deploy/runtime status, grabbing the repo+branch before triggering a deploy, or inspecting the health-check / autoscale knobs before tweaking them via update_service_config.",
1717
2014
  "",
1718
2015
  "Inputs:",
1719
2016
  ' - service_id: publicId of the service (e.g. "svc_abc123").',
1720
2017
  "",
1721
- "Returns: { service: Service } \u2014 type, status, runtime, repoUrl, branch, autoDeploy, region, plan, createdAt, updatedAt.",
2018
+ "Returns: { service: Service, config: ServiceConfig } \u2014 service has type/status/runtime/repoUrl/branch/autoDeploy/region/plan/timestamps; config has memoryMb, cpuShares, diskSizeGb, port, protocol, healthCheckEnabled, healthCheckInterval, healthCheckTimeout, healthCheckGracePeriodSec, restartPolicy, preDeployCommand, min/maxInstances, scale thresholds.",
1722
2019
  "",
1723
- 'Example: get_service({ service_id: "svc_abc" }) \u2192 { service: { type: "web", status: "running", \u2026 } }'
2020
+ 'Example: get_service({ service_id: "svc_abc" }) \u2192 { service: { type: "web", status: "running", \u2026 }, config: { healthCheckGracePeriodSec: 120, \u2026 } }'
1724
2021
  ].join("\n"),
1725
2022
  input: {
1726
- service_id: z12.string().describe("Service publicId (e.g. svc_abc123).")
2023
+ service_id: z13.string().describe("Service publicId (e.g. svc_abc123).")
1727
2024
  },
1728
2025
  handler: async (args, ctx) => {
1729
2026
  const teamId = await ctx.resolveTeamId();
1730
- const response = await ctx.hoststack.services.get(teamId, args.service_id);
1731
- const data = { service: shapeService(response.service) };
2027
+ const [serviceResponse, configResponse] = await Promise.all([
2028
+ ctx.hoststack.services.get(teamId, args.service_id),
2029
+ ctx.hoststack.services.getConfig(teamId, args.service_id)
2030
+ ]);
2031
+ const data = {
2032
+ service: shapeService(serviceResponse.service),
2033
+ config: shape(configResponse.config)
2034
+ };
1732
2035
  const status = data.service && "status" in data.service ? data.service.status : "unknown";
1733
2036
  return respond({ summary: `Service ${args.service_id} is ${status}.`, data });
1734
2037
  }
@@ -1749,7 +2052,7 @@ defineTool({
1749
2052
  'Example: get_service_metrics({ service_id: "svc_abc" }) \u2192 { metrics: { cpu: 0.42, memory: 0.71, \u2026 } }'
1750
2053
  ].join("\n"),
1751
2054
  input: {
1752
- service_id: z12.string().describe("Service publicId.")
2055
+ service_id: z13.string().describe("Service publicId.")
1753
2056
  },
1754
2057
  handler: async (args, ctx) => {
1755
2058
  const teamId = await ctx.resolveTeamId();
@@ -1778,9 +2081,9 @@ defineTool({
1778
2081
  'Example: get_service_metrics_history({ service_id: "svc_abc", from: "-1h" }) \u2192 60-ish points for the last hour.'
1779
2082
  ].join("\n"),
1780
2083
  input: {
1781
- service_id: z12.string().describe("Service publicId."),
1782
- from: z12.string().optional().describe('ISO-8601 lower bound or relative offset (e.g. "-1h", "-2d").'),
1783
- to: z12.string().optional().describe("ISO-8601 upper bound; defaults to now.")
2084
+ service_id: z13.string().describe("Service publicId."),
2085
+ from: z13.string().optional().describe('ISO-8601 lower bound or relative offset (e.g. "-1h", "-2d").'),
2086
+ to: z13.string().optional().describe("ISO-8601 upper bound; defaults to now.")
1784
2087
  },
1785
2088
  handler: async (args, ctx) => {
1786
2089
  const teamId = await ctx.resolveTeamId();
@@ -1824,8 +2127,8 @@ defineTool({
1824
2127
  'Example: update_service({ service_id: "svc_abc", name: "api-prod" }) \u2192 { service: { name: "api-prod", \u2026 } }'
1825
2128
  ].join("\n"),
1826
2129
  input: {
1827
- service_id: z12.string().describe("Service publicId."),
1828
- name: z12.string().min(1).max(60).describe("New service name (1\u201360 chars).")
2130
+ service_id: z13.string().describe("Service publicId."),
2131
+ name: z13.string().min(1).max(60).describe("New service name (1\u201360 chars).")
1829
2132
  },
1830
2133
  handler: async (args, ctx) => {
1831
2134
  const teamId = await ctx.resolveTeamId();
@@ -1840,9 +2143,9 @@ defineTool({
1840
2143
  name: "update_service_config",
1841
2144
  category: "services",
1842
2145
  description: [
1843
- "Update build/runtime configuration for a service: build command, start command, install command, branch, root directory, dockerfile path, auto-deploy flag, instance count. All fields optional \u2014 pass only what you want to change.",
2146
+ "Update build/runtime configuration for a service. All fields optional \u2014 pass only what you want to change.",
1844
2147
  "",
1845
- "When to use: the user wants to tweak how a service builds or runs. Build/runtime fields (branch, install/build/start command, root, dockerfile) take effect on the next deploy \u2014 call trigger_deploy after if you need them applied immediately. instance_count rescales without a redeploy.",
2148
+ "When to use: the user wants to tweak how a service builds, runs, scales, or health-checks. Build/runtime fields (branch, install/build/start command, root, dockerfile) take effect on the next deploy \u2014 call trigger_deploy after if you need them applied immediately. Instance_count, resource and health-check changes rescale or rewire without a redeploy.",
1846
2149
  "",
1847
2150
  "Inputs:",
1848
2151
  " - service_id: publicId of the service.",
@@ -1851,27 +2154,59 @@ defineTool({
1851
2154
  " - root_directory (optional): build context root inside the repo.",
1852
2155
  " - dockerfile_path (optional): path to Dockerfile relative to root_directory. Pass null to clear.",
1853
2156
  " - auto_deploy (optional): boolean \u2014 auto-deploy on git push.",
1854
- " - instance_count (optional): integer \u22651 \u2014 pin both min and max instances to this value.",
2157
+ ' - health_check_path (optional): HTTP path the platform GETs to verify liveness (e.g. "/health"). Pass null for TCP-only check.',
2158
+ " - health_check_enabled (optional): boolean \u2014 toggle health checking on/off.",
2159
+ " - health_check_interval (optional): integer 5\u2013300 seconds \u2014 how often the check runs.",
2160
+ " - health_check_timeout (optional): integer 1\u201360 seconds \u2014 single-attempt timeout.",
2161
+ ' - health_check_grace_period_sec (optional): integer 1\u20131800 seconds \u2014 startup tolerance before failures count. RAISE THIS (e.g. 180) when the agent reports "Health check timed out" on a cold-boot app (Bun + Vite SSR typically need 90\u2013180s).',
2162
+ " - memory_mb (optional): integer 128\u201316384 \u2014 container memory cap.",
2163
+ " - cpu_shares (optional): integer 128\u20134096 \u2014 relative CPU weight.",
2164
+ " - disk_size_gb (optional): integer 1\u2013100 \u2014 ephemeral disk cap.",
2165
+ " - port (optional): integer 1\u201365535 \u2014 container port the platform forwards traffic to.",
2166
+ ' - protocol (optional): "http" | "tcp".',
2167
+ ' - restart_policy (optional): "always" | "on-failure" | "no".',
2168
+ " - pre_deploy_command (optional): shell command run before the new release accepts traffic (typical use: migrations).",
2169
+ " - instance_count (optional): integer 1\u201350 \u2014 pin both min and max instances to this value.",
2170
+ " - min_instances, max_instances (optional): integers \u2014 autoscale bounds. Use instead of instance_count when you want a range.",
2171
+ " - scale_cpu_threshold, scale_memory_threshold (optional): integer 10\u2013100 \u2014 autoscale trigger percentage.",
1855
2172
  ' - log_filter_rules (optional): list of { pattern, action } rules applied to runtime logs at query time. Pattern matches the message by case-insensitive substring; action is "drop" (filter out) or "downgrade" (flip stderr \u2192 stdout so it stops looking like an error). Pass [] to clear all rules. Capped at 50 rules.',
1856
2173
  "",
1857
2174
  "Returns: { service?: Service, config?: ServiceConfig } \u2014 whichever rows were touched.",
1858
2175
  "",
1859
- 'Example: update_service_config({ service_id: "svc_abc", start_command: "bun apps/api/src/index.ts" }) \u2192 { service: { startCommand: "bun apps/api/src/index.ts", \u2026 } }'
2176
+ 'Example: update_service_config({ service_id: "svc_abc", health_check_grace_period_sec: 180 }) \u2192 { config: { healthCheckGracePeriodSec: 180, \u2026 } }'
1860
2177
  ].join("\n"),
1861
2178
  input: {
1862
- service_id: z12.string().describe("Service publicId."),
1863
- install_command: z12.string().nullable().optional().describe("Install shell command. Null clears."),
1864
- build_command: z12.string().nullable().optional().describe("Build shell command. Null clears."),
1865
- start_command: z12.string().nullable().optional().describe("Start shell command. Null clears."),
1866
- branch: z12.string().optional().describe("Git branch to track."),
1867
- root_directory: z12.string().optional().describe("Build context root."),
1868
- dockerfile_path: z12.string().nullable().optional().describe("Path to Dockerfile relative to root. Null clears."),
1869
- auto_deploy: z12.boolean().optional().describe("Auto-deploy on push."),
1870
- instance_count: z12.number().int().positive().max(50).optional().describe("Pin min and max instances to this value (1\u201350)."),
1871
- log_filter_rules: z12.array(
1872
- z12.object({
1873
- pattern: z12.string().min(1).max(200),
1874
- action: z12.enum(["drop", "downgrade"])
2179
+ service_id: z13.string().describe("Service publicId."),
2180
+ install_command: z13.string().nullable().optional().describe("Install shell command. Null clears."),
2181
+ build_command: z13.string().nullable().optional().describe("Build shell command. Null clears."),
2182
+ start_command: z13.string().nullable().optional().describe("Start shell command. Null clears."),
2183
+ branch: z13.string().optional().describe("Git branch to track."),
2184
+ root_directory: z13.string().optional().describe("Build context root."),
2185
+ dockerfile_path: z13.string().nullable().optional().describe("Path to Dockerfile relative to root. Null clears."),
2186
+ auto_deploy: z13.boolean().optional().describe("Auto-deploy on push."),
2187
+ health_check_path: z13.string().nullable().optional().describe('HTTP health-check path (e.g. "/health"). Null = TCP-only check.'),
2188
+ health_check_enabled: z13.boolean().optional().describe("Toggle health checking on/off."),
2189
+ health_check_interval: z13.number().int().min(5).max(300).optional().describe("How often the check runs, in seconds (5\u2013300)."),
2190
+ health_check_timeout: z13.number().int().min(1).max(60).optional().describe("Single-attempt timeout in seconds (1\u201360)."),
2191
+ health_check_grace_period_sec: z13.number().int().min(1).max(1800).optional().describe(
2192
+ "Startup grace period in seconds (1\u20131800). Raise this if the app needs more time to boot before health checks start counting failures."
2193
+ ),
2194
+ memory_mb: z13.number().int().min(128).max(16384).optional().describe("Container memory cap in MB (128\u201316384)."),
2195
+ cpu_shares: z13.number().int().min(128).max(4096).optional().describe("Relative CPU weight (128\u20134096)."),
2196
+ disk_size_gb: z13.number().int().min(1).max(100).optional().describe("Ephemeral disk size in GB (1\u2013100)."),
2197
+ port: z13.number().int().min(1).max(65535).optional().describe("Container port the platform forwards traffic to."),
2198
+ protocol: z13.enum(["http", "tcp"]).optional().describe("Traffic protocol."),
2199
+ restart_policy: z13.enum(["always", "on-failure", "no"]).optional().describe("Docker restart policy."),
2200
+ pre_deploy_command: z13.string().optional().describe("Shell command run before the new release accepts traffic."),
2201
+ instance_count: z13.number().int().positive().max(50).optional().describe("Pin min and max instances to this value (1\u201350)."),
2202
+ min_instances: z13.number().int().min(0).max(50).optional().describe("Autoscale lower bound. Use with max_instances for a range."),
2203
+ max_instances: z13.number().int().min(1).max(50).optional().describe("Autoscale upper bound. Use with min_instances for a range."),
2204
+ scale_cpu_threshold: z13.number().int().min(10).max(100).optional().describe("Autoscale CPU trigger percentage (10\u2013100)."),
2205
+ scale_memory_threshold: z13.number().int().min(10).max(100).optional().describe("Autoscale memory trigger percentage (10\u2013100)."),
2206
+ log_filter_rules: z13.array(
2207
+ z13.object({
2208
+ pattern: z13.string().min(1).max(200),
2209
+ action: z13.enum(["drop", "downgrade"])
1875
2210
  })
1876
2211
  ).max(50).optional().describe(
1877
2212
  "Runtime-log filter rules. Empty array [] clears all rules. Each pattern is case-insensitive substring match against the message."
@@ -1889,11 +2224,35 @@ defineTool({
1889
2224
  if (args.branch !== void 0) serviceUpdate["branch"] = args.branch;
1890
2225
  if (args.root_directory !== void 0) serviceUpdate["rootDirectory"] = args.root_directory;
1891
2226
  if (args.auto_deploy !== void 0) serviceUpdate["autoDeploy"] = args.auto_deploy;
2227
+ if (args.health_check_path !== void 0)
2228
+ serviceUpdate["healthCheckPath"] = args.health_check_path;
1892
2229
  const configUpdate = {};
2230
+ if (args.health_check_enabled !== void 0)
2231
+ configUpdate["healthCheckEnabled"] = args.health_check_enabled;
2232
+ if (args.health_check_interval !== void 0)
2233
+ configUpdate["healthCheckInterval"] = args.health_check_interval;
2234
+ if (args.health_check_timeout !== void 0)
2235
+ configUpdate["healthCheckTimeout"] = args.health_check_timeout;
2236
+ if (args.health_check_grace_period_sec !== void 0)
2237
+ configUpdate["healthCheckGracePeriodSec"] = args.health_check_grace_period_sec;
2238
+ if (args.memory_mb !== void 0) configUpdate["memoryMb"] = args.memory_mb;
2239
+ if (args.cpu_shares !== void 0) configUpdate["cpuShares"] = args.cpu_shares;
2240
+ if (args.disk_size_gb !== void 0) configUpdate["diskSizeGb"] = args.disk_size_gb;
2241
+ if (args.port !== void 0) configUpdate["port"] = args.port;
2242
+ if (args.protocol !== void 0) configUpdate["protocol"] = args.protocol;
2243
+ if (args.restart_policy !== void 0) configUpdate["restartPolicy"] = args.restart_policy;
2244
+ if (args.pre_deploy_command !== void 0)
2245
+ configUpdate["preDeployCommand"] = args.pre_deploy_command;
1893
2246
  if (args.instance_count !== void 0) {
1894
2247
  configUpdate["minInstances"] = args.instance_count;
1895
2248
  configUpdate["maxInstances"] = args.instance_count;
1896
2249
  }
2250
+ if (args.min_instances !== void 0) configUpdate["minInstances"] = args.min_instances;
2251
+ if (args.max_instances !== void 0) configUpdate["maxInstances"] = args.max_instances;
2252
+ if (args.scale_cpu_threshold !== void 0)
2253
+ configUpdate["scaleCpuThreshold"] = args.scale_cpu_threshold;
2254
+ if (args.scale_memory_threshold !== void 0)
2255
+ configUpdate["scaleMemoryThreshold"] = args.scale_memory_threshold;
1897
2256
  if (args.log_filter_rules !== void 0) {
1898
2257
  configUpdate["logFilterRules"] = args.log_filter_rules;
1899
2258
  }
@@ -1942,7 +2301,7 @@ defineTool({
1942
2301
  'Example: suspend_service({ service_id: "svc_dev" }) \u2192 { ok: true }'
1943
2302
  ].join("\n"),
1944
2303
  input: {
1945
- service_id: z12.string().describe("Service publicId.")
2304
+ service_id: z13.string().describe("Service publicId.")
1946
2305
  },
1947
2306
  handler: async (args, ctx) => {
1948
2307
  const teamId = await ctx.resolveTeamId();
@@ -1966,7 +2325,7 @@ defineTool({
1966
2325
  'Example: resume_service({ service_id: "svc_dev" }) \u2192 { ok: true }'
1967
2326
  ].join("\n"),
1968
2327
  input: {
1969
- service_id: z12.string().describe("Service publicId.")
2328
+ service_id: z13.string().describe("Service publicId.")
1970
2329
  },
1971
2330
  handler: async (args, ctx) => {
1972
2331
  const teamId = await ctx.resolveTeamId();
@@ -2000,16 +2359,16 @@ defineTool({
2000
2359
  ' - Just count error lines without fetching them: get_service_logs({ service_id: "svc_abc", level: "error", since: "-5m", count_only: true }) \u2192 { count: 47 }'
2001
2360
  ].join("\n"),
2002
2361
  input: {
2003
- service_id: z12.string().describe("Service publicId."),
2004
- lines: z12.number().int().positive().max(1e3).optional().describe("Tail size; default 200, hard cap 1000."),
2005
- since: z12.string().optional().describe('ISO-8601 timestamp or relative offset (e.g. "-5m", "-1h").'),
2006
- until: z12.string().optional().describe("ISO-8601 timestamp or relative offset upper bound."),
2007
- stream: z12.enum(["stdout", "stderr"]).optional().describe("Restrict to one stream."),
2008
- level: z12.enum(["stdout", "stderr", "trace", "debug", "info", "warn", "error", "fatal"]).optional().describe(
2362
+ service_id: z13.string().describe("Service publicId."),
2363
+ lines: z13.number().int().positive().max(1e3).optional().describe("Tail size; default 200, hard cap 1000."),
2364
+ since: z13.string().optional().describe('ISO-8601 timestamp or relative offset (e.g. "-5m", "-1h").'),
2365
+ until: z13.string().optional().describe("ISO-8601 timestamp or relative offset upper bound."),
2366
+ stream: z13.enum(["stdout", "stderr"]).optional().describe("Restrict to one stream."),
2367
+ level: z13.enum(["stdout", "stderr", "trace", "debug", "info", "warn", "error", "fatal"]).optional().describe(
2009
2368
  "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)."
2010
2369
  ),
2011
- search: z12.string().max(100).optional().describe("Case-insensitive substring filter."),
2012
- count_only: z12.boolean().optional().describe("When true, return only { count } \u2014 skips the log payload.")
2370
+ search: z13.string().max(100).optional().describe("Case-insensitive substring filter."),
2371
+ count_only: z13.boolean().optional().describe("When true, return only { count } \u2014 skips the log payload.")
2013
2372
  },
2014
2373
  handler: async (args, ctx) => {
2015
2374
  const teamId = await ctx.resolveTeamId();
@@ -2058,14 +2417,14 @@ defineTool({
2058
2417
  '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 } } }.'
2059
2418
  ].join("\n"),
2060
2419
  input: {
2061
- service_ids: z12.array(z12.string()).min(1).max(10).describe("Service publicIds (1\u201310). Hard cap 10 to bound parallel work."),
2062
- lines_per_service: z12.number().int().positive().max(500).optional().describe("Tail size per service; default 100, hard cap 500."),
2063
- since: z12.string().optional().describe('ISO-8601 timestamp or relative offset (e.g. "-5m", "-1h").'),
2064
- until: z12.string().optional().describe("ISO-8601 timestamp or relative offset upper bound."),
2065
- stream: z12.enum(["stdout", "stderr"]).optional().describe("Restrict to one stream."),
2066
- level: z12.enum(["stdout", "stderr", "trace", "debug", "info", "warn", "error", "fatal"]).optional().describe("Structured log level filter (same as get_service_logs)."),
2067
- search: z12.string().max(100).optional().describe("Case-insensitive substring filter."),
2068
- count_only: z12.boolean().optional().describe("When true, return only counts per service \u2014 skips the log payload.")
2420
+ service_ids: z13.array(z13.string()).min(1).max(10).describe("Service publicIds (1\u201310). Hard cap 10 to bound parallel work."),
2421
+ lines_per_service: z13.number().int().positive().max(500).optional().describe("Tail size per service; default 100, hard cap 500."),
2422
+ since: z13.string().optional().describe('ISO-8601 timestamp or relative offset (e.g. "-5m", "-1h").'),
2423
+ until: z13.string().optional().describe("ISO-8601 timestamp or relative offset upper bound."),
2424
+ stream: z13.enum(["stdout", "stderr"]).optional().describe("Restrict to one stream."),
2425
+ level: z13.enum(["stdout", "stderr", "trace", "debug", "info", "warn", "error", "fatal"]).optional().describe("Structured log level filter (same as get_service_logs)."),
2426
+ search: z13.string().max(100).optional().describe("Case-insensitive substring filter."),
2427
+ count_only: z13.boolean().optional().describe("When true, return only counts per service \u2014 skips the log payload.")
2069
2428
  },
2070
2429
  handler: async (args, ctx) => {
2071
2430
  const teamId = await ctx.resolveTeamId();
@@ -2111,7 +2470,7 @@ defineTool({
2111
2470
  });
2112
2471
 
2113
2472
  // src/tools/volumes.ts
2114
- import { z as z13 } from "zod";
2473
+ import { z as z14 } from "zod";
2115
2474
  defineTool({
2116
2475
  name: "list_volumes",
2117
2476
  category: "volumes",
@@ -2128,7 +2487,7 @@ defineTool({
2128
2487
  'Example: list_volumes({ service_id: "svc_abc" }) \u2192 { items: [{ name: "data", mountPath: "/var/data", sizeGb: 10, status: "active" }] }'
2129
2488
  ].join("\n"),
2130
2489
  input: {
2131
- service_id: z13.string().describe("Service publicId (e.g. svc_abc123).")
2490
+ service_id: z14.string().describe("Service publicId (e.g. svc_abc123).")
2132
2491
  },
2133
2492
  handler: async (args, ctx) => {
2134
2493
  const teamId = await ctx.resolveTeamId();
@@ -2157,10 +2516,10 @@ defineTool({
2157
2516
  '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: "active" } }'
2158
2517
  ].join("\n"),
2159
2518
  input: {
2160
- service_id: z13.string().describe("Service publicId."),
2161
- name: z13.string().min(1).max(64).regex(/^[a-z0-9-]+$/).describe("Volume name (lowercase alphanumeric + hyphens)."),
2162
- mount_path: z13.string().startsWith("/").max(500).describe("In-container mount path (absolute)."),
2163
- size_gb: z13.number().int().min(1).max(100).optional().describe("Disk size in GB (default 1, max 100).")
2519
+ service_id: z14.string().describe("Service publicId."),
2520
+ name: z14.string().min(1).max(64).regex(/^[a-z0-9-]+$/).describe("Volume name (lowercase alphanumeric + hyphens)."),
2521
+ mount_path: z14.string().startsWith("/").max(500).describe("In-container mount path (absolute)."),
2522
+ size_gb: z14.number().int().min(1).max(100).optional().describe("Disk size in GB (default 1, max 100).")
2164
2523
  },
2165
2524
  handler: async (args, ctx) => {
2166
2525
  const teamId = await ctx.resolveTeamId();
@@ -2196,10 +2555,10 @@ defineTool({
2196
2555
  'Example: update_volume({ service_id: "svc_abc", volume_id: "vol_xyz", size_gb: 20 }) \u2192 { volume: { sizeGb: 20, \u2026 } }'
2197
2556
  ].join("\n"),
2198
2557
  input: {
2199
- service_id: z13.string().describe("Service publicId."),
2200
- volume_id: z13.string().describe("Volume publicId (e.g. vol_\u2026)."),
2201
- mount_path: z13.string().startsWith("/").max(500).optional().describe("New mount path."),
2202
- size_gb: z13.number().int().min(1).max(100).optional().describe("New size in GB.")
2558
+ service_id: z14.string().describe("Service publicId."),
2559
+ volume_id: z14.string().describe("Volume publicId (e.g. vol_\u2026)."),
2560
+ mount_path: z14.string().startsWith("/").max(500).optional().describe("New mount path."),
2561
+ size_gb: z14.number().int().min(1).max(100).optional().describe("New size in GB.")
2203
2562
  },
2204
2563
  handler: async (args, ctx) => {
2205
2564
  const teamId = await ctx.resolveTeamId();
@@ -2237,8 +2596,8 @@ defineTool({
2237
2596
  'Example: delete_volume({ service_id: "svc_abc", volume_id: "vol_xyz" }) \u2192 { ok: true }'
2238
2597
  ].join("\n"),
2239
2598
  input: {
2240
- service_id: z13.string().describe("Service publicId."),
2241
- volume_id: z13.string().describe("Volume publicId.")
2599
+ service_id: z14.string().describe("Service publicId."),
2600
+ volume_id: z14.string().describe("Volume publicId.")
2242
2601
  },
2243
2602
  handler: async (args, ctx) => {
2244
2603
  const teamId = await ctx.resolveTeamId();
@@ -2252,7 +2611,7 @@ defineTool({
2252
2611
 
2253
2612
  // src/server-factory.ts
2254
2613
  var PACKAGE_NAME = "hoststack";
2255
- var PACKAGE_VERSION = "0.1.2";
2614
+ var PACKAGE_VERSION = "0.6.0";
2256
2615
  function createMcpServer(options) {
2257
2616
  const baseUrl = (options.baseUrl ?? "https://hoststack.dev").replace(/\/$/, "");
2258
2617
  const hoststack = new HostStack({ apiKey: options.apiKey, baseUrl });