@hoststack.dev/mcp 0.24.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/hoststack-mcp.js +450 -218
- package/dist/hoststack-mcp.js.map +1 -1
- package/dist/index.js +450 -218
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
3
3
|
import { HostStack } from "@hoststack.dev/sdk";
|
|
4
4
|
|
|
5
5
|
// src/version.ts
|
|
6
|
-
var MCP_VERSION = true ? "0.
|
|
6
|
+
var MCP_VERSION = true ? "0.26.0" : "0.0.0-dev";
|
|
7
7
|
var USER_AGENT = `hoststack-mcp/${MCP_VERSION}`;
|
|
8
8
|
|
|
9
9
|
// src/api-client.ts
|
|
@@ -656,6 +656,10 @@ async function resolveMachineId(ctx, teamId, machine) {
|
|
|
656
656
|
return matches[0].id;
|
|
657
657
|
}
|
|
658
658
|
function describeMachine(m) {
|
|
659
|
+
if (m.kind === "infra") {
|
|
660
|
+
const state = m.status === "active" ? "online" : "offline";
|
|
661
|
+
return `${m.name}: infrastructure machine for project ${m.infraProjectId}, ${state} (only that project's services can be pinned to it)`;
|
|
662
|
+
}
|
|
659
663
|
if (!m.enrolled) return `${m.name}: registered but never paired`;
|
|
660
664
|
if (m.status !== "active") return `${m.name}: offline`;
|
|
661
665
|
if (m.agentBuild === "from-source") return `${m.name}: online, agent running from source`;
|
|
@@ -1394,7 +1398,7 @@ var DNS_RECORD_TYPES = [
|
|
|
1394
1398
|
async function resolveZonePublicId(hoststack, teamId, input) {
|
|
1395
1399
|
if (input.zone_id) {
|
|
1396
1400
|
const { zones: zones2 } = await hoststack.dns.listZones(teamId);
|
|
1397
|
-
const match = zones2.find((
|
|
1401
|
+
const match = zones2.find((z23) => z23.publicId === input.zone_id);
|
|
1398
1402
|
if (!match) {
|
|
1399
1403
|
throw new Error(`Zone ${input.zone_id} not found on this team.`);
|
|
1400
1404
|
}
|
|
@@ -1408,7 +1412,7 @@ async function resolveZonePublicId(hoststack, teamId, input) {
|
|
|
1408
1412
|
const labels = fqdn.split(".");
|
|
1409
1413
|
for (let i = 0; i < labels.length - 1; i++) {
|
|
1410
1414
|
const candidate = labels.slice(i).join(".");
|
|
1411
|
-
const match = zones.find((
|
|
1415
|
+
const match = zones.find((z23) => z23.domainName.toLowerCase() === candidate);
|
|
1412
1416
|
if (match && match.status !== "deleting") {
|
|
1413
1417
|
return { publicId: match.publicId, domainName: match.domainName };
|
|
1414
1418
|
}
|
|
@@ -1425,19 +1429,85 @@ defineTool({
|
|
|
1425
1429
|
"",
|
|
1426
1430
|
"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.",
|
|
1427
1431
|
"",
|
|
1428
|
-
'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.',
|
|
1432
|
+
'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, plus the delegation reading below.',
|
|
1433
|
+
"",
|
|
1434
|
+
'READ delegationStatus, NOT status, to answer "is this domain live?". `status` describes the zone on OUR side \u2014 "active" means PowerDNS holds it and answers for it. `delegationStatus` describes whether the parent registry actually SENDS anyone here:',
|
|
1435
|
+
' - "delegated" \u2014 the registry names our nameservers; queries reach us.',
|
|
1436
|
+
' - "foreign" \u2014 the registry names somebody else (delegationObservedNs says who). The zone is authoritative for nobody: its records are correct and unreachable, public lookups return the OLD host or nothing, and no domain under it can pass verification or get a certificate. This is the state a half-finished migration sits in, and it looks identical to a healthy zone on every other field.',
|
|
1437
|
+
' - "unknown" \u2014 no usable answer (SERVFAIL/timeout/never checked). NOT the same as "foreign"; do not tell a user to change their registrar on the strength of it. Call check_dns_delegation for a live reading.',
|
|
1438
|
+
"",
|
|
1439
|
+
"delegationObservedNs is the NS set the parent publishes today, and delegationCheckedAt is when it was read (refreshed hourly). For an answer current to the second \u2014 e.g. just after a registrar change \u2014 call check_dns_delegation.",
|
|
1440
|
+
"",
|
|
1441
|
+
'Example: list_dns_zones() \u2192 { items: [{ publicId: "dnz_abc", domainName: "micci.dk", status: "active", nsRecords: ["ns1.hoststack.dev","ns2.hoststack.dev"], delegationStatus: "foreign", delegationObservedNs: ["docks15.rzone.de","shades13.rzone.de"] }] }',
|
|
1442
|
+
"",
|
|
1443
|
+
'Each zone ALSO carries what the registry says about the REGISTRATION, refreshed daily over RDAP: registrar, registryStatus (EPP codes such as clientHold / pendingTransfer / pendingDelete / redemptionPeriod), registryExpiresAt, registryCheckedAt and registryCheckOutcome ("ok" | "not_found" | "unsupported" | "rate_limited" | "error").',
|
|
1429
1444
|
"",
|
|
1430
|
-
'
|
|
1445
|
+
'This is a different question from delegation, and a zone can fail either one alone. Delegation asks whether the world is being sent here; these fields ask whether the domain is still going to be the customer\'s next month. `status: "active"` answers neither \u2014 it describes our nameservers only.',
|
|
1446
|
+
"",
|
|
1447
|
+
'SILENCE IS NOT GOOD NEWS HERE. A null registryExpiresAt or registrar on a zone that HAS been checked means the registry publishes neither \u2014 DENIC publishes no expiry and no registrar for any .de \u2014 and registryCheckOutcome "unsupported" means the TLD has no RDAP service at all (there is none for .dk), so every field above it is unknowable rather than fine. Read registryCheckOutcome before drawing any conclusion from an empty field.',
|
|
1448
|
+
"",
|
|
1449
|
+
"HostStack is not the registrar for a domain whose DNS it merely hosts. Nothing in this MCP, the CLI or the dashboard can renew, transfer or un-hold a registration \u2014 if registryStatus or registryExpiresAt shows trouble, the action is at the registrar named in `registrar`.",
|
|
1450
|
+
"",
|
|
1451
|
+
'Example: list_dns_zones() \u2192 { items: [{ publicId: "dnz_abc", domainName: "micci.dk", status: "active", nsRecords: ["ns1.hoststack.dev","ns2.hoststack.dev"], registrar: "GoDaddy.com, LLC", registryStatus: ["clientTransferProhibited"], registryExpiresAt: "2027-03-04T00:00:00Z", registryCheckOutcome: "ok" }] }'
|
|
1431
1452
|
].join("\n"),
|
|
1432
1453
|
input: {},
|
|
1433
1454
|
handler: async (_args, ctx) => {
|
|
1434
1455
|
const teamId = await ctx.resolveTeamId();
|
|
1435
1456
|
const response = await ctx.hoststack.dns.listZones(teamId);
|
|
1436
|
-
const
|
|
1437
|
-
const
|
|
1457
|
+
const zones = Array.isArray(response.zones) ? response.zones : [];
|
|
1458
|
+
const items = zones.map(shape);
|
|
1459
|
+
const foreign = zones.filter((z23) => z23.delegationStatus === "foreign");
|
|
1460
|
+
const foreignNote = foreign.length === 0 ? "" : ` WARNING: ${foreign.length} zone${foreign.length === 1 ? " is" : "s are"} hosted here but still delegated elsewhere by the registry \u2014 ` + foreign.map(
|
|
1461
|
+
(z23) => `${z23.domainName} \u2192 ${(z23.delegationObservedNs ?? []).join(", ") || "unknown nameservers"}`
|
|
1462
|
+
).join("; ") + `. Records on ${foreign.length === 1 ? "it" : "them"} are correct but unreachable; the nameservers must be changed at the registrar.`;
|
|
1463
|
+
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"}.${foreignNote}`;
|
|
1438
1464
|
return respond({ summary, data: { items } });
|
|
1439
1465
|
}
|
|
1440
1466
|
});
|
|
1467
|
+
defineTool({
|
|
1468
|
+
name: "check_dns_delegation",
|
|
1469
|
+
category: "dns",
|
|
1470
|
+
description: [
|
|
1471
|
+
"Read, live, whether the parent registry actually delegates a zone's apex to HostStack \u2014 and store the reading on the zone.",
|
|
1472
|
+
"",
|
|
1473
|
+
'WHAT THIS ANSWERS that nothing else does: a hosted zone can be `status: "active"`, DNSSEC-signed, full of correct records, and authoritative for NOBODY, because the registry still points the domain at the previous host. Public lookups then return the old host (or nothing) while a query against our own nameservers returns the right answer \u2014 and every other field we expose says healthy. This is the normal shape of a half-finished migration, and it is what to check FIRST when a zone looks perfect but the site is not live, a domain will not verify, or a certificate will not issue.',
|
|
1474
|
+
"",
|
|
1475
|
+
"When to use: right after someone changes nameservers at a registrar (list_dns_zones carries the hourly reading, which will be stale by minutes); before telling anyone a cutover is done; or to explain why verify_domain keeps returning pending.",
|
|
1476
|
+
"",
|
|
1477
|
+
"Provide EITHER zone_id (the zone publicId) OR domain (apex or subdomain \u2014 the longest-matching hosted apex wins).",
|
|
1478
|
+
"",
|
|
1479
|
+
"Returns: { zone, delegation: { status, observedNameservers, expectedNameservers, checkedAt } }.",
|
|
1480
|
+
` - status "delegated": at least one of our nameservers is in the parent's NS set. Any, not all \u2014 a staged migration legitimately runs split for a while, and queries already reach us during it.`,
|
|
1481
|
+
' - status "foreign": the parent answered and named somebody else. observedNameservers is who. The fix is at the REGISTRAR of the apex, never in our records; expect up to 48h of propagation after the change.',
|
|
1482
|
+
' - status "unknown": no usable answer (SERVFAIL, timeout, NXDOMAIN). This is NOT "not delegated" \u2014 never tell someone to re-point a registrar on the strength of it. Retry, or check the domain is registered at all.',
|
|
1483
|
+
"",
|
|
1484
|
+
'Note for `.dk` and other strict registries: they refuse the nameserver change until we already answer authoritatively for the domain, so a zone there is legitimately "foreign" for a while by design. That is the ordering the registry imposes, not a mistake.',
|
|
1485
|
+
"",
|
|
1486
|
+
'Example: check_dns_delegation({ domain: "wohnwagen-wuckert.de" }) \u2192 { delegation: { status: "foreign", observedNameservers: ["docks15.rzone.de","shades13.rzone.de"], expectedNameservers: ["ns1.hoststack.dev","ns2.hoststack.dev"] } }'
|
|
1487
|
+
].join("\n"),
|
|
1488
|
+
input: {
|
|
1489
|
+
zone_id: z8.string().optional().describe('Zone publicId (e.g. "dnz_abc").'),
|
|
1490
|
+
domain: z8.string().optional().describe("Apex or subdomain \u2014 resolves to the longest-matching hosted zone.")
|
|
1491
|
+
},
|
|
1492
|
+
handler: async (args, ctx) => {
|
|
1493
|
+
const teamId = await ctx.resolveTeamId();
|
|
1494
|
+
const zoneInput = {};
|
|
1495
|
+
if (args.zone_id !== void 0) zoneInput.zone_id = args.zone_id;
|
|
1496
|
+
if (args.domain !== void 0) zoneInput.domain = args.domain;
|
|
1497
|
+
const zone = await resolveZonePublicId(ctx.hoststack, teamId, zoneInput);
|
|
1498
|
+
const { delegation } = await ctx.hoststack.dns.checkDelegation(teamId, zone.publicId);
|
|
1499
|
+
const observed = delegation.observedNameservers.join(", ");
|
|
1500
|
+
const expected = delegation.expectedNameservers.join(" / ");
|
|
1501
|
+
const summary = delegation.status === "delegated" ? `${zone.domainName} is delegated to HostStack (${observed}). Queries for it reach this zone.` : delegation.status === "foreign" ? `${zone.domainName} is HOSTED here but NOT delegated here: the registry still points it at ${observed}. Records in this zone are correct but nobody is being sent to read them \u2014 public lookups return the old host. Change the nameservers at the registrar of ${zone.domainName} to ${expected}; propagation can take up to 48 hours.` : `Could not determine the delegation for ${zone.domainName} \u2014 the NS lookup returned no usable answer (SERVFAIL, timeout, or the domain is not registered). This is NOT evidence that the delegation is wrong; do not change a registrar on the strength of it. Retry in a moment.`;
|
|
1502
|
+
return respond({
|
|
1503
|
+
summary,
|
|
1504
|
+
data: {
|
|
1505
|
+
zone: { publicId: zone.publicId, domainName: zone.domainName },
|
|
1506
|
+
delegation
|
|
1507
|
+
}
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
});
|
|
1441
1511
|
defineTool({
|
|
1442
1512
|
name: "list_dns_records",
|
|
1443
1513
|
category: "dns",
|
|
@@ -1769,10 +1839,14 @@ defineTool({
|
|
|
1769
1839
|
};
|
|
1770
1840
|
if (args.path_prefix !== void 0) input.pathPrefix = args.path_prefix;
|
|
1771
1841
|
const response = await ctx.hoststack.domains.add(teamId, input);
|
|
1772
|
-
const dnsSyncWarning = response.domain
|
|
1773
|
-
const data =
|
|
1842
|
+
const { dnsSyncWarning, delegationWarning } = response.domain;
|
|
1843
|
+
const data = {
|
|
1844
|
+
domain: shapeDomain(response.domain),
|
|
1845
|
+
...dnsSyncWarning ? { dnsSyncWarning } : {},
|
|
1846
|
+
...delegationWarning ? { delegationWarning } : {}
|
|
1847
|
+
};
|
|
1774
1848
|
return respond({
|
|
1775
|
-
summary: dnsSyncWarning ? `Added domain ${args.hostname}, but its auto-created DNS record hasn't synced yet \u2014 ${dnsSyncWarning}` : `Added domain ${args.hostname}. Configure DNS, then call verify_domain.`,
|
|
1849
|
+
summary: delegationWarning ? `Added domain ${args.hostname}, but it CANNOT verify yet: ${delegationWarning}` : dnsSyncWarning ? `Added domain ${args.hostname}, but its auto-created DNS record hasn't synced yet \u2014 ${dnsSyncWarning}` : `Added domain ${args.hostname}. Configure DNS, then call verify_domain.`,
|
|
1776
1850
|
data
|
|
1777
1851
|
});
|
|
1778
1852
|
}
|
|
@@ -1788,19 +1862,30 @@ defineTool({
|
|
|
1788
1862
|
"Inputs:",
|
|
1789
1863
|
" - domain_id: publicId of the domain (from list_domains or add_domain).",
|
|
1790
1864
|
"",
|
|
1791
|
-
"Returns: {
|
|
1865
|
+
"Returns: { domain, verified }. `verified` is the outcome of THIS check \u2014 the call does not merely queue one.",
|
|
1866
|
+
"",
|
|
1867
|
+
"If it fails and HostStack hosts the zone, the response may carry `delegationWarning`: the records are ours and correct, but the registry still delegates the apex to another host, so queries never reach us. That will NOT resolve by waiting or by calling this again \u2014 the nameservers have to change at the registrar. Use check_dns_delegation to see who the apex currently points at.",
|
|
1792
1868
|
"",
|
|
1793
|
-
|
|
1869
|
+
"It may instead carry `dnsSyncWarning`: the record HostStack creates for this hostname could not be published, usually because a record already at that name conflicts with it (a CNAME and an A cannot share a name). Also not fixable by retrying \u2014 resolve the conflict it names with list_dns_records / delete_dns_record first.",
|
|
1870
|
+
"",
|
|
1871
|
+
'Example: verify_domain({ domain_id: "dom_xyz" }) \u2192 { verified: false, delegationWarning: "\u2026the registry still delegates wohnwagen-wuckert.de to docks15.rzone.de\u2026" }'
|
|
1794
1872
|
].join("\n"),
|
|
1795
1873
|
input: {
|
|
1796
1874
|
domain_id: z9.string().describe("Domain publicId.")
|
|
1797
1875
|
},
|
|
1798
1876
|
handler: async (args, ctx) => {
|
|
1799
1877
|
const teamId = await ctx.resolveTeamId();
|
|
1800
|
-
await ctx.hoststack.domains.verify(teamId, args.domain_id);
|
|
1878
|
+
const { domain } = await ctx.hoststack.domains.verify(teamId, args.domain_id);
|
|
1879
|
+
const verified = domain.status === "active";
|
|
1880
|
+
const summary = verified ? `Verified ${domain.domain} \u2014 it is now active.` : domain.delegationWarning ? `${domain.domain} did NOT verify, and retrying will not help: ${domain.delegationWarning}` : domain.dnsSyncWarning ? `${domain.domain} did NOT verify, and retrying will not help: ${domain.dnsSyncWarning}` : `${domain.domain} did not verify yet (status: ${domain.status}). DNS changes can take time to propagate; check the records resolve publicly, then call this again.`;
|
|
1801
1881
|
return respond({
|
|
1802
|
-
summary
|
|
1803
|
-
data: {
|
|
1882
|
+
summary,
|
|
1883
|
+
data: {
|
|
1884
|
+
domain: shapeDomain(domain),
|
|
1885
|
+
verified,
|
|
1886
|
+
...domain.delegationWarning ? { delegationWarning: domain.delegationWarning } : {},
|
|
1887
|
+
...domain.dnsSyncWarning ? { dnsSyncWarning: domain.dnsSyncWarning } : {}
|
|
1888
|
+
}
|
|
1804
1889
|
});
|
|
1805
1890
|
}
|
|
1806
1891
|
});
|
|
@@ -2894,19 +2979,69 @@ defineTool({
|
|
|
2894
2979
|
});
|
|
2895
2980
|
|
|
2896
2981
|
// src/tools/github.ts
|
|
2982
|
+
import { z as z14 } from "zod";
|
|
2983
|
+
function shapeRepo(repo) {
|
|
2984
|
+
return {
|
|
2985
|
+
id: repo.id,
|
|
2986
|
+
fullName: repo.fullName,
|
|
2987
|
+
defaultBranch: repo.defaultBranch,
|
|
2988
|
+
private: repo.isPrivate,
|
|
2989
|
+
url: repo.htmlUrl,
|
|
2990
|
+
...repo.language ? { language: repo.language } : {}
|
|
2991
|
+
};
|
|
2992
|
+
}
|
|
2993
|
+
defineTool({
|
|
2994
|
+
name: "list_github_repos",
|
|
2995
|
+
category: "github",
|
|
2996
|
+
description: [
|
|
2997
|
+
"Every GitHub repository this team can deploy, with the `id` create_service takes \u2014 and the `owner/name` you can pass instead of it.",
|
|
2998
|
+
"",
|
|
2999
|
+
"When to use: before create_service or create_standalone_dev_environment with a repo source. `github_repo_id` is a HostStack row id, NOT the id GitHub shows \u2014 it appears in no clone URL and no GitHub API response, so it cannot be derived and must not be guessed. (Guessing used to be survivable-looking: the create returned 200 and the mistake surfaced one step later as `Invalid git clone URL` from the deploy. It is now refused at create time.)",
|
|
3000
|
+
"",
|
|
3001
|
+
"Inputs:",
|
|
3002
|
+
' - q (optional): case-insensitive substring of the full name ("owner/name"). Omit for everything.',
|
|
3003
|
+
"",
|
|
3004
|
+
"Returns: { items: [{ id, fullName, defaultBranch, private, url, language? }], total } \u2014 `total` is the count before `q` filtered it.",
|
|
3005
|
+
"",
|
|
3006
|
+
"If the repo you just pushed is missing, call sync_github_repos first: HostStack cannot see a brand-new repository until an installation re-sync runs.",
|
|
3007
|
+
"",
|
|
3008
|
+
'Example: list_github_repos({ q: "stenshoppen" }) \u2192 { items: [{ id: 143, fullName: "acme/stenshoppen", defaultBranch: "master", private: true }], total: 45 }',
|
|
3009
|
+
'Then either: create_service({ \u2026, github_repo: "acme/stenshoppen" }) \u2014 or the id, if you prefer.'
|
|
3010
|
+
].join("\n"),
|
|
3011
|
+
input: {
|
|
3012
|
+
q: z14.string().min(1).max(200).optional().describe('Case-insensitive substring of "owner/name". Omit to list everything.')
|
|
3013
|
+
},
|
|
3014
|
+
handler: async (args, ctx) => {
|
|
3015
|
+
const teamId = await ctx.resolveTeamId();
|
|
3016
|
+
const { repos } = await ctx.api.get(
|
|
3017
|
+
`/api/github/${teamId}/repos`
|
|
3018
|
+
);
|
|
3019
|
+
const needle = args.q?.trim().toLowerCase();
|
|
3020
|
+
const matched = needle ? repos.filter((r) => r.fullName.toLowerCase().includes(needle)) : repos;
|
|
3021
|
+
const items = matched.map(shapeRepo).sort((a, b) => a.fullName.localeCompare(b.fullName));
|
|
3022
|
+
if (repos.length === 0) {
|
|
3023
|
+
return respond({
|
|
3024
|
+
summary: "No GitHub repositories are connected to this team. Install the HostStack GitHub App from the dashboard (Settings \u2192 GitHub), then call sync_github_repos.",
|
|
3025
|
+
data: { items, total: 0 }
|
|
3026
|
+
});
|
|
3027
|
+
}
|
|
3028
|
+
const summary = needle ? items.length === 0 ? `No connected repository matches "${args.q}" (${repos.length} connected). Push it, then call sync_github_repos.` : `${items.length} of ${repos.length} connected repositories match "${args.q}".` : `${items.length} connected repositor${items.length === 1 ? "y" : "ies"}.`;
|
|
3029
|
+
return respond({ summary, data: { items, total: repos.length } });
|
|
3030
|
+
}
|
|
3031
|
+
});
|
|
2897
3032
|
defineTool({
|
|
2898
3033
|
name: "sync_github_repos",
|
|
2899
3034
|
category: "github",
|
|
2900
3035
|
description: [
|
|
2901
|
-
"Re-sync the repository list from GitHub for every connected GitHub App installation.",
|
|
3036
|
+
"Re-sync the repository list from GitHub for every connected GitHub App installation, and return what that now exposes.",
|
|
2902
3037
|
"",
|
|
2903
|
-
"When to use: right after pushing a brand-new repository, HostStack cannot see it until an installation re-sync runs \u2014 so create_service with a github_repo_id would fail. Call this first. (Same operation as the dashboard's refresh icon on the repo picker.)",
|
|
3038
|
+
"When to use: right after pushing a brand-new repository, HostStack cannot see it until an installation re-sync runs \u2014 so create_service with a github_repo_id or github_repo would fail. Call this first. (Same operation as the dashboard's refresh icon on the repo picker.) To read the list WITHOUT re-syncing, call list_github_repos.",
|
|
2904
3039
|
"",
|
|
2905
3040
|
"No inputs.",
|
|
2906
3041
|
"",
|
|
2907
|
-
"Returns: { installations: number,
|
|
3042
|
+
"Returns: { installations: number, accounts: [{ account, count }], totalRepos, repos: [{ id, fullName }] } \u2014 `repos` carries the ids create_service takes, so a sync is enough to act on; it does not have to be followed by a list.",
|
|
2908
3043
|
"",
|
|
2909
|
-
'Example: sync_github_repos({}) \u2192 { installations: 1,
|
|
3044
|
+
'Example: sync_github_repos({}) \u2192 { installations: 1, accounts: [{ account: "acme", count: 12 }], totalRepos: 12, repos: [{ id: 143, fullName: "acme/stenshoppen" }, \u2026] }'
|
|
2910
3045
|
].join("\n"),
|
|
2911
3046
|
input: {},
|
|
2912
3047
|
handler: async (_args, ctx) => {
|
|
@@ -2920,23 +3055,29 @@ defineTool({
|
|
|
2920
3055
|
);
|
|
2921
3056
|
}
|
|
2922
3057
|
const perAccount = [];
|
|
2923
|
-
|
|
3058
|
+
const synced = [];
|
|
2924
3059
|
for (const inst of installations) {
|
|
2925
3060
|
const { repos } = await ctx.api.post(
|
|
2926
3061
|
`/api/github/${teamId}/installations/${inst.id}/sync`
|
|
2927
3062
|
);
|
|
2928
3063
|
perAccount.push({ account: inst.accountLogin, count: repos.length });
|
|
2929
|
-
|
|
3064
|
+
for (const repo of repos) synced.push({ id: repo.id, fullName: repo.fullName });
|
|
2930
3065
|
}
|
|
3066
|
+
synced.sort((a, b) => a.fullName.localeCompare(b.fullName));
|
|
2931
3067
|
return respond({
|
|
2932
|
-
summary: `Synced ${
|
|
2933
|
-
data: {
|
|
3068
|
+
summary: `Synced ${synced.length} repositories across ${installations.length} installation(s).`,
|
|
3069
|
+
data: {
|
|
3070
|
+
installations: installations.length,
|
|
3071
|
+
accounts: perAccount,
|
|
3072
|
+
totalRepos: synced.length,
|
|
3073
|
+
repos: synced
|
|
3074
|
+
}
|
|
2934
3075
|
});
|
|
2935
3076
|
}
|
|
2936
3077
|
});
|
|
2937
3078
|
|
|
2938
3079
|
// src/tools/issue-reports.ts
|
|
2939
|
-
import { z as
|
|
3080
|
+
import { z as z15 } from "zod";
|
|
2940
3081
|
defineTool({
|
|
2941
3082
|
name: "report_issue",
|
|
2942
3083
|
category: "support",
|
|
@@ -2958,16 +3099,16 @@ defineTool({
|
|
|
2958
3099
|
"Example: report_issue({ title: 'Dev box deploys fail with 404 No such image', description: 'Every deploy of svc 172 since 2026-08-22 09:03 fails at container create with `404 No such image: \u2026/dev-env:latest`. The deploy log reports \"Image ready in 1s\" for a 5.5 GB image, so the pull is not happening. The image pulls fine by hand from the same registry.', severity: 'high', serviceId: 172 }) \u2192 { ticket: { publicId: 'tkt_\u2026' } }"
|
|
2959
3100
|
].join("\n"),
|
|
2960
3101
|
input: {
|
|
2961
|
-
title:
|
|
2962
|
-
description:
|
|
2963
|
-
severity:
|
|
3102
|
+
title: z15.string().min(1).max(300).describe("One-line summary of the fault."),
|
|
3103
|
+
description: z15.string().min(1).max(1e4).describe("What happened, what you expected instead, and what you already ruled out."),
|
|
3104
|
+
severity: z15.enum(["low", "normal", "high", "urgent"]).optional().describe(
|
|
2964
3105
|
'Default normal. Use high/urgent only when something is DOWN or losing data \u2014 not for "this is annoying".'
|
|
2965
3106
|
),
|
|
2966
|
-
serviceId:
|
|
3107
|
+
serviceId: z15.number().int().positive().optional().describe(
|
|
2967
3108
|
"The affected service. Attaches the service, its latest deploy, and that deploy log tail automatically."
|
|
2968
3109
|
),
|
|
2969
|
-
deployId:
|
|
2970
|
-
databaseId:
|
|
3110
|
+
deployId: z15.number().int().positive().optional().describe("Pin a specific deploy instead of the service\u2019s most recent one."),
|
|
3111
|
+
databaseId: z15.number().int().positive().optional().describe("The affected database.")
|
|
2971
3112
|
},
|
|
2972
3113
|
handler: async (args, ctx) => {
|
|
2973
3114
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -3055,7 +3196,7 @@ defineTool({
|
|
|
3055
3196
|
});
|
|
3056
3197
|
|
|
3057
3198
|
// src/tools/notifications.ts
|
|
3058
|
-
import { z as
|
|
3199
|
+
import { z as z16 } from "zod";
|
|
3059
3200
|
var NOTIFICATION_EVENTS = [
|
|
3060
3201
|
"deploy.started",
|
|
3061
3202
|
"deploy.succeeded",
|
|
@@ -3081,6 +3222,7 @@ var NOTIFICATION_EVENTS = [
|
|
|
3081
3222
|
"error.issue_regressed",
|
|
3082
3223
|
"git.auth_failed",
|
|
3083
3224
|
"cron.execution_failed",
|
|
3225
|
+
"cron.schedule_missed",
|
|
3084
3226
|
"workflow.failed",
|
|
3085
3227
|
"devenv.agent.needs_input",
|
|
3086
3228
|
"devenv.agent.finished",
|
|
@@ -3093,6 +3235,10 @@ var NOTIFICATION_EVENTS = [
|
|
|
3093
3235
|
"database.restore_failed",
|
|
3094
3236
|
"volume.backup_failed",
|
|
3095
3237
|
"volume.backup_overdue",
|
|
3238
|
+
"dns.registry_status_changed",
|
|
3239
|
+
"dns.registry_expiring",
|
|
3240
|
+
"dns.registry_domain_missing",
|
|
3241
|
+
"dns.registry_record_changed",
|
|
3096
3242
|
"domain.registrant_verification_lapsed",
|
|
3097
3243
|
"service.auto_restarted",
|
|
3098
3244
|
"machine.offline",
|
|
@@ -3149,10 +3295,10 @@ defineTool({
|
|
|
3149
3295
|
"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'] })"
|
|
3150
3296
|
].join("\n"),
|
|
3151
3297
|
input: {
|
|
3152
|
-
type:
|
|
3153
|
-
name:
|
|
3154
|
-
webhook_url:
|
|
3155
|
-
events:
|
|
3298
|
+
type: z16.enum(["slack", "discord", "email"]).describe("Channel type."),
|
|
3299
|
+
name: z16.string().min(1).max(128).describe("Human-readable label."),
|
|
3300
|
+
webhook_url: z16.string().max(500).describe("Slack/Discord webhook URL or email address (when type=email)."),
|
|
3301
|
+
events: z16.array(z16.enum(NOTIFICATION_EVENTS)).describe(
|
|
3156
3302
|
"List of events the channel subscribes to. Empty list = subscribe to nothing."
|
|
3157
3303
|
)
|
|
3158
3304
|
},
|
|
@@ -3190,10 +3336,10 @@ defineTool({
|
|
|
3190
3336
|
"Example: update_notification_channel({ channel_id: 3, events: ['deploy.failed', 'service.restart_failed', 'git.auth_failed'] })"
|
|
3191
3337
|
].join("\n"),
|
|
3192
3338
|
input: {
|
|
3193
|
-
channel_id:
|
|
3194
|
-
name:
|
|
3195
|
-
active:
|
|
3196
|
-
events:
|
|
3339
|
+
channel_id: z16.number().int().positive().describe("Numeric channel id from list_notification_channels."),
|
|
3340
|
+
name: z16.string().min(1).max(128).optional().describe("New label."),
|
|
3341
|
+
active: z16.boolean().optional().describe("false silences without deleting."),
|
|
3342
|
+
events: z16.array(z16.enum(NOTIFICATION_EVENTS)).optional().describe("Replaces the full subscription list.")
|
|
3197
3343
|
},
|
|
3198
3344
|
handler: async (args, ctx) => {
|
|
3199
3345
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -3232,7 +3378,7 @@ defineTool({
|
|
|
3232
3378
|
"Example: delete_notification_channel({ channel_id: 3 }) \u2192 { ok: true }"
|
|
3233
3379
|
].join("\n"),
|
|
3234
3380
|
input: {
|
|
3235
|
-
channel_id:
|
|
3381
|
+
channel_id: z16.number().int().positive().describe("Numeric channel id.")
|
|
3236
3382
|
},
|
|
3237
3383
|
handler: async (args, ctx) => {
|
|
3238
3384
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -3259,7 +3405,7 @@ defineTool({
|
|
|
3259
3405
|
"Example: test_notification_channel({ channel_id: 3 }) \u2192 { success: true }"
|
|
3260
3406
|
].join("\n"),
|
|
3261
3407
|
input: {
|
|
3262
|
-
channel_id:
|
|
3408
|
+
channel_id: z16.number().int().positive().describe("Numeric channel id.")
|
|
3263
3409
|
},
|
|
3264
3410
|
handler: async (args, ctx) => {
|
|
3265
3411
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -3274,7 +3420,7 @@ defineTool({
|
|
|
3274
3420
|
});
|
|
3275
3421
|
|
|
3276
3422
|
// src/tools/projects.ts
|
|
3277
|
-
import { z as
|
|
3423
|
+
import { z as z17 } from "zod";
|
|
3278
3424
|
var AVAILABLE_REGION_IDS = ["eu-central-1"];
|
|
3279
3425
|
defineTool({
|
|
3280
3426
|
name: "list_projects",
|
|
@@ -3315,9 +3461,9 @@ defineTool({
|
|
|
3315
3461
|
'Example: create_project({ name: "billing-api", description: "Stripe webhooks", region: "eu-central-1" }) \u2192 { project: { id: 12, publicId: "prj_\u2026", \u2026 } }'
|
|
3316
3462
|
].join("\n"),
|
|
3317
3463
|
input: {
|
|
3318
|
-
name:
|
|
3319
|
-
description:
|
|
3320
|
-
region:
|
|
3464
|
+
name: z17.string().min(1).max(60).describe("Project name (1\u201360 chars)."),
|
|
3465
|
+
description: z17.string().max(500).optional().describe("Short description (\u2264500 chars)."),
|
|
3466
|
+
region: z17.enum(AVAILABLE_REGION_IDS).optional().describe("Region: eu-central-1 (Falkenstein) \u2014 currently the only available region.")
|
|
3321
3467
|
},
|
|
3322
3468
|
handler: async (args, ctx) => {
|
|
3323
3469
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -3350,9 +3496,9 @@ defineTool({
|
|
|
3350
3496
|
'Example: update_project({ project_id: "prj_abc", name: "billing-prod" }) \u2192 { project: { name: "billing-prod", \u2026 } }'
|
|
3351
3497
|
].join("\n"),
|
|
3352
3498
|
input: {
|
|
3353
|
-
project_id:
|
|
3354
|
-
name:
|
|
3355
|
-
description:
|
|
3499
|
+
project_id: z17.string().describe("Project publicId."),
|
|
3500
|
+
name: z17.string().min(1).max(60).optional().describe("New name (1\u201360 chars)."),
|
|
3501
|
+
description: z17.string().max(500).optional().describe("New description (\u2264500 chars).")
|
|
3356
3502
|
},
|
|
3357
3503
|
handler: async (args, ctx) => {
|
|
3358
3504
|
if (args.name === void 0 && args.description === void 0) {
|
|
@@ -3386,7 +3532,7 @@ defineTool({
|
|
|
3386
3532
|
'Example: get_project({ project_id: "prj_abc" }) \u2192 { project: { id: 12, name: "billing", \u2026 } }'
|
|
3387
3533
|
].join("\n"),
|
|
3388
3534
|
input: {
|
|
3389
|
-
project_id:
|
|
3535
|
+
project_id: z17.string().describe("Project publicId (e.g. prj_abc123).")
|
|
3390
3536
|
},
|
|
3391
3537
|
handler: async (args, ctx) => {
|
|
3392
3538
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -3398,7 +3544,7 @@ defineTool({
|
|
|
3398
3544
|
});
|
|
3399
3545
|
|
|
3400
3546
|
// src/tools/dev-tasks.ts
|
|
3401
|
-
import { z as
|
|
3547
|
+
import { z as z18 } from "zod";
|
|
3402
3548
|
var NOT_A_RUN = "Filing a task does NOT start an agent. It writes a prompt into the box's backlog for someone to run; a suspended box keeps it until it wakes.";
|
|
3403
3549
|
defineTool({
|
|
3404
3550
|
name: "list_dev_tasks",
|
|
@@ -3418,7 +3564,7 @@ defineTool({
|
|
|
3418
3564
|
'Example: list_dev_tasks({ project_id: 26 }) \u2192 { items: [{ publicId: "task_\u2026", status: "idea", title: "Fix the footprint join" }] }'
|
|
3419
3565
|
].join("\n"),
|
|
3420
3566
|
input: {
|
|
3421
|
-
project_id:
|
|
3567
|
+
project_id: z18.union([z18.number().int().positive(), z18.string()]).describe('Project \u2014 publicId ("prj_\u2026") or numeric id.')
|
|
3422
3568
|
},
|
|
3423
3569
|
handler: async (args, ctx) => {
|
|
3424
3570
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -3447,7 +3593,7 @@ defineTool({
|
|
|
3447
3593
|
'Example: get_dev_task({ task_id: "task_hy1i2jdp\u2026" }) \u2192 { data: { title: "Footprint coverage", body: "The BBRUUID join returns 17 of 49 \u2026", status: "idea" } }'
|
|
3448
3594
|
].join("\n"),
|
|
3449
3595
|
input: {
|
|
3450
|
-
task_id:
|
|
3596
|
+
task_id: z18.union([z18.number().int().positive(), z18.string()]).describe('Task \u2014 publicId ("task_\u2026") or numeric id.')
|
|
3451
3597
|
},
|
|
3452
3598
|
handler: async (args, ctx) => {
|
|
3453
3599
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -3475,10 +3621,10 @@ defineTool({
|
|
|
3475
3621
|
'Example: create_dev_task({ project_id: 26, service_id: 51, title: "Footprint coverage", body: "The BBRUUID join returns 17 of 49 \u2026" })'
|
|
3476
3622
|
].join("\n"),
|
|
3477
3623
|
input: {
|
|
3478
|
-
project_id:
|
|
3479
|
-
title:
|
|
3480
|
-
body:
|
|
3481
|
-
service_id:
|
|
3624
|
+
project_id: z18.union([z18.number().int().positive(), z18.string()]).describe('Project \u2014 publicId ("prj_\u2026") or numeric id.'),
|
|
3625
|
+
title: z18.string().min(1).max(200).describe("One-line title, \u2264200 chars."),
|
|
3626
|
+
body: z18.string().max(2e4).optional().describe("The prompt handed to the agent. Markdown, \u226420 000 chars."),
|
|
3627
|
+
service_id: z18.union([z18.number().int().positive(), z18.string()]).optional().describe(
|
|
3482
3628
|
'Dev box to pin it to \u2014 publicId ("svc_\u2026") or numeric id. Omit for a loose idea.'
|
|
3483
3629
|
)
|
|
3484
3630
|
},
|
|
@@ -3524,13 +3670,13 @@ defineTool({
|
|
|
3524
3670
|
'Example: update_dev_task({ task_id: "task_hy1i2jdp\u2026", status: "done" }) \u2192 { data: { status: "done" } }'
|
|
3525
3671
|
].join("\n"),
|
|
3526
3672
|
input: {
|
|
3527
|
-
task_id:
|
|
3528
|
-
status:
|
|
3529
|
-
title:
|
|
3530
|
-
body:
|
|
3531
|
-
service_id:
|
|
3532
|
-
provider:
|
|
3533
|
-
permission_mode:
|
|
3673
|
+
task_id: z18.union([z18.number().int().positive(), z18.string()]).describe('Task \u2014 publicId ("task_\u2026") or numeric id.'),
|
|
3674
|
+
status: z18.enum(["idea", "done"]).optional().describe("The only two a person may set. The runner owns the rest of the lifecycle."),
|
|
3675
|
+
title: z18.string().min(1).max(200).optional().describe("New title."),
|
|
3676
|
+
body: z18.string().max(2e4).optional().describe("New prompt body."),
|
|
3677
|
+
service_id: z18.union([z18.number().int().positive(), z18.string()]).nullable().optional().describe("Pin to a dev box, or null to unpin it back to a loose idea."),
|
|
3678
|
+
provider: z18.enum(["claude", "codex", "opencode"]).nullable().optional().describe("Which agent runs this task, or null to leave it to the box."),
|
|
3679
|
+
permission_mode: z18.enum(["acceptEdits", "bypassPermissions", "default"]).optional().describe(
|
|
3534
3680
|
"How the agent may act. bypassPermissions asks for nothing \u2014 set it only on request."
|
|
3535
3681
|
)
|
|
3536
3682
|
},
|
|
@@ -3555,7 +3701,7 @@ defineTool({
|
|
|
3555
3701
|
});
|
|
3556
3702
|
|
|
3557
3703
|
// src/tools/resource-links.ts
|
|
3558
|
-
import { z as
|
|
3704
|
+
import { z as z19 } from "zod";
|
|
3559
3705
|
var RESOURCE_LINK_TYPES = [
|
|
3560
3706
|
"database",
|
|
3561
3707
|
"object_storage",
|
|
@@ -3616,7 +3762,7 @@ defineTool({
|
|
|
3616
3762
|
'Example: list_service_resources({ service_id: "svc_abc" }) \u2192 { items: [{ id: 7, resourceType: "database", resourceId: 42, alias: "APP_DB" }] }'
|
|
3617
3763
|
].join("\n"),
|
|
3618
3764
|
input: {
|
|
3619
|
-
service_id:
|
|
3765
|
+
service_id: z19.union([z19.number().int().positive(), z19.string()]).describe('Service \u2014 publicId ("svc_\u2026") or numeric id.')
|
|
3620
3766
|
},
|
|
3621
3767
|
handler: async (args, ctx) => {
|
|
3622
3768
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -3653,10 +3799,10 @@ defineTool({
|
|
|
3653
3799
|
'Example: link_resource_to_service({ service_id: "svc_abc", resource_type: "database", resource_id: 42, alias: "APP_DB" }) \u2192 { link: { id: 7, alias: "APP_DB" } }'
|
|
3654
3800
|
].join("\n"),
|
|
3655
3801
|
input: {
|
|
3656
|
-
service_id:
|
|
3657
|
-
resource_type:
|
|
3658
|
-
resource_id:
|
|
3659
|
-
alias:
|
|
3802
|
+
service_id: z19.union([z19.number().int().positive(), z19.string()]).describe('Consuming service \u2014 publicId ("svc_\u2026") or numeric id.'),
|
|
3803
|
+
resource_type: z19.enum(RESOURCE_LINK_TYPES).describe("Kind of resource being linked."),
|
|
3804
|
+
resource_id: z19.number().int().positive().describe("NUMERIC id of the resource (e.g. database.id) \u2014 not the publicId."),
|
|
3805
|
+
alias: z19.string().min(1).max(48).regex(
|
|
3660
3806
|
/^[A-Z][A-Z0-9_]*$/,
|
|
3661
3807
|
"Alias must be uppercase letters, digits and underscores, starting with a letter."
|
|
3662
3808
|
).describe('Uppercase env-var prefix, e.g. "APP_DB". Unique within the service.')
|
|
@@ -3692,8 +3838,8 @@ defineTool({
|
|
|
3692
3838
|
'Example: unlink_resource_from_service({ service_id: "svc_abc", link_id: 7 }) \u2192 { ok: true }'
|
|
3693
3839
|
].join("\n"),
|
|
3694
3840
|
input: {
|
|
3695
|
-
service_id:
|
|
3696
|
-
link_id:
|
|
3841
|
+
service_id: z19.union([z19.number().int().positive(), z19.string()]).describe('Service \u2014 publicId ("svc_\u2026") or numeric id.'),
|
|
3842
|
+
link_id: z19.number().int().positive().describe("Numeric linkId from list_service_resources (the link's own `id`).")
|
|
3697
3843
|
},
|
|
3698
3844
|
handler: async (args, ctx) => {
|
|
3699
3845
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -3705,7 +3851,7 @@ defineTool({
|
|
|
3705
3851
|
});
|
|
3706
3852
|
|
|
3707
3853
|
// src/tools/services.ts
|
|
3708
|
-
import { z as
|
|
3854
|
+
import { z as z20 } from "zod";
|
|
3709
3855
|
|
|
3710
3856
|
// src/lib/app-templates.ts
|
|
3711
3857
|
var MCP_APP_TEMPLATES = [
|
|
@@ -3957,16 +4103,16 @@ defineTool({
|
|
|
3957
4103
|
' - type: "web_service" | "private_service" | "worker" | "cron_job" | "static_site".',
|
|
3958
4104
|
" - dev_environment: include Dev Boxes in the results (excluded by default).",
|
|
3959
4105
|
"",
|
|
3960
|
-
"Returns: { items: Service[] } \u2014 each service includes id, publicId, name, type, status, projectId,
|
|
4106
|
+
"Returns: { items: Service[] } \u2014 each service includes id, publicId, name, type, status, projectId, branch, runtime, createdAt, and, for a source-built service, repoProvider/repoFullName/repoUrl (what it actually builds \u2014 the stored id alone names no repository).",
|
|
3961
4107
|
"",
|
|
3962
4108
|
'Example: list_services({ status: "failed" }) \u2192 only services that need attention.'
|
|
3963
4109
|
].join("\n"),
|
|
3964
4110
|
input: {
|
|
3965
|
-
project_id:
|
|
3966
|
-
environment_id:
|
|
3967
|
-
status:
|
|
3968
|
-
type:
|
|
3969
|
-
dev_environment:
|
|
4111
|
+
project_id: z20.union([z20.number().int().positive(), z20.string()]).optional().describe("Project filter \u2014 numeric id or publicId."),
|
|
4112
|
+
environment_id: z20.union([z20.number().int().positive(), z20.string()]).optional().describe("Environment filter \u2014 numeric id or publicId."),
|
|
4113
|
+
status: z20.enum(["active", "deploying", "suspended", "failed", "not_deployed"]).optional().describe("Filter by current runtime status."),
|
|
4114
|
+
type: z20.enum(["web_service", "private_service", "worker", "cron_job", "static_site"]).optional().describe("Filter by service type."),
|
|
4115
|
+
dev_environment: z20.boolean().optional().describe(
|
|
3970
4116
|
"Include agentic Dev Boxes in the results (excluded by default; see list_dev_environments)."
|
|
3971
4117
|
)
|
|
3972
4118
|
},
|
|
@@ -4007,7 +4153,7 @@ defineTool({
|
|
|
4007
4153
|
name: "create_service",
|
|
4008
4154
|
category: "services",
|
|
4009
4155
|
description: [
|
|
4010
|
-
|
|
4156
|
+
'Create a new service in a project. Source is either a connected git repo (github_repo "owner/name", or github_repo_id) OR a pre-built docker_image \u2014 never both.',
|
|
4011
4157
|
"",
|
|
4012
4158
|
"When to use: the user wants to deploy something new. For a one-command AI dev environment specifically, prefer create_dev_environment (it also attaches the /workspace volume and sets the MCP keys).",
|
|
4013
4159
|
"",
|
|
@@ -4020,7 +4166,8 @@ defineTool({
|
|
|
4020
4166
|
" - name: service name (1\u2013100 chars).",
|
|
4021
4167
|
' - type: "web_service" | "private_service" | "worker" | "cron_job" | "static_site".',
|
|
4022
4168
|
" - docker_image (optional): pre-built image ref to deploy instead of building from source.",
|
|
4023
|
-
|
|
4169
|
+
' - github_repo (optional): connect a connected GitHub repo by name \u2014 "owner/name". Preferred over github_repo_id: it is what you actually know, and a wrong name is refused here instead of failing the deploy.',
|
|
4170
|
+
" - github_repo_id (optional): the same repo by its HostStack numeric id (from list_github_repos). NOT GitHub's id. Mutually exclusive with github_repo.",
|
|
4024
4171
|
" - git_repo_url (optional): a PUBLIC https git URL, for a repo that is not connected through GitHub. Private repos do not work here \u2014 they need a provider connection, because this field carries no credentials and embedding them in the URL is rejected. Mutually exclusive with github_repo_id and docker_image.",
|
|
4025
4172
|
' - branch (optional): git branch (default "main").',
|
|
4026
4173
|
" - install_command / build_command / start_command (optional): build/run shell commands. start_command is required for web/private services without a docker_image.",
|
|
@@ -4036,36 +4183,42 @@ defineTool({
|
|
|
4036
4183
|
"",
|
|
4037
4184
|
"Returns: { service: Service, deployId: number | null }.",
|
|
4038
4185
|
"",
|
|
4039
|
-
'Example: create_service({ project_id: "prj_abc", name: "api", type: "web_service",
|
|
4186
|
+
'Example: create_service({ project_id: "prj_abc", name: "api", type: "web_service", github_repo: "acme/api" }) \u2192 { service: { publicId: "svc_\u2026", repoUrl: "https://github.com/acme/api" }, deployId: 1234 }',
|
|
4187
|
+
"Repo not listed by list_github_repos? Call sync_github_repos \u2014 a repository pushed since the last sync is invisible until then.",
|
|
4040
4188
|
'Example (one-click app): create_service({ project_id: "prj_abc", name: "blog", type: "web_service", template_id: "wordpress", docker_image: "wordpress:php8.3-apache", port: 80 }) \u2014 the wp-content volume, the www-data uid, the Apache scratch dirs and the managed MySQL come from the template id.'
|
|
4041
4189
|
].join("\n"),
|
|
4042
4190
|
input: {
|
|
4043
|
-
project_id:
|
|
4044
|
-
name:
|
|
4045
|
-
type:
|
|
4046
|
-
docker_image:
|
|
4191
|
+
project_id: z20.union([z20.number().int().positive(), z20.string()]).describe("Target project \u2014 numeric id or publicId."),
|
|
4192
|
+
name: z20.string().min(1).max(100).describe("Service name (1\u2013100 chars)."),
|
|
4193
|
+
type: z20.enum(SERVICE_TYPES).describe("Service type."),
|
|
4194
|
+
docker_image: z20.string().max(500).optional().describe(
|
|
4047
4195
|
"Pre-built APPLICATION image ref. Mutually exclusive with github_repo_id. Not for databases \u2014 use create_database for postgres/redis/mysql/mariadb/mongodb."
|
|
4048
4196
|
),
|
|
4049
|
-
|
|
4050
|
-
|
|
4197
|
+
github_repo: z20.string().min(1).max(255).optional().describe(
|
|
4198
|
+
'Connected GitHub repo as "owner/name" (see list_github_repos). Resolved server-side. Mutually exclusive with github_repo_id and docker_image.'
|
|
4199
|
+
),
|
|
4200
|
+
github_repo_id: z20.number().int().positive().optional().describe(
|
|
4201
|
+
"HostStack row id of a connected GitHub repo (list_github_repos) \u2014 not GitHub's own id. Prefer github_repo. Mutually exclusive with docker_image."
|
|
4202
|
+
),
|
|
4203
|
+
git_repo_url: z20.string().max(2048).optional().describe(
|
|
4051
4204
|
"Public https git URL. No embedded credentials \u2014 private repos need a provider connection. Mutually exclusive with github_repo_id and docker_image."
|
|
4052
4205
|
),
|
|
4053
|
-
branch:
|
|
4054
|
-
install_command:
|
|
4055
|
-
build_command:
|
|
4056
|
-
start_command:
|
|
4057
|
-
cron_schedule:
|
|
4058
|
-
publish_path:
|
|
4059
|
-
runtime:
|
|
4060
|
-
port:
|
|
4206
|
+
branch: z20.string().max(200).optional().describe('Git branch (default "main").'),
|
|
4207
|
+
install_command: z20.string().max(1e3).optional().describe("Install shell command."),
|
|
4208
|
+
build_command: z20.string().max(1e3).optional().describe("Build shell command."),
|
|
4209
|
+
start_command: z20.string().max(1e3).optional().describe("Start shell command (required for web/private services without an image)."),
|
|
4210
|
+
cron_schedule: z20.string().max(100).optional().describe("Cron expression \u2014 required for cron_job."),
|
|
4211
|
+
publish_path: z20.string().max(500).optional().describe("Static-site output dir."),
|
|
4212
|
+
runtime: z20.string().max(50).optional().describe("Runtime hint (node/bun/python/\u2026)."),
|
|
4213
|
+
port: z20.number().int().min(1).max(65535).optional().describe(
|
|
4061
4214
|
"Listen port, for a prebuilt image whose port is fixed by the image. Omit for a source-built service \u2014 it binds the injected $PORT."
|
|
4062
4215
|
),
|
|
4063
|
-
template_id:
|
|
4216
|
+
template_id: z20.string().max(64).optional().describe(
|
|
4064
4217
|
"Quickstart template id from list_templates. Its volumes, scratch dirs, uid, secrets and companion database are resolved server-side; for an image template also pass its docker_image and port."
|
|
4065
4218
|
),
|
|
4066
|
-
plan:
|
|
4067
|
-
environment_id:
|
|
4068
|
-
auto_deploy:
|
|
4219
|
+
plan: z20.enum(SERVICE_PLANS).optional().describe('Service size (default "micro").'),
|
|
4220
|
+
environment_id: z20.union([z20.number().int().positive(), z20.string()]).optional().describe("Bind to a specific environment; defaults to Production."),
|
|
4221
|
+
auto_deploy: z20.boolean().optional().describe("Trigger the first deploy immediately (default true)."),
|
|
4069
4222
|
machine: machineInput
|
|
4070
4223
|
},
|
|
4071
4224
|
handler: async (args, ctx) => {
|
|
@@ -4080,6 +4233,7 @@ defineTool({
|
|
|
4080
4233
|
projectId
|
|
4081
4234
|
};
|
|
4082
4235
|
if (args.docker_image !== void 0) input.dockerImage = args.docker_image;
|
|
4236
|
+
if (args.github_repo !== void 0) input.githubRepo = args.github_repo;
|
|
4083
4237
|
if (args.github_repo_id !== void 0) input.githubRepoId = args.github_repo_id;
|
|
4084
4238
|
if (args.git_repo_url !== void 0) input.gitRepoUrl = args.git_repo_url;
|
|
4085
4239
|
if (args.branch !== void 0) input.branch = args.branch;
|
|
@@ -4137,18 +4291,18 @@ defineTool({
|
|
|
4137
4291
|
'Example: create_dev_environment({ project_id: "prj_abc", name: "scratch", hoststack_api_key: "hs_live_\u2026" })'
|
|
4138
4292
|
].join("\n"),
|
|
4139
4293
|
input: {
|
|
4140
|
-
project_id:
|
|
4141
|
-
name:
|
|
4142
|
-
plan:
|
|
4294
|
+
project_id: z20.union([z20.number().int().positive(), z20.string()]).describe("Target project \u2014 numeric id or publicId."),
|
|
4295
|
+
name: z20.string().min(1).max(100).optional().describe('Service name (default "dev-environment").'),
|
|
4296
|
+
plan: z20.enum(SERVICE_PLANS).optional().describe(
|
|
4143
4297
|
'Box size (default "standard" \u2014 2 GB, the OOM-safe floor; a smaller plan is clamped up to "standard").'
|
|
4144
4298
|
),
|
|
4145
|
-
disk_gb:
|
|
4146
|
-
hoststack_api_key:
|
|
4147
|
-
poststack_api_key:
|
|
4148
|
-
repo_url:
|
|
4299
|
+
disk_gb: z20.number().int().min(10).max(10240).optional().describe("/workspace volume size in GB (default 10, min 10, max 10240)."),
|
|
4300
|
+
hoststack_api_key: z20.string().optional().describe("Value for HOSTSTACK_API_KEY (enables the hoststack MCP in-container)."),
|
|
4301
|
+
poststack_api_key: z20.string().optional().describe("Value for POSTSTACK_API_KEY (enables the poststack MCP in-container)."),
|
|
4302
|
+
repo_url: z20.string().max(500).optional().describe(
|
|
4149
4303
|
"Clone this git URL into /workspace on first boot (HTTPS, or SSH once a key is set)."
|
|
4150
4304
|
),
|
|
4151
|
-
branch:
|
|
4305
|
+
branch: z20.string().max(200).optional().describe("Branch to clone (with repo_url)."),
|
|
4152
4306
|
machine: machineInput
|
|
4153
4307
|
},
|
|
4154
4308
|
handler: async (args, ctx) => {
|
|
@@ -4265,9 +4419,9 @@ defineTool({
|
|
|
4265
4419
|
'Example: spin_up_dev_environment({ service_id: "svc_api" }) \u2192 a dev box running a clone of the api service (repo + env-vars + cloned DB) with a public dev URL.'
|
|
4266
4420
|
].join("\n"),
|
|
4267
4421
|
input: {
|
|
4268
|
-
service_id:
|
|
4269
|
-
include_database_clone:
|
|
4270
|
-
name:
|
|
4422
|
+
service_id: z20.union([z20.number().int().positive(), z20.string()]).describe("Source service to debug \u2014 numeric id or publicId."),
|
|
4423
|
+
include_database_clone: z20.boolean().optional().describe("Clone the linked database so the app runs on copied data (default true)."),
|
|
4424
|
+
name: z20.string().min(1).max(100).optional().describe('Dev box name (default "<source>-dev").')
|
|
4271
4425
|
},
|
|
4272
4426
|
handler: async (args, ctx) => {
|
|
4273
4427
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -4308,7 +4462,7 @@ defineTool({
|
|
|
4308
4462
|
'Example: delete_dev_environment({ service_id: "svc_api_dev" }) \u2192 removes the dev box, its cloned database, and the /workspace volume.'
|
|
4309
4463
|
].join("\n"),
|
|
4310
4464
|
input: {
|
|
4311
|
-
service_id:
|
|
4465
|
+
service_id: z20.union([z20.number().int().positive(), z20.string()]).describe("The dev box to tear down \u2014 numeric id or publicId.")
|
|
4312
4466
|
},
|
|
4313
4467
|
handler: async (args, ctx) => {
|
|
4314
4468
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -4346,8 +4500,8 @@ defineTool({
|
|
|
4346
4500
|
'Example: resize_dev_environment({ service_id: "svc_skyskraber_dev", size: "large" }) \u2192 bumps the box to the large tier, applied live.'
|
|
4347
4501
|
].join("\n"),
|
|
4348
4502
|
input: {
|
|
4349
|
-
service_id:
|
|
4350
|
-
size:
|
|
4503
|
+
service_id: z20.union([z20.number().int().positive(), z20.string()]).describe("The box to resize \u2014 numeric id or publicId."),
|
|
4504
|
+
size: z20.enum(SERVICE_PLANS).describe(
|
|
4351
4505
|
'Target size tier (service catalog size, e.g. "standard", "large", "xlarge").'
|
|
4352
4506
|
)
|
|
4353
4507
|
},
|
|
@@ -4484,7 +4638,7 @@ defineTool({
|
|
|
4484
4638
|
"Inputs:",
|
|
4485
4639
|
' - name (optional): the Dev Box name. Omit it and the box is named after its source (the repo name, or "dev-box" when blank), with a numeric suffix if that name is taken \u2014 so "give me a dev box" needs no invented name.',
|
|
4486
4640
|
' - source_kind: "github_repo" (clone a connected repo \u2014 needs github_repo_id), "url" (clone any http(s) git URL \u2014 needs clone_url), or "blank" (empty box).',
|
|
4487
|
-
" - github_repo_id (for github_repo):
|
|
4641
|
+
" - github_repo_id (for github_repo): HostStack row id of a connected GitHub repo \u2014 call list_github_repos for it. It is NOT the id GitHub shows, and it cannot be guessed.",
|
|
4488
4642
|
" - clone_url (for url): an http(s) git clone URL.",
|
|
4489
4643
|
" - branch (optional): branch to clone.",
|
|
4490
4644
|
' - databases (optional): companion services to attach \u2014 any of "postgres", "redis", "meilisearch".',
|
|
@@ -4494,24 +4648,26 @@ defineTool({
|
|
|
4494
4648
|
"",
|
|
4495
4649
|
"Returns: { service, devUrl, deployId } \u2014 deploying. Once live: open the Terminal tab, run `claude`, start the dev server on $PORT, view at https://<devUrl>. Tear down with delete_dev_environment.",
|
|
4496
4650
|
"",
|
|
4497
|
-
'Example: create_standalone_dev_environment({ name: "app-dev", source_kind: "github_repo", github_repo_id: 42, databases: ["postgres","redis"] })'
|
|
4651
|
+
'Example: list_github_repos({ q: "app" }) \u2192 id 42, then create_standalone_dev_environment({ name: "app-dev", source_kind: "github_repo", github_repo_id: 42, databases: ["postgres","redis"] })'
|
|
4498
4652
|
].join("\n"),
|
|
4499
4653
|
input: {
|
|
4500
|
-
name:
|
|
4654
|
+
name: z20.string().min(1).max(100).optional().describe(
|
|
4501
4655
|
'Dev Box name. Omit to have it named after the source (the repo name, or "dev-box" for a blank one), de-duplicated against existing boxes.'
|
|
4502
4656
|
),
|
|
4503
|
-
source_kind:
|
|
4504
|
-
github_repo_id:
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4657
|
+
source_kind: z20.enum(["github_repo", "url", "blank"]).describe("Where the code comes from."),
|
|
4658
|
+
github_repo_id: z20.number().int().positive().optional().describe(
|
|
4659
|
+
'Connected GitHub repo id from list_github_repos (required when source_kind="github_repo").'
|
|
4660
|
+
),
|
|
4661
|
+
clone_url: z20.string().url().optional().describe('http(s) git clone URL (required when source_kind="url").'),
|
|
4662
|
+
branch: z20.string().min(1).max(255).optional().describe("Branch to clone."),
|
|
4663
|
+
databases: z20.array(z20.enum(["postgres", "redis", "meilisearch"])).optional().describe("Companion services to attach (fresh + empty)."),
|
|
4664
|
+
plan: z20.enum(SERVICE_PLANS).optional().describe(
|
|
4509
4665
|
'Box size (default "standard" \u2014 2 GB; a smaller plan is floored to "standard").'
|
|
4510
4666
|
),
|
|
4511
|
-
agent_accounts:
|
|
4512
|
-
|
|
4513
|
-
provider:
|
|
4514
|
-
account_id:
|
|
4667
|
+
agent_accounts: z20.array(
|
|
4668
|
+
z20.object({
|
|
4669
|
+
provider: z20.enum(["claude", "codex", "opencode"]),
|
|
4670
|
+
account_id: z20.number().int().positive()
|
|
4515
4671
|
})
|
|
4516
4672
|
).max(3).optional().describe(
|
|
4517
4673
|
"Bind saved agent logins by account id per provider. Omit to inherit the box owner's default logins automatically."
|
|
@@ -4584,12 +4740,12 @@ defineTool({
|
|
|
4584
4740
|
"Inputs:",
|
|
4585
4741
|
' - service_id: publicId of the service (e.g. "svc_abc123").',
|
|
4586
4742
|
"",
|
|
4587
|
-
'Returns: { service: Service, config: ServiceConfig } \u2014 service has type/status/runtime/
|
|
4743
|
+
'Returns: { service: Service, config: ServiceConfig } \u2014 service has type/status/runtime/branch/autoDeploy/region/plan/timestamps plus repoProvider/repoFullName/repoUrl for a source-built service (absent when it deploys a prebuilt image); config has memoryMb, cpuShares, diskSizeGb, port, protocol, healthCheckEnabled, healthCheckInterval, healthCheckTimeout, healthCheckGracePeriodSec, allowSearchIndexing, restartPolicy, deployStrategy ("rolling" | "recreate"), preDeployCommand, min/maxInstances, scale thresholds.',
|
|
4588
4744
|
"",
|
|
4589
4745
|
'Example: get_service({ service_id: "svc_abc" }) \u2192 { service: { type: "web", status: "running", \u2026 }, config: { healthCheckGracePeriodSec: 120, \u2026 } }'
|
|
4590
4746
|
].join("\n"),
|
|
4591
4747
|
input: {
|
|
4592
|
-
service_id:
|
|
4748
|
+
service_id: z20.string().describe("Service publicId (e.g. svc_abc123).")
|
|
4593
4749
|
},
|
|
4594
4750
|
handler: async (args, ctx) => {
|
|
4595
4751
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -4621,7 +4777,7 @@ defineTool({
|
|
|
4621
4777
|
'Example: get_service_metrics({ service_id: "svc_abc" }) \u2192 { metrics: { cpu: 0.42, memory: 0.71, \u2026 } }'
|
|
4622
4778
|
].join("\n"),
|
|
4623
4779
|
input: {
|
|
4624
|
-
service_id:
|
|
4780
|
+
service_id: z20.string().describe("Service publicId.")
|
|
4625
4781
|
},
|
|
4626
4782
|
handler: async (args, ctx) => {
|
|
4627
4783
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -4654,9 +4810,9 @@ defineTool({
|
|
|
4654
4810
|
'Example: get_service_metrics_history({ service_id: "svc_abc", from: "-1h" }) \u2192 ~120 points for the last hour.'
|
|
4655
4811
|
].join("\n"),
|
|
4656
4812
|
input: {
|
|
4657
|
-
service_id:
|
|
4658
|
-
from:
|
|
4659
|
-
to:
|
|
4813
|
+
service_id: z20.string().describe("Service publicId."),
|
|
4814
|
+
from: z20.string().optional().describe('ISO-8601 lower bound or relative offset (e.g. "-1h", "-2d").'),
|
|
4815
|
+
to: z20.string().optional().describe("ISO-8601 upper bound; defaults to now.")
|
|
4660
4816
|
},
|
|
4661
4817
|
handler: async (args, ctx) => {
|
|
4662
4818
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -4700,8 +4856,8 @@ defineTool({
|
|
|
4700
4856
|
'Example: update_service({ service_id: "svc_abc", name: "api-prod" }) \u2192 { service: { name: "api-prod", \u2026 } }'
|
|
4701
4857
|
].join("\n"),
|
|
4702
4858
|
input: {
|
|
4703
|
-
service_id:
|
|
4704
|
-
name:
|
|
4859
|
+
service_id: z20.string().describe("Service publicId."),
|
|
4860
|
+
name: z20.string().min(1).max(60).describe("New service name (1\u201360 chars).")
|
|
4705
4861
|
},
|
|
4706
4862
|
handler: async (args, ctx) => {
|
|
4707
4863
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -4758,54 +4914,54 @@ defineTool({
|
|
|
4758
4914
|
'Example: update_service_config({ service_id: "svc_abc", health_check_grace_period_sec: 180 }) \u2192 { config: { healthCheckGracePeriodSec: 180, \u2026 } }'
|
|
4759
4915
|
].join("\n"),
|
|
4760
4916
|
input: {
|
|
4761
|
-
service_id:
|
|
4762
|
-
install_command:
|
|
4763
|
-
build_command:
|
|
4764
|
-
start_command:
|
|
4765
|
-
branch:
|
|
4766
|
-
root_directory:
|
|
4767
|
-
dockerfile_path:
|
|
4768
|
-
auto_deploy:
|
|
4769
|
-
health_check_path:
|
|
4770
|
-
health_check_enabled:
|
|
4771
|
-
allow_search_indexing:
|
|
4917
|
+
service_id: z20.string().describe("Service publicId."),
|
|
4918
|
+
install_command: z20.string().nullable().optional().describe("Install shell command. Null clears."),
|
|
4919
|
+
build_command: z20.string().nullable().optional().describe("Build shell command. Null clears."),
|
|
4920
|
+
start_command: z20.string().nullable().optional().describe("Start shell command. Null clears."),
|
|
4921
|
+
branch: z20.string().optional().describe("Git branch to track."),
|
|
4922
|
+
root_directory: z20.string().optional().describe("Build context root."),
|
|
4923
|
+
dockerfile_path: z20.string().nullable().optional().describe("Path to Dockerfile relative to root. Null clears."),
|
|
4924
|
+
auto_deploy: z20.boolean().optional().describe("Auto-deploy on push."),
|
|
4925
|
+
health_check_path: z20.string().nullable().optional().describe('HTTP health-check path (e.g. "/health"). Null = TCP-only check.'),
|
|
4926
|
+
health_check_enabled: z20.boolean().optional().describe("Toggle health checking on/off."),
|
|
4927
|
+
allow_search_indexing: z20.boolean().optional().describe(
|
|
4772
4928
|
"Let search engines index the free *.hoststack.dev platform URL (off by default). Custom domains are always indexable. Applies on the next deploy."
|
|
4773
4929
|
),
|
|
4774
|
-
health_check_interval:
|
|
4775
|
-
health_check_timeout:
|
|
4776
|
-
health_check_grace_period_sec:
|
|
4930
|
+
health_check_interval: z20.number().int().min(5).max(300).optional().describe("How often the check runs, in seconds (5\u2013300)."),
|
|
4931
|
+
health_check_timeout: z20.number().int().min(1).max(60).optional().describe("Single-attempt timeout in seconds (1\u201360)."),
|
|
4932
|
+
health_check_grace_period_sec: z20.number().int().min(1).max(1800).optional().describe(
|
|
4777
4933
|
"Startup grace period in seconds (1\u20131800). Raise this if the app needs more time to boot before health checks start counting failures."
|
|
4778
4934
|
),
|
|
4779
|
-
memory_mb:
|
|
4780
|
-
cpu_shares:
|
|
4781
|
-
disk_size_gb:
|
|
4782
|
-
port:
|
|
4783
|
-
protocol:
|
|
4784
|
-
restart_policy:
|
|
4785
|
-
deploy_strategy:
|
|
4935
|
+
memory_mb: z20.number().int().min(128).max(16384).optional().describe("Container memory cap in MB (128\u201316384)."),
|
|
4936
|
+
cpu_shares: z20.number().int().min(128).max(4096).optional().describe("Relative CPU weight (128\u20134096)."),
|
|
4937
|
+
disk_size_gb: z20.number().int().min(1).max(100).optional().describe("Ephemeral disk size in GB (1\u2013100)."),
|
|
4938
|
+
port: z20.number().int().min(1).max(65535).optional().describe("Container port the platform forwards traffic to."),
|
|
4939
|
+
protocol: z20.enum(["http", "tcp"]).optional().describe("Traffic protocol."),
|
|
4940
|
+
restart_policy: z20.enum(["always", "on-failure", "no"]).optional().describe("Docker restart policy."),
|
|
4941
|
+
deploy_strategy: z20.enum(["rolling", "recreate"]).optional().describe(
|
|
4786
4942
|
'How a deploy replaces the container. "rolling" (default) = start new, wait for healthy, switch traffic, stop old (zero downtime). "recreate" = stop old first, then start new (brief outage) \u2014 required for a container holding an exclusive lock on a mounted volume, which cannot deploy at all under rolling.'
|
|
4787
4943
|
),
|
|
4788
|
-
pre_deploy_command:
|
|
4789
|
-
instance_count:
|
|
4790
|
-
min_instances:
|
|
4791
|
-
max_instances:
|
|
4792
|
-
scale_cpu_threshold:
|
|
4793
|
-
scale_memory_threshold:
|
|
4794
|
-
scale_requests_per_sec_threshold:
|
|
4795
|
-
build_timeout_seconds:
|
|
4796
|
-
plan:
|
|
4944
|
+
pre_deploy_command: z20.string().optional().describe("Shell command run before the new release accepts traffic."),
|
|
4945
|
+
instance_count: z20.number().int().positive().max(50).optional().describe("Pin min and max instances to this value (1\u201350)."),
|
|
4946
|
+
min_instances: z20.number().int().min(0).max(50).optional().describe("Autoscale lower bound. Use with max_instances for a range."),
|
|
4947
|
+
max_instances: z20.number().int().min(1).max(50).optional().describe("Autoscale upper bound. Use with min_instances for a range."),
|
|
4948
|
+
scale_cpu_threshold: z20.number().int().min(10).max(100).optional().describe("Autoscale CPU trigger percentage (10\u2013100)."),
|
|
4949
|
+
scale_memory_threshold: z20.number().int().min(10).max(100).optional().describe("Autoscale memory trigger percentage (10\u2013100)."),
|
|
4950
|
+
scale_requests_per_sec_threshold: z20.number().int().min(1).max(1e4).nullable().optional().describe("Autoscale request-rate trigger (1\u201310000 req/s). null stops using it."),
|
|
4951
|
+
build_timeout_seconds: z20.number().int().min(60).max(1800).nullable().optional().describe("Kill a build that runs longer than this (60\u20131800s). null = default."),
|
|
4952
|
+
plan: z20.enum(SERVICE_PLANS).optional().describe(
|
|
4797
4953
|
"Move to a different size tier. memory_mb and cpu_shares are clamped to the tier, so this is what raises the ceiling. Changes cost."
|
|
4798
4954
|
),
|
|
4799
|
-
docker_image:
|
|
4955
|
+
docker_image: z20.string().max(500).nullable().optional().describe(
|
|
4800
4956
|
"Repoint an image-backed service. Applies on the next deploy. null clears it."
|
|
4801
4957
|
),
|
|
4802
|
-
cron_schedule:
|
|
4803
|
-
publish_path:
|
|
4804
|
-
runtime:
|
|
4805
|
-
log_filter_rules:
|
|
4806
|
-
|
|
4807
|
-
pattern:
|
|
4808
|
-
action:
|
|
4958
|
+
cron_schedule: z20.string().max(100).nullable().optional().describe("Cron expression for a cron_job service. null clears it."),
|
|
4959
|
+
publish_path: z20.string().max(500).nullable().optional().describe('Static-site output directory, e.g. "dist". null clears it.'),
|
|
4960
|
+
runtime: z20.string().max(50).optional().describe("Runtime hint (node/bun/python/\u2026). Normally auto-detected."),
|
|
4961
|
+
log_filter_rules: z20.array(
|
|
4962
|
+
z20.object({
|
|
4963
|
+
pattern: z20.string().min(1).max(200),
|
|
4964
|
+
action: z20.enum(["drop", "downgrade"])
|
|
4809
4965
|
})
|
|
4810
4966
|
).max(50).optional().describe(
|
|
4811
4967
|
"Runtime-log filter rules. Empty array [] clears all rules. Each pattern is case-insensitive substring match against the message."
|
|
@@ -4913,7 +5069,7 @@ defineTool({
|
|
|
4913
5069
|
'Example: suspend_service({ service_id: "svc_dev" }) \u2192 { ok: true }'
|
|
4914
5070
|
].join("\n"),
|
|
4915
5071
|
input: {
|
|
4916
|
-
service_id:
|
|
5072
|
+
service_id: z20.string().describe("Service publicId.")
|
|
4917
5073
|
},
|
|
4918
5074
|
handler: async (args, ctx) => {
|
|
4919
5075
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -4937,7 +5093,7 @@ defineTool({
|
|
|
4937
5093
|
'Example: resume_service({ service_id: "svc_dev" }) \u2192 { ok: true }'
|
|
4938
5094
|
].join("\n"),
|
|
4939
5095
|
input: {
|
|
4940
|
-
service_id:
|
|
5096
|
+
service_id: z20.string().describe("Service publicId.")
|
|
4941
5097
|
},
|
|
4942
5098
|
handler: async (args, ctx) => {
|
|
4943
5099
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -4963,7 +5119,7 @@ defineTool({
|
|
|
4963
5119
|
'Example: delete_service({ service_id: "svc_abandoned" }) \u2192 { ok: true }'
|
|
4964
5120
|
].join("\n"),
|
|
4965
5121
|
input: {
|
|
4966
|
-
service_id:
|
|
5122
|
+
service_id: z20.string().describe("Service publicId.")
|
|
4967
5123
|
},
|
|
4968
5124
|
handler: async (args, ctx) => {
|
|
4969
5125
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -5002,16 +5158,16 @@ defineTool({
|
|
|
5002
5158
|
' - Just count error lines without fetching them: get_service_logs({ service_id: "svc_abc", level: "error", since: "-5m", count_only: true }) \u2192 { count: 47 }'
|
|
5003
5159
|
].join("\n"),
|
|
5004
5160
|
input: {
|
|
5005
|
-
service_id:
|
|
5006
|
-
lines:
|
|
5007
|
-
since:
|
|
5008
|
-
until:
|
|
5009
|
-
stream:
|
|
5010
|
-
level:
|
|
5161
|
+
service_id: z20.string().describe("Service publicId."),
|
|
5162
|
+
lines: z20.number().int().positive().max(1e3).optional().describe("Tail size; default 200, hard cap 1000."),
|
|
5163
|
+
since: z20.string().optional().describe('ISO-8601 timestamp or relative offset (e.g. "-5m", "-1h").'),
|
|
5164
|
+
until: z20.string().optional().describe("ISO-8601 timestamp or relative offset upper bound."),
|
|
5165
|
+
stream: z20.enum(["stdout", "stderr"]).optional().describe("Restrict to one stream."),
|
|
5166
|
+
level: z20.enum(["stdout", "stderr", "trace", "debug", "info", "warn", "error", "fatal"]).optional().describe(
|
|
5011
5167
|
"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)."
|
|
5012
5168
|
),
|
|
5013
|
-
search:
|
|
5014
|
-
count_only:
|
|
5169
|
+
search: z20.string().max(100).optional().describe("Case-insensitive substring filter."),
|
|
5170
|
+
count_only: z20.boolean().optional().describe("When true, return only { count } \u2014 skips the log payload.")
|
|
5015
5171
|
},
|
|
5016
5172
|
handler: async (args, ctx) => {
|
|
5017
5173
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -5060,14 +5216,14 @@ defineTool({
|
|
|
5060
5216
|
'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 } } }.'
|
|
5061
5217
|
].join("\n"),
|
|
5062
5218
|
input: {
|
|
5063
|
-
service_ids:
|
|
5064
|
-
lines_per_service:
|
|
5065
|
-
since:
|
|
5066
|
-
until:
|
|
5067
|
-
stream:
|
|
5068
|
-
level:
|
|
5069
|
-
search:
|
|
5070
|
-
count_only:
|
|
5219
|
+
service_ids: z20.array(z20.string()).min(1).max(10).describe("Service publicIds (1\u201310). Hard cap 10 to bound parallel work."),
|
|
5220
|
+
lines_per_service: z20.number().int().positive().max(500).optional().describe("Tail size per service; default 100, hard cap 500."),
|
|
5221
|
+
since: z20.string().optional().describe('ISO-8601 timestamp or relative offset (e.g. "-5m", "-1h").'),
|
|
5222
|
+
until: z20.string().optional().describe("ISO-8601 timestamp or relative offset upper bound."),
|
|
5223
|
+
stream: z20.enum(["stdout", "stderr"]).optional().describe("Restrict to one stream."),
|
|
5224
|
+
level: z20.enum(["stdout", "stderr", "trace", "debug", "info", "warn", "error", "fatal"]).optional().describe("Structured log level filter (same as get_service_logs)."),
|
|
5225
|
+
search: z20.string().max(100).optional().describe("Case-insensitive substring filter."),
|
|
5226
|
+
count_only: z20.boolean().optional().describe("When true, return only counts per service \u2014 skips the log payload.")
|
|
5071
5227
|
},
|
|
5072
5228
|
handler: async (args, ctx) => {
|
|
5073
5229
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -5113,7 +5269,7 @@ defineTool({
|
|
|
5113
5269
|
});
|
|
5114
5270
|
|
|
5115
5271
|
// src/tools/uptime.ts
|
|
5116
|
-
import { z as
|
|
5272
|
+
import { z as z21 } from "zod";
|
|
5117
5273
|
var STATUS_MEANING = [
|
|
5118
5274
|
"Status vocabulary: `up` answering as expected \xB7 `down` failed the threshold and an alert is open \xB7 `unknown` not probed yet \xB7 `unresolvable` the service has no active domain to request \xB7 `paused` the service is not meant to be answering (suspended, mid-deploy, never deployed) OR it sleeps when idle and probing it would keep it awake."
|
|
5119
5275
|
].join("\n");
|
|
@@ -5135,7 +5291,7 @@ defineTool({
|
|
|
5135
5291
|
"",
|
|
5136
5292
|
"Example: get_uptime_check({ serviceId: 48 }) \u2192 { check: { path: '/healthz', status: 'down', consecutiveFailures: 5, lastError: 'No response within 10000ms', lastChangedAt: '2026-08-21T04:12:00Z', lastTargetUrl: 'https://grundfast.dk/healthz' } }."
|
|
5137
5293
|
].join("\n"),
|
|
5138
|
-
input: { serviceId:
|
|
5294
|
+
input: { serviceId: z21.number().int().positive() },
|
|
5139
5295
|
handler: async (args, ctx) => {
|
|
5140
5296
|
const teamId = await ctx.resolveTeamId();
|
|
5141
5297
|
const response = await ctx.api.get(
|
|
@@ -5181,14 +5337,14 @@ defineTool({
|
|
|
5181
5337
|
"Example: set_uptime_check({ serviceId: 48, path: '/healthz', intervalSeconds: 60, failureThreshold: 3 }) \u2192 { check: { status: 'unknown', \u2026 } }."
|
|
5182
5338
|
].join("\n"),
|
|
5183
5339
|
input: {
|
|
5184
|
-
serviceId:
|
|
5185
|
-
enabled:
|
|
5186
|
-
path:
|
|
5187
|
-
method:
|
|
5188
|
-
expectedStatus:
|
|
5189
|
-
timeoutMs:
|
|
5190
|
-
intervalSeconds:
|
|
5191
|
-
failureThreshold:
|
|
5340
|
+
serviceId: z21.number().int().positive(),
|
|
5341
|
+
enabled: z21.boolean().optional(),
|
|
5342
|
+
path: z21.string().max(500).optional().describe('Must start with /. Default "/".'),
|
|
5343
|
+
method: z21.enum(["GET", "HEAD"]).optional(),
|
|
5344
|
+
expectedStatus: z21.number().int().min(100).max(599).optional(),
|
|
5345
|
+
timeoutMs: z21.number().int().min(1e3).max(6e4).optional(),
|
|
5346
|
+
intervalSeconds: z21.number().int().min(30).max(3600).optional(),
|
|
5347
|
+
failureThreshold: z21.number().int().min(1).max(10).optional()
|
|
5192
5348
|
},
|
|
5193
5349
|
handler: async (args, ctx) => {
|
|
5194
5350
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -5219,7 +5375,7 @@ defineTool({
|
|
|
5219
5375
|
"",
|
|
5220
5376
|
"Example: delete_uptime_check({ serviceId: 48 }) \u2192 { success: true }."
|
|
5221
5377
|
].join("\n"),
|
|
5222
|
-
input: { serviceId:
|
|
5378
|
+
input: { serviceId: z21.number().int().positive() },
|
|
5223
5379
|
handler: async (args, ctx) => {
|
|
5224
5380
|
const teamId = await ctx.resolveTeamId();
|
|
5225
5381
|
await ctx.api.delete(`/api/services/${teamId}/${args.serviceId}/uptime-check`);
|
|
@@ -5228,7 +5384,7 @@ defineTool({
|
|
|
5228
5384
|
});
|
|
5229
5385
|
|
|
5230
5386
|
// src/tools/volumes.ts
|
|
5231
|
-
import { z as
|
|
5387
|
+
import { z as z22 } from "zod";
|
|
5232
5388
|
var MIN_VOLUME_SIZE_GB = 10;
|
|
5233
5389
|
defineTool({
|
|
5234
5390
|
name: "list_volumes",
|
|
@@ -5241,12 +5397,14 @@ defineTool({
|
|
|
5241
5397
|
"Inputs:",
|
|
5242
5398
|
" - service_id: publicId of the service.",
|
|
5243
5399
|
"",
|
|
5244
|
-
"Returns: { items: Volume[] } \u2014 each entry has id, publicId, name, mountPath, sizeGb, status (pending|active|deleting), createdAt, updatedAt.",
|
|
5400
|
+
"Returns: { items: Volume[] } \u2014 each entry has id, publicId, name, mountPath, sizeGb, status (pending|active|deleting), backupEnabled, createdAt, updatedAt.",
|
|
5401
|
+
"",
|
|
5402
|
+
"`backupEnabled` says backups are being TAKEN, not that any exist. Call list_volume_backups to see which archives are actually restorable \u2014 a volume can be enabled and have nothing behind it (nothing has completed yet, or the host has no upload grant and is writing tars onto the very disk it is backing up, which is not a copy of anything). Turn it on with update_volume({ backup_enabled: true }).",
|
|
5245
5403
|
"",
|
|
5246
|
-
'Example: list_volumes({ service_id: "svc_abc" }) \u2192 { items: [{ name: "data", mountPath: "/var/data", sizeGb: 10, status: "active" }] }'
|
|
5404
|
+
'Example: list_volumes({ service_id: "svc_abc" }) \u2192 { items: [{ name: "data", mountPath: "/var/data", sizeGb: 10, status: "active", backupEnabled: false }] }'
|
|
5247
5405
|
].join("\n"),
|
|
5248
5406
|
input: {
|
|
5249
|
-
service_id:
|
|
5407
|
+
service_id: z22.string().describe("Service publicId (e.g. svc_abc123).")
|
|
5250
5408
|
},
|
|
5251
5409
|
handler: async (args, ctx) => {
|
|
5252
5410
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -5275,15 +5433,15 @@ defineTool({
|
|
|
5275
5433
|
'Example: create_volume({ service_id: "svc_abc", name: "data", mount_path: "/var/data", size_gb: 10 }) \u2192 { volume: { name: "data", mountPath: "/var/data", sizeGb: 10, status: "pending" } }'
|
|
5276
5434
|
].join("\n"),
|
|
5277
5435
|
input: {
|
|
5278
|
-
service_id:
|
|
5279
|
-
name:
|
|
5280
|
-
mount_path:
|
|
5436
|
+
service_id: z22.string().describe("Service publicId."),
|
|
5437
|
+
name: z22.string().min(1).max(64).regex(/^[a-z0-9-]+$/).describe("Volume name (lowercase alphanumeric + hyphens)."),
|
|
5438
|
+
mount_path: z22.string().startsWith("/").max(500).describe("In-container mount path (absolute)."),
|
|
5281
5439
|
// 10 GB is the real floor: the block-storage backend rejects anything
|
|
5282
5440
|
// smaller. Advertising 1 GB here (and defaulting to it) meant taking the
|
|
5283
5441
|
// defaults produced a volume that provisioned with `Hetzner API error:
|
|
5284
5442
|
// 422` on the NEXT deploy, with nothing tying the failure back to the
|
|
5285
5443
|
// size. Reject it at the call instead.
|
|
5286
|
-
size_gb:
|
|
5444
|
+
size_gb: z22.number().int().min(MIN_VOLUME_SIZE_GB).max(100).optional().describe(
|
|
5287
5445
|
`Disk size in GB (minimum ${MIN_VOLUME_SIZE_GB}, default ${MIN_VOLUME_SIZE_GB}, max 100 via MCP).`
|
|
5288
5446
|
)
|
|
5289
5447
|
},
|
|
@@ -5315,18 +5473,20 @@ defineTool({
|
|
|
5315
5473
|
" - volume_id: publicId of the volume to update.",
|
|
5316
5474
|
" - mount_path (optional): new in-container mount path.",
|
|
5317
5475
|
" - size_gb (optional): new size in GB (must be \u2265 current).",
|
|
5318
|
-
" - backup_enabled (optional): whether this volume is backed up. Turning it off stops future backups; it does not delete the ones already taken.",
|
|
5476
|
+
" - backup_enabled (optional): whether this volume is backed up nightly. Turning it off stops future backups; it does not delete the ones already taken. Verify with list_volume_backups \u2014 enabling is not the same as having a backup.",
|
|
5477
|
+
"",
|
|
5478
|
+
"IMPORTANT about what a volume backup is: a block-level tar of a LIVE filesystem, i.e. CRASH CONSISTENT, not application consistent. Nothing is quiesced. For a container running its own database (WordPress + MariaDB in one box, Postgres on a disk, SQLite under write), the archive captures whatever was on disk mid-write \u2014 the same state the database would face after a power cut. Usually recoverable, occasionally not. Recommend this as the disaster fallback and a scheduled dump as the actual backup; do not present it as a substitute for one.",
|
|
5319
5479
|
"",
|
|
5320
5480
|
"Returns: { volume: Volume } \u2014 the updated record.",
|
|
5321
5481
|
"",
|
|
5322
5482
|
'Example: update_volume({ service_id: "svc_abc", volume_id: "vol_xyz", size_gb: 20 }) \u2192 { volume: { sizeGb: 20, \u2026 } }'
|
|
5323
5483
|
].join("\n"),
|
|
5324
5484
|
input: {
|
|
5325
|
-
service_id:
|
|
5326
|
-
volume_id:
|
|
5327
|
-
mount_path:
|
|
5328
|
-
size_gb:
|
|
5329
|
-
backup_enabled:
|
|
5485
|
+
service_id: z22.string().describe("Service publicId."),
|
|
5486
|
+
volume_id: z22.string().describe("Volume publicId (e.g. vol_\u2026)."),
|
|
5487
|
+
mount_path: z22.string().startsWith("/").max(500).optional().describe("New mount path."),
|
|
5488
|
+
size_gb: z22.number().int().min(MIN_VOLUME_SIZE_GB).max(100).optional().describe(`New size in GB (minimum ${MIN_VOLUME_SIZE_GB}, grow-only).`),
|
|
5489
|
+
backup_enabled: z22.boolean().optional().describe("Whether to keep backing this volume up. Past backups are kept either way.")
|
|
5330
5490
|
},
|
|
5331
5491
|
handler: async (args, ctx) => {
|
|
5332
5492
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -5348,6 +5508,78 @@ defineTool({
|
|
|
5348
5508
|
return respond({ summary: `Updated ${fields} on volume ${args.volume_id}.`, data });
|
|
5349
5509
|
}
|
|
5350
5510
|
});
|
|
5511
|
+
defineTool({
|
|
5512
|
+
name: "list_volume_backups",
|
|
5513
|
+
category: "volumes",
|
|
5514
|
+
description: [
|
|
5515
|
+
"List the archives a volume can be restored from, newest first.",
|
|
5516
|
+
"",
|
|
5517
|
+
'WHY THIS MATTERS: `backupEnabled` on a volume says backups are being TAKEN. It does not say any exist. A volume can be enabled and have nothing behind it \u2014 nothing has completed a cycle yet, or the host has no upload grant and is writing its tar onto the very disk it is backing up (that file is not a copy of anything and is deliberately NOT listed here). This tool is how you tell "protected" from "believed to be protected". Check it whenever a volume holds the only copy of something.',
|
|
5518
|
+
"",
|
|
5519
|
+
"When to use: before a risky migration or deploy, when someone asks what the recovery position is, or to pick an archive to hand to restore_volume.",
|
|
5520
|
+
"",
|
|
5521
|
+
"Inputs:",
|
|
5522
|
+
" - service_id: publicId of the service.",
|
|
5523
|
+
" - volume_id: publicId of the volume (vol_\u2026).",
|
|
5524
|
+
"",
|
|
5525
|
+
"Returns: { items: RestorePoint[] } \u2014 id (pass to restore_volume), archiveName, sizeBytes (may be null on older agents), createdAt, s3Url. Only the most recent few are kept: the host prunes older archives out of the bucket on its own schedule.",
|
|
5526
|
+
"",
|
|
5527
|
+
'Example: list_volume_backups({ service_id: "svc_abc", volume_id: "vol_xyz" }) \u2192 { items: [{ id: 41, archiveName: "backup-20260911-0300.tar.gz", sizeBytes: 184532992, createdAt: "2026-09-11T03:00:11Z" }] }'
|
|
5528
|
+
].join("\n"),
|
|
5529
|
+
input: {
|
|
5530
|
+
service_id: z22.string().describe("Service publicId."),
|
|
5531
|
+
volume_id: z22.string().describe("Volume publicId (e.g. vol_\u2026).")
|
|
5532
|
+
},
|
|
5533
|
+
handler: async (args, ctx) => {
|
|
5534
|
+
const teamId = await ctx.resolveTeamId();
|
|
5535
|
+
const response = await ctx.hoststack.volumes.listRestorePoints(
|
|
5536
|
+
teamId,
|
|
5537
|
+
args.service_id,
|
|
5538
|
+
args.volume_id
|
|
5539
|
+
);
|
|
5540
|
+
const data = shapeList(response, "restorePoints", shape);
|
|
5541
|
+
const newest = response.restorePoints[0];
|
|
5542
|
+
const summary = data.items.length === 0 ? `Volume ${args.volume_id} has NO restore points. If backupEnabled is true, either no backup has completed yet or the host cannot upload off-site \u2014 either way there is currently nothing to restore from.` : `Volume ${args.volume_id} has ${data.items.length} restore point${data.items.length === 1 ? "" : "s"}; newest ${newest?.archiveName} from ${newest?.createdAt}.`;
|
|
5543
|
+
return respond({ summary, data });
|
|
5544
|
+
}
|
|
5545
|
+
});
|
|
5546
|
+
defineTool({
|
|
5547
|
+
name: "restore_volume",
|
|
5548
|
+
category: "volumes",
|
|
5549
|
+
description: [
|
|
5550
|
+
"Unpack one of a volume's archives back over its contents.",
|
|
5551
|
+
"",
|
|
5552
|
+
'DESTRUCTIVE AND NOT REVERSIBLE. Everything on the disk now is replaced by what was on it when the archive was taken. Nothing snapshots the pre-restore state first, so a restore to the wrong point loses the data you had. CONFIRM WITH THE USER \u2014 naming the archive and its timestamp \u2014 before calling this. Never call it to "check whether a backup works".',
|
|
5553
|
+
"",
|
|
5554
|
+
"What the archive is: a block-level tar of a filesystem that was LIVE when it was taken \u2014 crash consistent, not application consistent. A database inside it will come back the way it would after a power cut, and may need its own recovery on first start. Stop the service before restoring if you can.",
|
|
5555
|
+
"",
|
|
5556
|
+
"When to use: the user explicitly asks to roll a volume back to an earlier state, or to recover after data loss.",
|
|
5557
|
+
"",
|
|
5558
|
+
"Inputs:",
|
|
5559
|
+
" - service_id: publicId of the service.",
|
|
5560
|
+
" - volume_id: publicId of the volume.",
|
|
5561
|
+
" - backup_id: the numeric id of a restore point from list_volume_backups. Call that FIRST and show the user the choice \u2014 never guess an id.",
|
|
5562
|
+
"",
|
|
5563
|
+
"Returns: { ok: true } once the host has accepted the restore. The unpack itself runs on the host; re-check the service afterwards.",
|
|
5564
|
+
"",
|
|
5565
|
+
'Example: restore_volume({ service_id: "svc_abc", volume_id: "vol_xyz", backup_id: 41 })'
|
|
5566
|
+
].join("\n"),
|
|
5567
|
+
input: {
|
|
5568
|
+
service_id: z22.string().describe("Service publicId."),
|
|
5569
|
+
volume_id: z22.string().describe("Volume publicId."),
|
|
5570
|
+
backup_id: z22.number().int().positive().describe("Restore-point id from list_volume_backups. Confirm with the user first.")
|
|
5571
|
+
},
|
|
5572
|
+
handler: async (args, ctx) => {
|
|
5573
|
+
const teamId = await ctx.resolveTeamId();
|
|
5574
|
+
await ctx.hoststack.volumes.restore(teamId, args.service_id, args.volume_id, {
|
|
5575
|
+
backupId: args.backup_id
|
|
5576
|
+
});
|
|
5577
|
+
return respond({
|
|
5578
|
+
summary: `Restore of volume ${args.volume_id} from backup ${args.backup_id} accepted \u2014 the host is unpacking the archive over the volume's contents. Check the service once it finishes; a database on this volume may run its own crash recovery on first start.`,
|
|
5579
|
+
data: { ok: true }
|
|
5580
|
+
});
|
|
5581
|
+
}
|
|
5582
|
+
});
|
|
5351
5583
|
defineTool({
|
|
5352
5584
|
name: "delete_volume",
|
|
5353
5585
|
category: "volumes",
|
|
@@ -5365,8 +5597,8 @@ defineTool({
|
|
|
5365
5597
|
'Example: delete_volume({ service_id: "svc_abc", volume_id: "vol_xyz" }) \u2192 { ok: true }'
|
|
5366
5598
|
].join("\n"),
|
|
5367
5599
|
input: {
|
|
5368
|
-
service_id:
|
|
5369
|
-
volume_id:
|
|
5600
|
+
service_id: z22.string().describe("Service publicId."),
|
|
5601
|
+
volume_id: z22.string().describe("Volume publicId.")
|
|
5370
5602
|
},
|
|
5371
5603
|
handler: async (args, ctx) => {
|
|
5372
5604
|
const teamId = await ctx.resolveTeamId();
|