@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/README.md +18 -15
- package/dist/hoststack-mcp.js +474 -115
- package/dist/hoststack-mcp.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +474 -115
- package/dist/index.js.map +1 -1
- package/manifest.json +1 -1
- package/package.json +6 -5
package/dist/hoststack-mcp.js
CHANGED
|
@@ -46,6 +46,14 @@ var ApiClient = class {
|
|
|
46
46
|
});
|
|
47
47
|
return this.handle(res);
|
|
48
48
|
}
|
|
49
|
+
async put(path, body) {
|
|
50
|
+
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
51
|
+
method: "PUT",
|
|
52
|
+
headers: this.headers,
|
|
53
|
+
body: JSON.stringify(body)
|
|
54
|
+
});
|
|
55
|
+
return this.handle(res);
|
|
56
|
+
}
|
|
49
57
|
async delete(path) {
|
|
50
58
|
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
51
59
|
method: "DELETE",
|
|
@@ -902,8 +910,297 @@ defineTool({
|
|
|
902
910
|
}
|
|
903
911
|
});
|
|
904
912
|
|
|
905
|
-
// src/tools/
|
|
913
|
+
// src/tools/dns-records.ts
|
|
906
914
|
import { z as z7 } from "zod";
|
|
915
|
+
var DNS_RECORD_TYPES = ["A", "AAAA", "CNAME", "MX", "TXT", "NS", "SRV", "CAA", "ALIAS"];
|
|
916
|
+
async function resolveZonePublicId(api, teamId, input) {
|
|
917
|
+
if (input.zone_id) {
|
|
918
|
+
const { zones: zones2 } = await api.get(`/api/dns-zones/${teamId}`);
|
|
919
|
+
const match = zones2.find((z15) => z15.publicId === input.zone_id);
|
|
920
|
+
if (!match) {
|
|
921
|
+
throw new Error(`Zone ${input.zone_id} not found on this team.`);
|
|
922
|
+
}
|
|
923
|
+
return { publicId: match.publicId, domainName: match.domainName };
|
|
924
|
+
}
|
|
925
|
+
if (!input.domain) {
|
|
926
|
+
throw new Error("Provide either zone_id or domain.");
|
|
927
|
+
}
|
|
928
|
+
const { zones } = await api.get(`/api/dns-zones/${teamId}`);
|
|
929
|
+
const fqdn = input.domain.toLowerCase().replace(/\.$/, "");
|
|
930
|
+
const labels = fqdn.split(".");
|
|
931
|
+
for (let i = 0; i < labels.length - 1; i++) {
|
|
932
|
+
const candidate = labels.slice(i).join(".");
|
|
933
|
+
const match = zones.find((z15) => z15.domainName.toLowerCase() === candidate);
|
|
934
|
+
if (match && match.status !== "deleting") {
|
|
935
|
+
return { publicId: match.publicId, domainName: match.domainName };
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
throw new Error(
|
|
939
|
+
`No hosted zone matches ${input.domain}. Create the zone in the dashboard (Domains \u2192 DNS) first, then retry.`
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
defineTool({
|
|
943
|
+
name: "list_dns_zones",
|
|
944
|
+
category: "dns",
|
|
945
|
+
description: [
|
|
946
|
+
"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.",
|
|
947
|
+
"",
|
|
948
|
+
"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.",
|
|
949
|
+
"",
|
|
950
|
+
'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.',
|
|
951
|
+
"",
|
|
952
|
+
'Example: list_dns_zones() \u2192 { items: [{ publicId: "dnz_abc", domainName: "micci.dk", status: "active", nsRecords: ["ns1.hoststack.dev","ns2.hoststack.dev"] }] }'
|
|
953
|
+
].join("\n"),
|
|
954
|
+
input: {},
|
|
955
|
+
handler: async (_args, ctx) => {
|
|
956
|
+
const teamId = await ctx.resolveTeamId();
|
|
957
|
+
const response = await ctx.api.get(`/api/dns-zones/${teamId}`);
|
|
958
|
+
const items = Array.isArray(response.zones) ? response.zones.map(shape) : [];
|
|
959
|
+
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"}.`;
|
|
960
|
+
return respond({ summary, data: { items } });
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
defineTool({
|
|
964
|
+
name: "list_dns_records",
|
|
965
|
+
category: "dns",
|
|
966
|
+
description: [
|
|
967
|
+
"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.",
|
|
968
|
+
"",
|
|
969
|
+
"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.",
|
|
970
|
+
"",
|
|
971
|
+
'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).',
|
|
972
|
+
"",
|
|
973
|
+
'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.',
|
|
974
|
+
"",
|
|
975
|
+
'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 }] }'
|
|
976
|
+
].join("\n"),
|
|
977
|
+
input: {
|
|
978
|
+
zone_id: z7.string().optional().describe('Zone publicId (e.g. "dnz_abc").'),
|
|
979
|
+
domain: z7.string().optional().describe("Apex or subdomain \u2014 resolves to the longest-matching hosted zone.")
|
|
980
|
+
},
|
|
981
|
+
handler: async (args2, ctx) => {
|
|
982
|
+
const teamId = await ctx.resolveTeamId();
|
|
983
|
+
const zoneInput = {};
|
|
984
|
+
if (args2.zone_id !== void 0) zoneInput.zone_id = args2.zone_id;
|
|
985
|
+
if (args2.domain !== void 0) zoneInput.domain = args2.domain;
|
|
986
|
+
const zone = await resolveZonePublicId(ctx.api, teamId, zoneInput);
|
|
987
|
+
const response = await ctx.api.get(
|
|
988
|
+
`/api/dns-zones/${teamId}/${zone.publicId}/records`
|
|
989
|
+
);
|
|
990
|
+
const items = Array.isArray(response.records) ? response.records.map(shape) : [];
|
|
991
|
+
const summary = items.length === 0 ? `Zone ${zone.domainName} has no records yet.` : `Returned ${items.length} record${items.length === 1 ? "" : "s"} for ${zone.domainName}.`;
|
|
992
|
+
return respond({
|
|
993
|
+
summary,
|
|
994
|
+
data: { zone: { publicId: zone.publicId, domainName: zone.domainName }, items }
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
});
|
|
998
|
+
defineTool({
|
|
999
|
+
name: "get_dns_record",
|
|
1000
|
+
category: "dns",
|
|
1001
|
+
description: [
|
|
1002
|
+
'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).',
|
|
1003
|
+
"",
|
|
1004
|
+
"When to use: verify a record exists, inspect its current ttl/value, or check the sync status of an in-flight write.",
|
|
1005
|
+
"",
|
|
1006
|
+
"Inputs:",
|
|
1007
|
+
' - record_id: record publicId (e.g. "dnr_abc"). Returned by list_dns_records and create_dns_record.',
|
|
1008
|
+
"",
|
|
1009
|
+
"Returns: { record: Record }. Mirrors the shape returned by list_dns_records.",
|
|
1010
|
+
"",
|
|
1011
|
+
'Example: get_dns_record({ record_id: "dnr_abc" }) \u2192 { record: { type: "TXT", name: "@", value: "\u2026", status: "active", \u2026 } }'
|
|
1012
|
+
].join("\n"),
|
|
1013
|
+
input: {
|
|
1014
|
+
record_id: z7.string().describe("Record publicId.")
|
|
1015
|
+
},
|
|
1016
|
+
handler: async (args2, ctx) => {
|
|
1017
|
+
const teamId = await ctx.resolveTeamId();
|
|
1018
|
+
const { zones } = await ctx.api.get(`/api/dns-zones/${teamId}`);
|
|
1019
|
+
for (const zone of zones) {
|
|
1020
|
+
const { records } = await ctx.api.get(
|
|
1021
|
+
`/api/dns-zones/${teamId}/${zone.publicId}/records`
|
|
1022
|
+
);
|
|
1023
|
+
const match = records.find((r) => r.publicId === args2.record_id);
|
|
1024
|
+
if (match) {
|
|
1025
|
+
return respond({
|
|
1026
|
+
summary: `Record ${match.type} ${match.name} on ${zone.domainName}.`,
|
|
1027
|
+
data: {
|
|
1028
|
+
zone: { publicId: zone.publicId, domainName: zone.domainName },
|
|
1029
|
+
record: shape(match)
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
return respondError(`Record ${args2.record_id} not found on any zone owned by this team.`);
|
|
1035
|
+
}
|
|
1036
|
+
});
|
|
1037
|
+
defineTool({
|
|
1038
|
+
name: "create_dns_record",
|
|
1039
|
+
category: "dns",
|
|
1040
|
+
description: [
|
|
1041
|
+
'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.',
|
|
1042
|
+
"",
|
|
1043
|
+
"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.",
|
|
1044
|
+
"",
|
|
1045
|
+
"Provide EITHER zone_id (zone publicId) OR domain (resolves via longest-suffix match against hosted zones).",
|
|
1046
|
+
"",
|
|
1047
|
+
"Inputs:",
|
|
1048
|
+
' - type: "A" | "AAAA" | "CNAME" | "MX" | "TXT" | "NS" | "SRV" | "CAA" | "ALIAS".',
|
|
1049
|
+
' - name: "@" for the zone apex, or a subdomain label ("www", "api", "_acme-challenge").',
|
|
1050
|
+
" - content: the literal record value. For TXT pass the unquoted string \u2014 the PowerDNS layer handles quoting.",
|
|
1051
|
+
" - ttl (optional): 60\u201386400 seconds, default 3600.",
|
|
1052
|
+
" - priority (optional, required for MX and SRV): 0\u201365535.",
|
|
1053
|
+
"",
|
|
1054
|
+
"Returns: { record: Record } \u2014 see get_dns_record for the shape.",
|
|
1055
|
+
"",
|
|
1056
|
+
'Example: create_dns_record({ domain: "micci.dk", type: "TXT", name: "@", content: "google-site-verification=C0V0scK48g2\u2026", ttl: 300 })'
|
|
1057
|
+
].join("\n"),
|
|
1058
|
+
input: {
|
|
1059
|
+
zone_id: z7.string().optional().describe("Zone publicId."),
|
|
1060
|
+
domain: z7.string().optional().describe("Apex or subdomain to resolve a hosted zone."),
|
|
1061
|
+
type: z7.enum(DNS_RECORD_TYPES).describe("DNS record type."),
|
|
1062
|
+
name: z7.string().min(1).max(253).describe('"@" for apex, or label (no trailing dot).'),
|
|
1063
|
+
content: z7.string().min(1).max(2e3).describe("Record value; TXT values are unquoted."),
|
|
1064
|
+
ttl: z7.number().int().min(60).max(86400).optional().describe("TTL in seconds (default 3600)."),
|
|
1065
|
+
priority: z7.number().int().min(0).max(65535).optional().describe("Required for MX / SRV.")
|
|
1066
|
+
},
|
|
1067
|
+
handler: async (args2, ctx) => {
|
|
1068
|
+
if ((args2.type === "MX" || args2.type === "SRV") && args2.priority === void 0) {
|
|
1069
|
+
return respondError(`Priority is required for ${args2.type} records (0\u201365535).`);
|
|
1070
|
+
}
|
|
1071
|
+
const teamId = await ctx.resolveTeamId();
|
|
1072
|
+
const zoneInput = {};
|
|
1073
|
+
if (args2.zone_id !== void 0) zoneInput.zone_id = args2.zone_id;
|
|
1074
|
+
if (args2.domain !== void 0) zoneInput.domain = args2.domain;
|
|
1075
|
+
const zone = await resolveZonePublicId(ctx.api, teamId, zoneInput);
|
|
1076
|
+
const body = {
|
|
1077
|
+
type: args2.type,
|
|
1078
|
+
name: args2.name,
|
|
1079
|
+
value: args2.content
|
|
1080
|
+
};
|
|
1081
|
+
if (args2.ttl !== void 0) body.ttl = args2.ttl;
|
|
1082
|
+
if (args2.priority !== void 0) body.priority = args2.priority;
|
|
1083
|
+
const response = await ctx.api.post(
|
|
1084
|
+
`/api/dns-zones/${teamId}/${zone.publicId}/records`,
|
|
1085
|
+
body
|
|
1086
|
+
);
|
|
1087
|
+
return respond({
|
|
1088
|
+
summary: `Created ${args2.type} ${args2.name} on ${zone.domainName}.`,
|
|
1089
|
+
data: {
|
|
1090
|
+
zone: { publicId: zone.publicId, domainName: zone.domainName },
|
|
1091
|
+
record: shape(response.record)
|
|
1092
|
+
}
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
});
|
|
1096
|
+
defineTool({
|
|
1097
|
+
name: "update_dns_record",
|
|
1098
|
+
category: "dns",
|
|
1099
|
+
description: [
|
|
1100
|
+
"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.",
|
|
1101
|
+
"",
|
|
1102
|
+
"When to use: rotate a DKIM key, repoint an A record at a new IP, bump TTL ahead of a planned change.",
|
|
1103
|
+
"",
|
|
1104
|
+
"Inputs:",
|
|
1105
|
+
" - record_id: record publicId.",
|
|
1106
|
+
" - 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).",
|
|
1107
|
+
" - ttl (optional), priority (optional, required for MX / SRV).",
|
|
1108
|
+
"",
|
|
1109
|
+
"Returns: { record: Record } reflecting the new state.",
|
|
1110
|
+
"",
|
|
1111
|
+
'Example: update_dns_record({ record_id: "dnr_abc", type: "A", name: "api", content: "203.0.113.42", ttl: 300 })'
|
|
1112
|
+
].join("\n"),
|
|
1113
|
+
input: {
|
|
1114
|
+
record_id: z7.string().describe("Record publicId."),
|
|
1115
|
+
type: z7.enum(DNS_RECORD_TYPES).describe("DNS record type."),
|
|
1116
|
+
name: z7.string().min(1).max(253).describe('"@" for apex, or label.'),
|
|
1117
|
+
content: z7.string().min(1).max(2e3).describe("Record value; TXT unquoted."),
|
|
1118
|
+
ttl: z7.number().int().min(60).max(86400).optional().describe("TTL in seconds (default 3600)."),
|
|
1119
|
+
priority: z7.number().int().min(0).max(65535).optional().describe("Required for MX / SRV.")
|
|
1120
|
+
},
|
|
1121
|
+
handler: async (args2, ctx) => {
|
|
1122
|
+
if ((args2.type === "MX" || args2.type === "SRV") && args2.priority === void 0) {
|
|
1123
|
+
return respondError(`Priority is required for ${args2.type} records (0\u201365535).`);
|
|
1124
|
+
}
|
|
1125
|
+
const teamId = await ctx.resolveTeamId();
|
|
1126
|
+
const target = await findRecordZone(ctx.api, teamId, args2.record_id);
|
|
1127
|
+
if (!target) {
|
|
1128
|
+
return respondError(
|
|
1129
|
+
`Record ${args2.record_id} not found on any zone owned by this team.`
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
const body = {
|
|
1133
|
+
type: args2.type,
|
|
1134
|
+
name: args2.name,
|
|
1135
|
+
value: args2.content
|
|
1136
|
+
};
|
|
1137
|
+
if (args2.ttl !== void 0) body.ttl = args2.ttl;
|
|
1138
|
+
if (args2.priority !== void 0) body.priority = args2.priority;
|
|
1139
|
+
const response = await ctx.api.put(
|
|
1140
|
+
`/api/dns-zones/${teamId}/${target.zone.publicId}/records/${args2.record_id}`,
|
|
1141
|
+
body
|
|
1142
|
+
);
|
|
1143
|
+
return respond({
|
|
1144
|
+
summary: `Updated ${args2.type} ${args2.name} on ${target.zone.domainName}.`,
|
|
1145
|
+
data: {
|
|
1146
|
+
zone: {
|
|
1147
|
+
publicId: target.zone.publicId,
|
|
1148
|
+
domainName: target.zone.domainName
|
|
1149
|
+
},
|
|
1150
|
+
record: shape(response.record)
|
|
1151
|
+
}
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
});
|
|
1155
|
+
defineTool({
|
|
1156
|
+
name: "delete_dns_record",
|
|
1157
|
+
category: "dns",
|
|
1158
|
+
description: [
|
|
1159
|
+
"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.",
|
|
1160
|
+
"",
|
|
1161
|
+
"When to use: retire a TXT verification record once the challenge has been confirmed; drop an A record for a decommissioned subdomain.",
|
|
1162
|
+
"",
|
|
1163
|
+
"Inputs:",
|
|
1164
|
+
" - record_id: record publicId.",
|
|
1165
|
+
"",
|
|
1166
|
+
"Returns: { ok: true }.",
|
|
1167
|
+
"",
|
|
1168
|
+
'Example: delete_dns_record({ record_id: "dnr_abc" }) \u2192 { ok: true }'
|
|
1169
|
+
].join("\n"),
|
|
1170
|
+
input: {
|
|
1171
|
+
record_id: z7.string().describe("Record publicId.")
|
|
1172
|
+
},
|
|
1173
|
+
handler: async (args2, ctx) => {
|
|
1174
|
+
const teamId = await ctx.resolveTeamId();
|
|
1175
|
+
const target = await findRecordZone(ctx.api, teamId, args2.record_id);
|
|
1176
|
+
if (!target) {
|
|
1177
|
+
return respondError(
|
|
1178
|
+
`Record ${args2.record_id} not found on any zone owned by this team.`
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
await ctx.api.delete(
|
|
1182
|
+
`/api/dns-zones/${teamId}/${target.zone.publicId}/records/${args2.record_id}`
|
|
1183
|
+
);
|
|
1184
|
+
return respond({
|
|
1185
|
+
summary: `Deleted record ${args2.record_id} from ${target.zone.domainName}.`,
|
|
1186
|
+
data: { ok: true }
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
});
|
|
1190
|
+
async function findRecordZone(api, teamId, recordPublicId) {
|
|
1191
|
+
const { zones } = await api.get(`/api/dns-zones/${teamId}`);
|
|
1192
|
+
for (const zone of zones) {
|
|
1193
|
+
const { records } = await api.get(
|
|
1194
|
+
`/api/dns-zones/${teamId}/${zone.publicId}/records`
|
|
1195
|
+
);
|
|
1196
|
+
const match = records.find((r) => r.publicId === recordPublicId);
|
|
1197
|
+
if (match) return { zone, record: match };
|
|
1198
|
+
}
|
|
1199
|
+
return null;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// src/tools/domains.ts
|
|
1203
|
+
import { z as z8 } from "zod";
|
|
907
1204
|
defineTool({
|
|
908
1205
|
name: "list_domains",
|
|
909
1206
|
category: "domains",
|
|
@@ -942,8 +1239,8 @@ defineTool({
|
|
|
942
1239
|
'Example: add_domain({ hostname: "api.example.com", service_id: "svc_abc" }) \u2192 { domain: { hostname: "api.example.com", dnsTargets: [...], verified: false, \u2026 } }'
|
|
943
1240
|
].join("\n"),
|
|
944
1241
|
input: {
|
|
945
|
-
hostname:
|
|
946
|
-
service_id:
|
|
1242
|
+
hostname: z8.string().min(3).max(253).describe("Fully-qualified hostname (e.g. api.example.com)."),
|
|
1243
|
+
service_id: z8.string().optional().describe("Optional service publicId to bind to.")
|
|
947
1244
|
},
|
|
948
1245
|
handler: async (args2, ctx) => {
|
|
949
1246
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -973,7 +1270,7 @@ defineTool({
|
|
|
973
1270
|
'Example: verify_domain({ domain_id: "dom_xyz" }) \u2192 { ok: true }'
|
|
974
1271
|
].join("\n"),
|
|
975
1272
|
input: {
|
|
976
|
-
domain_id:
|
|
1273
|
+
domain_id: z8.string().describe("Domain publicId.")
|
|
977
1274
|
},
|
|
978
1275
|
handler: async (args2, ctx) => {
|
|
979
1276
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -1000,7 +1297,7 @@ defineTool({
|
|
|
1000
1297
|
'Example: remove_domain({ domain_id: "dom_xyz" }) \u2192 { ok: true }'
|
|
1001
1298
|
].join("\n"),
|
|
1002
1299
|
input: {
|
|
1003
|
-
domain_id:
|
|
1300
|
+
domain_id: z8.string().describe("Domain publicId.")
|
|
1004
1301
|
},
|
|
1005
1302
|
handler: async (args2, ctx) => {
|
|
1006
1303
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -1010,7 +1307,7 @@ defineTool({
|
|
|
1010
1307
|
});
|
|
1011
1308
|
|
|
1012
1309
|
// src/tools/env-vars.ts
|
|
1013
|
-
import { z as
|
|
1310
|
+
import { z as z9 } from "zod";
|
|
1014
1311
|
defineTool({
|
|
1015
1312
|
name: "list_env_vars",
|
|
1016
1313
|
category: "env-vars",
|
|
@@ -1027,7 +1324,7 @@ defineTool({
|
|
|
1027
1324
|
'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 }] }'
|
|
1028
1325
|
].join("\n"),
|
|
1029
1326
|
input: {
|
|
1030
|
-
service_id:
|
|
1327
|
+
service_id: z9.string().describe("Service publicId.")
|
|
1031
1328
|
},
|
|
1032
1329
|
handler: async (args2, ctx) => {
|
|
1033
1330
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -1056,10 +1353,10 @@ defineTool({
|
|
|
1056
1353
|
'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" }'
|
|
1057
1354
|
].join("\n"),
|
|
1058
1355
|
input: {
|
|
1059
|
-
service_id:
|
|
1060
|
-
key:
|
|
1061
|
-
value:
|
|
1062
|
-
is_secret:
|
|
1356
|
+
service_id: z9.string().describe("Service publicId."),
|
|
1357
|
+
key: z9.string().min(1).max(128).describe("Env-var key."),
|
|
1358
|
+
value: z9.string().describe("New value."),
|
|
1359
|
+
is_secret: z9.boolean().optional().describe("Mark as secret (encrypted, masked on read). Default true.")
|
|
1063
1360
|
},
|
|
1064
1361
|
handler: async (args2, ctx) => {
|
|
1065
1362
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -1111,8 +1408,8 @@ defineTool({
|
|
|
1111
1408
|
'Example: delete_env_var({ service_id: "svc_abc", key: "OLD_FLAG" }) \u2192 { ok: true }'
|
|
1112
1409
|
].join("\n"),
|
|
1113
1410
|
input: {
|
|
1114
|
-
service_id:
|
|
1115
|
-
key:
|
|
1411
|
+
service_id: z9.string().describe("Service publicId."),
|
|
1412
|
+
key: z9.string().min(1).max(128).describe("Env-var key to delete.")
|
|
1116
1413
|
},
|
|
1117
1414
|
handler: async (args2, ctx) => {
|
|
1118
1415
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -1148,12 +1445,12 @@ defineTool({
|
|
|
1148
1445
|
'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 }'
|
|
1149
1446
|
].join("\n"),
|
|
1150
1447
|
input: {
|
|
1151
|
-
service_id:
|
|
1152
|
-
env_vars:
|
|
1153
|
-
|
|
1154
|
-
key:
|
|
1155
|
-
value:
|
|
1156
|
-
is_secret:
|
|
1448
|
+
service_id: z9.string().describe("Service publicId."),
|
|
1449
|
+
env_vars: z9.array(
|
|
1450
|
+
z9.object({
|
|
1451
|
+
key: z9.string().min(1).max(128),
|
|
1452
|
+
value: z9.string(),
|
|
1453
|
+
is_secret: z9.boolean().optional()
|
|
1157
1454
|
})
|
|
1158
1455
|
).max(500).describe("Array of env-var rows. Hard cap 500.")
|
|
1159
1456
|
},
|
|
@@ -1178,7 +1475,7 @@ defineTool({
|
|
|
1178
1475
|
});
|
|
1179
1476
|
|
|
1180
1477
|
// src/tools/environments.ts
|
|
1181
|
-
import { z as
|
|
1478
|
+
import { z as z10 } from "zod";
|
|
1182
1479
|
defineTool({
|
|
1183
1480
|
name: "list_environments",
|
|
1184
1481
|
category: "environments",
|
|
@@ -1195,7 +1492,7 @@ defineTool({
|
|
|
1195
1492
|
'Example: list_environments({ project_id: "prj_abc" }) \u2192 { items: [{ name: "Production", type: "production", isDefault: true }, { name: "Staging", type: "staging" }] }'
|
|
1196
1493
|
].join("\n"),
|
|
1197
1494
|
input: {
|
|
1198
|
-
project_id:
|
|
1495
|
+
project_id: z10.string().describe("Project publicId.")
|
|
1199
1496
|
},
|
|
1200
1497
|
handler: async (args2, ctx) => {
|
|
1201
1498
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -1224,10 +1521,10 @@ defineTool({
|
|
|
1224
1521
|
'Example: create_environment({ project_id: "prj_abc", name: "Staging", type: "staging" }) \u2192 { environment: { id: 7, publicId: "environment_\u2026", name: "Staging", type: "staging" } }'
|
|
1225
1522
|
].join("\n"),
|
|
1226
1523
|
input: {
|
|
1227
|
-
project_id:
|
|
1228
|
-
name:
|
|
1229
|
-
type:
|
|
1230
|
-
is_protected:
|
|
1524
|
+
project_id: z10.string().describe("Project publicId."),
|
|
1525
|
+
name: z10.string().min(1).max(64).describe("Environment name (1\u201364 chars)."),
|
|
1526
|
+
type: z10.enum(["production", "staging", "development", "preview"]).describe("Environment type \u2014 drives the hostname suffix."),
|
|
1527
|
+
is_protected: z10.boolean().optional().describe("Require admin role for destructive actions. Default false.")
|
|
1231
1528
|
},
|
|
1232
1529
|
handler: async (args2, ctx) => {
|
|
1233
1530
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -1261,8 +1558,8 @@ defineTool({
|
|
|
1261
1558
|
'Example: delete_environment({ project_id: "prj_abc", environment_id: "environment_xyz" }) \u2192 { success: true }'
|
|
1262
1559
|
].join("\n"),
|
|
1263
1560
|
input: {
|
|
1264
|
-
project_id:
|
|
1265
|
-
environment_id:
|
|
1561
|
+
project_id: z10.string().describe("Project publicId."),
|
|
1562
|
+
environment_id: z10.union([z10.string(), z10.number()]).describe("Environment publicId or numeric id.")
|
|
1266
1563
|
},
|
|
1267
1564
|
handler: async (args2, ctx) => {
|
|
1268
1565
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -1293,9 +1590,9 @@ defineTool({
|
|
|
1293
1590
|
'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" } }'
|
|
1294
1591
|
].join("\n"),
|
|
1295
1592
|
input: {
|
|
1296
|
-
service_id:
|
|
1297
|
-
deploy_id:
|
|
1298
|
-
target_environment_id:
|
|
1593
|
+
service_id: z10.string().describe("Source service publicId."),
|
|
1594
|
+
deploy_id: z10.string().describe("Source deploy publicId (must have a built image)."),
|
|
1595
|
+
target_environment_id: z10.union([z10.string(), z10.number()]).describe("Target environment publicId or numeric id.")
|
|
1299
1596
|
},
|
|
1300
1597
|
handler: async (args2, ctx) => {
|
|
1301
1598
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -1344,7 +1641,7 @@ defineTool({
|
|
|
1344
1641
|
});
|
|
1345
1642
|
|
|
1346
1643
|
// src/tools/notifications.ts
|
|
1347
|
-
import { z as
|
|
1644
|
+
import { z as z11 } from "zod";
|
|
1348
1645
|
var NOTIFICATION_EVENTS = [
|
|
1349
1646
|
"deploy.started",
|
|
1350
1647
|
"deploy.succeeded",
|
|
@@ -1403,10 +1700,10 @@ defineTool({
|
|
|
1403
1700
|
"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'] })"
|
|
1404
1701
|
].join("\n"),
|
|
1405
1702
|
input: {
|
|
1406
|
-
type:
|
|
1407
|
-
name:
|
|
1408
|
-
webhook_url:
|
|
1409
|
-
events:
|
|
1703
|
+
type: z11.enum(["slack", "discord", "email"]).describe("Channel type."),
|
|
1704
|
+
name: z11.string().min(1).max(128).describe("Human-readable label."),
|
|
1705
|
+
webhook_url: z11.string().max(500).describe("Slack/Discord webhook URL or email address (when type=email)."),
|
|
1706
|
+
events: z11.array(z11.enum(NOTIFICATION_EVENTS)).describe("List of events the channel subscribes to. Empty list = subscribe to nothing.")
|
|
1410
1707
|
},
|
|
1411
1708
|
handler: async (args2, ctx) => {
|
|
1412
1709
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -1442,10 +1739,10 @@ defineTool({
|
|
|
1442
1739
|
"Example: update_notification_channel({ channel_id: 3, events: ['deploy.failed', 'service.restart_failed', 'git.auth_failed'] })"
|
|
1443
1740
|
].join("\n"),
|
|
1444
1741
|
input: {
|
|
1445
|
-
channel_id:
|
|
1446
|
-
name:
|
|
1447
|
-
active:
|
|
1448
|
-
events:
|
|
1742
|
+
channel_id: z11.number().int().positive().describe("Numeric channel id from list_notification_channels."),
|
|
1743
|
+
name: z11.string().min(1).max(128).optional().describe("New label."),
|
|
1744
|
+
active: z11.boolean().optional().describe("false silences without deleting."),
|
|
1745
|
+
events: z11.array(z11.enum(NOTIFICATION_EVENTS)).optional().describe("Replaces the full subscription list.")
|
|
1449
1746
|
},
|
|
1450
1747
|
handler: async (args2, ctx) => {
|
|
1451
1748
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -1484,7 +1781,7 @@ defineTool({
|
|
|
1484
1781
|
"Example: delete_notification_channel({ channel_id: 3 }) \u2192 { ok: true }"
|
|
1485
1782
|
].join("\n"),
|
|
1486
1783
|
input: {
|
|
1487
|
-
channel_id:
|
|
1784
|
+
channel_id: z11.number().int().positive().describe("Numeric channel id.")
|
|
1488
1785
|
},
|
|
1489
1786
|
handler: async (args2, ctx) => {
|
|
1490
1787
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -1511,7 +1808,7 @@ defineTool({
|
|
|
1511
1808
|
"Example: test_notification_channel({ channel_id: 3 }) \u2192 { success: true }"
|
|
1512
1809
|
].join("\n"),
|
|
1513
1810
|
input: {
|
|
1514
|
-
channel_id:
|
|
1811
|
+
channel_id: z11.number().int().positive().describe("Numeric channel id.")
|
|
1515
1812
|
},
|
|
1516
1813
|
handler: async (args2, ctx) => {
|
|
1517
1814
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -1526,7 +1823,7 @@ defineTool({
|
|
|
1526
1823
|
});
|
|
1527
1824
|
|
|
1528
1825
|
// src/tools/projects.ts
|
|
1529
|
-
import { z as
|
|
1826
|
+
import { z as z12 } from "zod";
|
|
1530
1827
|
defineTool({
|
|
1531
1828
|
name: "list_projects",
|
|
1532
1829
|
category: "projects",
|
|
@@ -1566,9 +1863,9 @@ defineTool({
|
|
|
1566
1863
|
'Example: create_project({ name: "billing-api", description: "Stripe webhooks", region: "fsn1" }) \u2192 { project: { id: 12, publicId: "prj_\u2026", \u2026 } }'
|
|
1567
1864
|
].join("\n"),
|
|
1568
1865
|
input: {
|
|
1569
|
-
name:
|
|
1570
|
-
description:
|
|
1571
|
-
region:
|
|
1866
|
+
name: z12.string().min(1).max(60).describe("Project name (1\u201360 chars)."),
|
|
1867
|
+
description: z12.string().max(500).optional().describe("Short description (\u2264500 chars)."),
|
|
1868
|
+
region: z12.enum(["fsn1", "nbg1", "hel1"]).optional().describe("Hetzner region: fsn1 | nbg1 | hel1.")
|
|
1572
1869
|
},
|
|
1573
1870
|
handler: async (args2, ctx) => {
|
|
1574
1871
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -1599,9 +1896,9 @@ defineTool({
|
|
|
1599
1896
|
'Example: update_project({ project_id: "prj_abc", name: "billing-prod" }) \u2192 { project: { name: "billing-prod", \u2026 } }'
|
|
1600
1897
|
].join("\n"),
|
|
1601
1898
|
input: {
|
|
1602
|
-
project_id:
|
|
1603
|
-
name:
|
|
1604
|
-
description:
|
|
1899
|
+
project_id: z12.string().describe("Project publicId."),
|
|
1900
|
+
name: z12.string().min(1).max(60).optional().describe("New name (1\u201360 chars)."),
|
|
1901
|
+
description: z12.string().max(500).optional().describe("New description (\u2264500 chars).")
|
|
1605
1902
|
},
|
|
1606
1903
|
handler: async (args2, ctx) => {
|
|
1607
1904
|
if (args2.name === void 0 && args2.description === void 0) {
|
|
@@ -1635,7 +1932,7 @@ defineTool({
|
|
|
1635
1932
|
'Example: get_project({ project_id: "prj_abc" }) \u2192 { project: { id: 12, name: "billing", \u2026 } }'
|
|
1636
1933
|
].join("\n"),
|
|
1637
1934
|
input: {
|
|
1638
|
-
project_id:
|
|
1935
|
+
project_id: z12.string().describe("Project publicId (e.g. prj_abc123).")
|
|
1639
1936
|
},
|
|
1640
1937
|
handler: async (args2, ctx) => {
|
|
1641
1938
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -1647,7 +1944,7 @@ defineTool({
|
|
|
1647
1944
|
});
|
|
1648
1945
|
|
|
1649
1946
|
// src/tools/services.ts
|
|
1650
|
-
import { z as
|
|
1947
|
+
import { z as z13 } from "zod";
|
|
1651
1948
|
defineTool({
|
|
1652
1949
|
name: "list_services",
|
|
1653
1950
|
category: "services",
|
|
@@ -1667,10 +1964,10 @@ defineTool({
|
|
|
1667
1964
|
'Example: list_services({ status: "failed" }) \u2192 only services that need attention.'
|
|
1668
1965
|
].join("\n"),
|
|
1669
1966
|
input: {
|
|
1670
|
-
project_id:
|
|
1671
|
-
environment_id:
|
|
1672
|
-
status:
|
|
1673
|
-
type:
|
|
1967
|
+
project_id: z13.union([z13.number().int().positive(), z13.string()]).optional().describe("Project filter \u2014 numeric id or publicId."),
|
|
1968
|
+
environment_id: z13.union([z13.number().int().positive(), z13.string()]).optional().describe("Environment filter \u2014 numeric id or publicId."),
|
|
1969
|
+
status: z13.enum(["active", "deploying", "suspended", "failed", "not_deployed"]).optional().describe("Filter by current runtime status."),
|
|
1970
|
+
type: z13.enum(["web_service", "private_service", "worker", "cron_job", "static_site"]).optional().describe("Filter by service type.")
|
|
1674
1971
|
},
|
|
1675
1972
|
handler: async (args2, ctx) => {
|
|
1676
1973
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -1707,24 +2004,30 @@ defineTool({
|
|
|
1707
2004
|
name: "get_service",
|
|
1708
2005
|
category: "services",
|
|
1709
2006
|
description: [
|
|
1710
|
-
"Fetch a single service by ID
|
|
2007
|
+
"Fetch a single service by ID with its current status AND its service_config row (resources, health-check tuning, scaling, restart policy).",
|
|
1711
2008
|
"",
|
|
1712
|
-
"When to use: drilling into a service after list_services, checking deploy/runtime status,
|
|
2009
|
+
"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.",
|
|
1713
2010
|
"",
|
|
1714
2011
|
"Inputs:",
|
|
1715
2012
|
' - service_id: publicId of the service (e.g. "svc_abc123").',
|
|
1716
2013
|
"",
|
|
1717
|
-
"Returns: { service: Service } \u2014 type,
|
|
2014
|
+
"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.",
|
|
1718
2015
|
"",
|
|
1719
|
-
'Example: get_service({ service_id: "svc_abc" }) \u2192 { service: { type: "web", status: "running", \u2026 } }'
|
|
2016
|
+
'Example: get_service({ service_id: "svc_abc" }) \u2192 { service: { type: "web", status: "running", \u2026 }, config: { healthCheckGracePeriodSec: 120, \u2026 } }'
|
|
1720
2017
|
].join("\n"),
|
|
1721
2018
|
input: {
|
|
1722
|
-
service_id:
|
|
2019
|
+
service_id: z13.string().describe("Service publicId (e.g. svc_abc123).")
|
|
1723
2020
|
},
|
|
1724
2021
|
handler: async (args2, ctx) => {
|
|
1725
2022
|
const teamId = await ctx.resolveTeamId();
|
|
1726
|
-
const
|
|
1727
|
-
|
|
2023
|
+
const [serviceResponse, configResponse] = await Promise.all([
|
|
2024
|
+
ctx.hoststack.services.get(teamId, args2.service_id),
|
|
2025
|
+
ctx.hoststack.services.getConfig(teamId, args2.service_id)
|
|
2026
|
+
]);
|
|
2027
|
+
const data = {
|
|
2028
|
+
service: shapeService(serviceResponse.service),
|
|
2029
|
+
config: shape(configResponse.config)
|
|
2030
|
+
};
|
|
1728
2031
|
const status = data.service && "status" in data.service ? data.service.status : "unknown";
|
|
1729
2032
|
return respond({ summary: `Service ${args2.service_id} is ${status}.`, data });
|
|
1730
2033
|
}
|
|
@@ -1745,7 +2048,7 @@ defineTool({
|
|
|
1745
2048
|
'Example: get_service_metrics({ service_id: "svc_abc" }) \u2192 { metrics: { cpu: 0.42, memory: 0.71, \u2026 } }'
|
|
1746
2049
|
].join("\n"),
|
|
1747
2050
|
input: {
|
|
1748
|
-
service_id:
|
|
2051
|
+
service_id: z13.string().describe("Service publicId.")
|
|
1749
2052
|
},
|
|
1750
2053
|
handler: async (args2, ctx) => {
|
|
1751
2054
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -1774,9 +2077,9 @@ defineTool({
|
|
|
1774
2077
|
'Example: get_service_metrics_history({ service_id: "svc_abc", from: "-1h" }) \u2192 60-ish points for the last hour.'
|
|
1775
2078
|
].join("\n"),
|
|
1776
2079
|
input: {
|
|
1777
|
-
service_id:
|
|
1778
|
-
from:
|
|
1779
|
-
to:
|
|
2080
|
+
service_id: z13.string().describe("Service publicId."),
|
|
2081
|
+
from: z13.string().optional().describe('ISO-8601 lower bound or relative offset (e.g. "-1h", "-2d").'),
|
|
2082
|
+
to: z13.string().optional().describe("ISO-8601 upper bound; defaults to now.")
|
|
1780
2083
|
},
|
|
1781
2084
|
handler: async (args2, ctx) => {
|
|
1782
2085
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -1820,8 +2123,8 @@ defineTool({
|
|
|
1820
2123
|
'Example: update_service({ service_id: "svc_abc", name: "api-prod" }) \u2192 { service: { name: "api-prod", \u2026 } }'
|
|
1821
2124
|
].join("\n"),
|
|
1822
2125
|
input: {
|
|
1823
|
-
service_id:
|
|
1824
|
-
name:
|
|
2126
|
+
service_id: z13.string().describe("Service publicId."),
|
|
2127
|
+
name: z13.string().min(1).max(60).describe("New service name (1\u201360 chars).")
|
|
1825
2128
|
},
|
|
1826
2129
|
handler: async (args2, ctx) => {
|
|
1827
2130
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -1836,9 +2139,9 @@ defineTool({
|
|
|
1836
2139
|
name: "update_service_config",
|
|
1837
2140
|
category: "services",
|
|
1838
2141
|
description: [
|
|
1839
|
-
"Update build/runtime configuration for a service
|
|
2142
|
+
"Update build/runtime configuration for a service. All fields optional \u2014 pass only what you want to change.",
|
|
1840
2143
|
"",
|
|
1841
|
-
"When to use: the user wants to tweak how a service builds or
|
|
2144
|
+
"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.",
|
|
1842
2145
|
"",
|
|
1843
2146
|
"Inputs:",
|
|
1844
2147
|
" - service_id: publicId of the service.",
|
|
@@ -1847,27 +2150,59 @@ defineTool({
|
|
|
1847
2150
|
" - root_directory (optional): build context root inside the repo.",
|
|
1848
2151
|
" - dockerfile_path (optional): path to Dockerfile relative to root_directory. Pass null to clear.",
|
|
1849
2152
|
" - auto_deploy (optional): boolean \u2014 auto-deploy on git push.",
|
|
1850
|
-
|
|
2153
|
+
' - health_check_path (optional): HTTP path the platform GETs to verify liveness (e.g. "/health"). Pass null for TCP-only check.',
|
|
2154
|
+
" - health_check_enabled (optional): boolean \u2014 toggle health checking on/off.",
|
|
2155
|
+
" - health_check_interval (optional): integer 5\u2013300 seconds \u2014 how often the check runs.",
|
|
2156
|
+
" - health_check_timeout (optional): integer 1\u201360 seconds \u2014 single-attempt timeout.",
|
|
2157
|
+
' - 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).',
|
|
2158
|
+
" - memory_mb (optional): integer 128\u201316384 \u2014 container memory cap.",
|
|
2159
|
+
" - cpu_shares (optional): integer 128\u20134096 \u2014 relative CPU weight.",
|
|
2160
|
+
" - disk_size_gb (optional): integer 1\u2013100 \u2014 ephemeral disk cap.",
|
|
2161
|
+
" - port (optional): integer 1\u201365535 \u2014 container port the platform forwards traffic to.",
|
|
2162
|
+
' - protocol (optional): "http" | "tcp".',
|
|
2163
|
+
' - restart_policy (optional): "always" | "on-failure" | "no".',
|
|
2164
|
+
" - pre_deploy_command (optional): shell command run before the new release accepts traffic (typical use: migrations).",
|
|
2165
|
+
" - instance_count (optional): integer 1\u201350 \u2014 pin both min and max instances to this value.",
|
|
2166
|
+
" - min_instances, max_instances (optional): integers \u2014 autoscale bounds. Use instead of instance_count when you want a range.",
|
|
2167
|
+
" - scale_cpu_threshold, scale_memory_threshold (optional): integer 10\u2013100 \u2014 autoscale trigger percentage.",
|
|
1851
2168
|
' - 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.',
|
|
1852
2169
|
"",
|
|
1853
2170
|
"Returns: { service?: Service, config?: ServiceConfig } \u2014 whichever rows were touched.",
|
|
1854
2171
|
"",
|
|
1855
|
-
'Example: update_service_config({ service_id: "svc_abc",
|
|
2172
|
+
'Example: update_service_config({ service_id: "svc_abc", health_check_grace_period_sec: 180 }) \u2192 { config: { healthCheckGracePeriodSec: 180, \u2026 } }'
|
|
1856
2173
|
].join("\n"),
|
|
1857
2174
|
input: {
|
|
1858
|
-
service_id:
|
|
1859
|
-
install_command:
|
|
1860
|
-
build_command:
|
|
1861
|
-
start_command:
|
|
1862
|
-
branch:
|
|
1863
|
-
root_directory:
|
|
1864
|
-
dockerfile_path:
|
|
1865
|
-
auto_deploy:
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
2175
|
+
service_id: z13.string().describe("Service publicId."),
|
|
2176
|
+
install_command: z13.string().nullable().optional().describe("Install shell command. Null clears."),
|
|
2177
|
+
build_command: z13.string().nullable().optional().describe("Build shell command. Null clears."),
|
|
2178
|
+
start_command: z13.string().nullable().optional().describe("Start shell command. Null clears."),
|
|
2179
|
+
branch: z13.string().optional().describe("Git branch to track."),
|
|
2180
|
+
root_directory: z13.string().optional().describe("Build context root."),
|
|
2181
|
+
dockerfile_path: z13.string().nullable().optional().describe("Path to Dockerfile relative to root. Null clears."),
|
|
2182
|
+
auto_deploy: z13.boolean().optional().describe("Auto-deploy on push."),
|
|
2183
|
+
health_check_path: z13.string().nullable().optional().describe('HTTP health-check path (e.g. "/health"). Null = TCP-only check.'),
|
|
2184
|
+
health_check_enabled: z13.boolean().optional().describe("Toggle health checking on/off."),
|
|
2185
|
+
health_check_interval: z13.number().int().min(5).max(300).optional().describe("How often the check runs, in seconds (5\u2013300)."),
|
|
2186
|
+
health_check_timeout: z13.number().int().min(1).max(60).optional().describe("Single-attempt timeout in seconds (1\u201360)."),
|
|
2187
|
+
health_check_grace_period_sec: z13.number().int().min(1).max(1800).optional().describe(
|
|
2188
|
+
"Startup grace period in seconds (1\u20131800). Raise this if the app needs more time to boot before health checks start counting failures."
|
|
2189
|
+
),
|
|
2190
|
+
memory_mb: z13.number().int().min(128).max(16384).optional().describe("Container memory cap in MB (128\u201316384)."),
|
|
2191
|
+
cpu_shares: z13.number().int().min(128).max(4096).optional().describe("Relative CPU weight (128\u20134096)."),
|
|
2192
|
+
disk_size_gb: z13.number().int().min(1).max(100).optional().describe("Ephemeral disk size in GB (1\u2013100)."),
|
|
2193
|
+
port: z13.number().int().min(1).max(65535).optional().describe("Container port the platform forwards traffic to."),
|
|
2194
|
+
protocol: z13.enum(["http", "tcp"]).optional().describe("Traffic protocol."),
|
|
2195
|
+
restart_policy: z13.enum(["always", "on-failure", "no"]).optional().describe("Docker restart policy."),
|
|
2196
|
+
pre_deploy_command: z13.string().optional().describe("Shell command run before the new release accepts traffic."),
|
|
2197
|
+
instance_count: z13.number().int().positive().max(50).optional().describe("Pin min and max instances to this value (1\u201350)."),
|
|
2198
|
+
min_instances: z13.number().int().min(0).max(50).optional().describe("Autoscale lower bound. Use with max_instances for a range."),
|
|
2199
|
+
max_instances: z13.number().int().min(1).max(50).optional().describe("Autoscale upper bound. Use with min_instances for a range."),
|
|
2200
|
+
scale_cpu_threshold: z13.number().int().min(10).max(100).optional().describe("Autoscale CPU trigger percentage (10\u2013100)."),
|
|
2201
|
+
scale_memory_threshold: z13.number().int().min(10).max(100).optional().describe("Autoscale memory trigger percentage (10\u2013100)."),
|
|
2202
|
+
log_filter_rules: z13.array(
|
|
2203
|
+
z13.object({
|
|
2204
|
+
pattern: z13.string().min(1).max(200),
|
|
2205
|
+
action: z13.enum(["drop", "downgrade"])
|
|
1871
2206
|
})
|
|
1872
2207
|
).max(50).optional().describe(
|
|
1873
2208
|
"Runtime-log filter rules. Empty array [] clears all rules. Each pattern is case-insensitive substring match against the message."
|
|
@@ -1885,11 +2220,35 @@ defineTool({
|
|
|
1885
2220
|
if (args2.branch !== void 0) serviceUpdate["branch"] = args2.branch;
|
|
1886
2221
|
if (args2.root_directory !== void 0) serviceUpdate["rootDirectory"] = args2.root_directory;
|
|
1887
2222
|
if (args2.auto_deploy !== void 0) serviceUpdate["autoDeploy"] = args2.auto_deploy;
|
|
2223
|
+
if (args2.health_check_path !== void 0)
|
|
2224
|
+
serviceUpdate["healthCheckPath"] = args2.health_check_path;
|
|
1888
2225
|
const configUpdate = {};
|
|
2226
|
+
if (args2.health_check_enabled !== void 0)
|
|
2227
|
+
configUpdate["healthCheckEnabled"] = args2.health_check_enabled;
|
|
2228
|
+
if (args2.health_check_interval !== void 0)
|
|
2229
|
+
configUpdate["healthCheckInterval"] = args2.health_check_interval;
|
|
2230
|
+
if (args2.health_check_timeout !== void 0)
|
|
2231
|
+
configUpdate["healthCheckTimeout"] = args2.health_check_timeout;
|
|
2232
|
+
if (args2.health_check_grace_period_sec !== void 0)
|
|
2233
|
+
configUpdate["healthCheckGracePeriodSec"] = args2.health_check_grace_period_sec;
|
|
2234
|
+
if (args2.memory_mb !== void 0) configUpdate["memoryMb"] = args2.memory_mb;
|
|
2235
|
+
if (args2.cpu_shares !== void 0) configUpdate["cpuShares"] = args2.cpu_shares;
|
|
2236
|
+
if (args2.disk_size_gb !== void 0) configUpdate["diskSizeGb"] = args2.disk_size_gb;
|
|
2237
|
+
if (args2.port !== void 0) configUpdate["port"] = args2.port;
|
|
2238
|
+
if (args2.protocol !== void 0) configUpdate["protocol"] = args2.protocol;
|
|
2239
|
+
if (args2.restart_policy !== void 0) configUpdate["restartPolicy"] = args2.restart_policy;
|
|
2240
|
+
if (args2.pre_deploy_command !== void 0)
|
|
2241
|
+
configUpdate["preDeployCommand"] = args2.pre_deploy_command;
|
|
1889
2242
|
if (args2.instance_count !== void 0) {
|
|
1890
2243
|
configUpdate["minInstances"] = args2.instance_count;
|
|
1891
2244
|
configUpdate["maxInstances"] = args2.instance_count;
|
|
1892
2245
|
}
|
|
2246
|
+
if (args2.min_instances !== void 0) configUpdate["minInstances"] = args2.min_instances;
|
|
2247
|
+
if (args2.max_instances !== void 0) configUpdate["maxInstances"] = args2.max_instances;
|
|
2248
|
+
if (args2.scale_cpu_threshold !== void 0)
|
|
2249
|
+
configUpdate["scaleCpuThreshold"] = args2.scale_cpu_threshold;
|
|
2250
|
+
if (args2.scale_memory_threshold !== void 0)
|
|
2251
|
+
configUpdate["scaleMemoryThreshold"] = args2.scale_memory_threshold;
|
|
1893
2252
|
if (args2.log_filter_rules !== void 0) {
|
|
1894
2253
|
configUpdate["logFilterRules"] = args2.log_filter_rules;
|
|
1895
2254
|
}
|
|
@@ -1938,7 +2297,7 @@ defineTool({
|
|
|
1938
2297
|
'Example: suspend_service({ service_id: "svc_dev" }) \u2192 { ok: true }'
|
|
1939
2298
|
].join("\n"),
|
|
1940
2299
|
input: {
|
|
1941
|
-
service_id:
|
|
2300
|
+
service_id: z13.string().describe("Service publicId.")
|
|
1942
2301
|
},
|
|
1943
2302
|
handler: async (args2, ctx) => {
|
|
1944
2303
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -1962,7 +2321,7 @@ defineTool({
|
|
|
1962
2321
|
'Example: resume_service({ service_id: "svc_dev" }) \u2192 { ok: true }'
|
|
1963
2322
|
].join("\n"),
|
|
1964
2323
|
input: {
|
|
1965
|
-
service_id:
|
|
2324
|
+
service_id: z13.string().describe("Service publicId.")
|
|
1966
2325
|
},
|
|
1967
2326
|
handler: async (args2, ctx) => {
|
|
1968
2327
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -1996,16 +2355,16 @@ defineTool({
|
|
|
1996
2355
|
' - Just count error lines without fetching them: get_service_logs({ service_id: "svc_abc", level: "error", since: "-5m", count_only: true }) \u2192 { count: 47 }'
|
|
1997
2356
|
].join("\n"),
|
|
1998
2357
|
input: {
|
|
1999
|
-
service_id:
|
|
2000
|
-
lines:
|
|
2001
|
-
since:
|
|
2002
|
-
until:
|
|
2003
|
-
stream:
|
|
2004
|
-
level:
|
|
2358
|
+
service_id: z13.string().describe("Service publicId."),
|
|
2359
|
+
lines: z13.number().int().positive().max(1e3).optional().describe("Tail size; default 200, hard cap 1000."),
|
|
2360
|
+
since: z13.string().optional().describe('ISO-8601 timestamp or relative offset (e.g. "-5m", "-1h").'),
|
|
2361
|
+
until: z13.string().optional().describe("ISO-8601 timestamp or relative offset upper bound."),
|
|
2362
|
+
stream: z13.enum(["stdout", "stderr"]).optional().describe("Restrict to one stream."),
|
|
2363
|
+
level: z13.enum(["stdout", "stderr", "trace", "debug", "info", "warn", "error", "fatal"]).optional().describe(
|
|
2005
2364
|
"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)."
|
|
2006
2365
|
),
|
|
2007
|
-
search:
|
|
2008
|
-
count_only:
|
|
2366
|
+
search: z13.string().max(100).optional().describe("Case-insensitive substring filter."),
|
|
2367
|
+
count_only: z13.boolean().optional().describe("When true, return only { count } \u2014 skips the log payload.")
|
|
2009
2368
|
},
|
|
2010
2369
|
handler: async (args2, ctx) => {
|
|
2011
2370
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -2054,14 +2413,14 @@ defineTool({
|
|
|
2054
2413
|
'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 } } }.'
|
|
2055
2414
|
].join("\n"),
|
|
2056
2415
|
input: {
|
|
2057
|
-
service_ids:
|
|
2058
|
-
lines_per_service:
|
|
2059
|
-
since:
|
|
2060
|
-
until:
|
|
2061
|
-
stream:
|
|
2062
|
-
level:
|
|
2063
|
-
search:
|
|
2064
|
-
count_only:
|
|
2416
|
+
service_ids: z13.array(z13.string()).min(1).max(10).describe("Service publicIds (1\u201310). Hard cap 10 to bound parallel work."),
|
|
2417
|
+
lines_per_service: z13.number().int().positive().max(500).optional().describe("Tail size per service; default 100, hard cap 500."),
|
|
2418
|
+
since: z13.string().optional().describe('ISO-8601 timestamp or relative offset (e.g. "-5m", "-1h").'),
|
|
2419
|
+
until: z13.string().optional().describe("ISO-8601 timestamp or relative offset upper bound."),
|
|
2420
|
+
stream: z13.enum(["stdout", "stderr"]).optional().describe("Restrict to one stream."),
|
|
2421
|
+
level: z13.enum(["stdout", "stderr", "trace", "debug", "info", "warn", "error", "fatal"]).optional().describe("Structured log level filter (same as get_service_logs)."),
|
|
2422
|
+
search: z13.string().max(100).optional().describe("Case-insensitive substring filter."),
|
|
2423
|
+
count_only: z13.boolean().optional().describe("When true, return only counts per service \u2014 skips the log payload.")
|
|
2065
2424
|
},
|
|
2066
2425
|
handler: async (args2, ctx) => {
|
|
2067
2426
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -2107,7 +2466,7 @@ defineTool({
|
|
|
2107
2466
|
});
|
|
2108
2467
|
|
|
2109
2468
|
// src/tools/volumes.ts
|
|
2110
|
-
import { z as
|
|
2469
|
+
import { z as z14 } from "zod";
|
|
2111
2470
|
defineTool({
|
|
2112
2471
|
name: "list_volumes",
|
|
2113
2472
|
category: "volumes",
|
|
@@ -2124,7 +2483,7 @@ defineTool({
|
|
|
2124
2483
|
'Example: list_volumes({ service_id: "svc_abc" }) \u2192 { items: [{ name: "data", mountPath: "/var/data", sizeGb: 10, status: "active" }] }'
|
|
2125
2484
|
].join("\n"),
|
|
2126
2485
|
input: {
|
|
2127
|
-
service_id:
|
|
2486
|
+
service_id: z14.string().describe("Service publicId (e.g. svc_abc123).")
|
|
2128
2487
|
},
|
|
2129
2488
|
handler: async (args2, ctx) => {
|
|
2130
2489
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -2153,10 +2512,10 @@ defineTool({
|
|
|
2153
2512
|
'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" } }'
|
|
2154
2513
|
].join("\n"),
|
|
2155
2514
|
input: {
|
|
2156
|
-
service_id:
|
|
2157
|
-
name:
|
|
2158
|
-
mount_path:
|
|
2159
|
-
size_gb:
|
|
2515
|
+
service_id: z14.string().describe("Service publicId."),
|
|
2516
|
+
name: z14.string().min(1).max(64).regex(/^[a-z0-9-]+$/).describe("Volume name (lowercase alphanumeric + hyphens)."),
|
|
2517
|
+
mount_path: z14.string().startsWith("/").max(500).describe("In-container mount path (absolute)."),
|
|
2518
|
+
size_gb: z14.number().int().min(1).max(100).optional().describe("Disk size in GB (default 1, max 100).")
|
|
2160
2519
|
},
|
|
2161
2520
|
handler: async (args2, ctx) => {
|
|
2162
2521
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -2192,10 +2551,10 @@ defineTool({
|
|
|
2192
2551
|
'Example: update_volume({ service_id: "svc_abc", volume_id: "vol_xyz", size_gb: 20 }) \u2192 { volume: { sizeGb: 20, \u2026 } }'
|
|
2193
2552
|
].join("\n"),
|
|
2194
2553
|
input: {
|
|
2195
|
-
service_id:
|
|
2196
|
-
volume_id:
|
|
2197
|
-
mount_path:
|
|
2198
|
-
size_gb:
|
|
2554
|
+
service_id: z14.string().describe("Service publicId."),
|
|
2555
|
+
volume_id: z14.string().describe("Volume publicId (e.g. vol_\u2026)."),
|
|
2556
|
+
mount_path: z14.string().startsWith("/").max(500).optional().describe("New mount path."),
|
|
2557
|
+
size_gb: z14.number().int().min(1).max(100).optional().describe("New size in GB.")
|
|
2199
2558
|
},
|
|
2200
2559
|
handler: async (args2, ctx) => {
|
|
2201
2560
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -2233,8 +2592,8 @@ defineTool({
|
|
|
2233
2592
|
'Example: delete_volume({ service_id: "svc_abc", volume_id: "vol_xyz" }) \u2192 { ok: true }'
|
|
2234
2593
|
].join("\n"),
|
|
2235
2594
|
input: {
|
|
2236
|
-
service_id:
|
|
2237
|
-
volume_id:
|
|
2595
|
+
service_id: z14.string().describe("Service publicId."),
|
|
2596
|
+
volume_id: z14.string().describe("Volume publicId.")
|
|
2238
2597
|
},
|
|
2239
2598
|
handler: async (args2, ctx) => {
|
|
2240
2599
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -2248,7 +2607,7 @@ defineTool({
|
|
|
2248
2607
|
|
|
2249
2608
|
// src/server-factory.ts
|
|
2250
2609
|
var PACKAGE_NAME = "hoststack";
|
|
2251
|
-
var PACKAGE_VERSION = "0.
|
|
2610
|
+
var PACKAGE_VERSION = "0.6.0";
|
|
2252
2611
|
function createMcpServer(options) {
|
|
2253
2612
|
const baseUrl2 = (options.baseUrl ?? "https://hoststack.dev").replace(/\/$/, "");
|
|
2254
2613
|
const hoststack = new HostStack({ apiKey: options.apiKey, baseUrl: baseUrl2 });
|