@hoststack.dev/mcp 0.18.0 → 0.19.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 -2
- package/dist/hoststack-mcp.js +462 -159
- package/dist/hoststack-mcp.js.map +1 -1
- package/dist/index.d.ts +12 -1
- package/dist/index.js +462 -159
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/hoststack-mcp.js
CHANGED
|
@@ -8,7 +8,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
8
8
|
import { HostStack } from "@hoststack.dev/sdk";
|
|
9
9
|
|
|
10
10
|
// src/version.ts
|
|
11
|
-
var MCP_VERSION = true ? "0.
|
|
11
|
+
var MCP_VERSION = true ? "0.19.0" : "0.0.0-dev";
|
|
12
12
|
var USER_AGENT = `hoststack-mcp/${MCP_VERSION}`;
|
|
13
13
|
|
|
14
14
|
// src/api-client.ts
|
|
@@ -1393,7 +1393,7 @@ var DNS_RECORD_TYPES = [
|
|
|
1393
1393
|
async function resolveZonePublicId(hoststack, teamId, input) {
|
|
1394
1394
|
if (input.zone_id) {
|
|
1395
1395
|
const { zones: zones2 } = await hoststack.dns.listZones(teamId);
|
|
1396
|
-
const match = zones2.find((
|
|
1396
|
+
const match = zones2.find((z22) => z22.publicId === input.zone_id);
|
|
1397
1397
|
if (!match) {
|
|
1398
1398
|
throw new Error(`Zone ${input.zone_id} not found on this team.`);
|
|
1399
1399
|
}
|
|
@@ -1407,7 +1407,7 @@ async function resolveZonePublicId(hoststack, teamId, input) {
|
|
|
1407
1407
|
const labels = fqdn.split(".");
|
|
1408
1408
|
for (let i = 0; i < labels.length - 1; i++) {
|
|
1409
1409
|
const candidate = labels.slice(i).join(".");
|
|
1410
|
-
const match = zones.find((
|
|
1410
|
+
const match = zones.find((z22) => z22.domainName.toLowerCase() === candidate);
|
|
1411
1411
|
if (match && match.status !== "deleting") {
|
|
1412
1412
|
return { publicId: match.publicId, domainName: match.domainName };
|
|
1413
1413
|
}
|
|
@@ -1868,6 +1868,7 @@ defineTool({
|
|
|
1868
1868
|
" - value: new value (will be encrypted at rest if is_secret=true).",
|
|
1869
1869
|
" - is_secret (optional): true marks the value as secret (masked on read). On create, defaults to true for safety. On update, omitting it leaves the existing flag untouched \u2014 pass it explicitly only when you want to change classification.",
|
|
1870
1870
|
' - target (optional): where the var is injected \u2014 "build", "runtime", or "both". On create, defaults to "both". On update, omitting it leaves the existing target untouched.',
|
|
1871
|
+
' IMPORTANT for build-time values (a frontend VITE_*/NEXT_PUBLIC_*, anything a bundler inlines): "both" and "build" reach the build, but a SECRET var never does on "both" \u2014 build args are baked into image history. Since is_secret defaults to TRUE here, set is_secret:false for a public build-time value or it will silently be missing from the built artifact. Use "build" to force a secret into the build anyway.',
|
|
1871
1872
|
"",
|
|
1872
1873
|
'Returns: { envVar: EnvVar, action: "created" | "updated" }.',
|
|
1873
1874
|
"",
|
|
@@ -1878,7 +1879,9 @@ defineTool({
|
|
|
1878
1879
|
key: z10.string().min(1).max(128).describe("Env-var key."),
|
|
1879
1880
|
value: z10.string().describe("New value."),
|
|
1880
1881
|
is_secret: z10.boolean().optional().describe("Mark as secret (encrypted, masked on read). Default true."),
|
|
1881
|
-
target: z10.enum(["build", "runtime", "both"]).optional().describe(
|
|
1882
|
+
target: z10.enum(["build", "runtime", "both"]).optional().describe(
|
|
1883
|
+
'Injection target: build, runtime, or both. Default both. A secret var on "both" is runtime-only (build args land in image history) \u2014 pass is_secret:false for a public build-time value, or use "build" to force it.'
|
|
1884
|
+
)
|
|
1882
1885
|
},
|
|
1883
1886
|
handler: async (args2, ctx) => {
|
|
1884
1887
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -1961,6 +1964,7 @@ defineTool({
|
|
|
1961
1964
|
"Inputs:",
|
|
1962
1965
|
" - service_id: publicId of the service.",
|
|
1963
1966
|
' - env_vars: array of { key, value, is_secret?, target? }. is_secret defaults to FALSE per row (a row is stored in the clear unless you set is_secret:true); target defaults to "both". Note this differs from set_env_var, which defaults a newly-created var to secret.',
|
|
1967
|
+
' A var reaches the image BUILD when target is "build", or when target is "both" and is_secret is false. A secret on "both" is runtime-only, because build args are visible in image history.',
|
|
1964
1968
|
"",
|
|
1965
1969
|
"Returns: { ok: true }. Re-list with list_env_vars to confirm the new state.",
|
|
1966
1970
|
"",
|
|
@@ -2205,7 +2209,7 @@ defineTool({
|
|
|
2205
2209
|
"",
|
|
2206
2210
|
"The ingest endpoint answers HTTP 204 to a real site key and to a typo alike \u2014 deliberately, so it cannot be used to enumerate keys \u2014 which means five completely different situations produce the same empty chart: a stale or mistyped key in the deployed snippet, an origin the site does not allow, a blown hourly quota, events blocked in the browser before they leave (CORS, ad blocker, CSP), and simply no visitors yet. This tool tells them apart.",
|
|
2207
2211
|
"",
|
|
2208
|
-
"Returns health ('receiving' | 'quiet' | 'refusing' | 'never'), a headline and detail sentence, lastEventAt, lastRefusalAt/Reason/Origin, refusedRecently (a 7-day tally by reason), the site key the snippet must carry, allowed origins, and quota usage this hour.",
|
|
2212
|
+
"Returns: health ('receiving' | 'quiet' | 'refusing' | 'never'), a headline and detail sentence, lastEventAt, lastRefusalAt/Reason/Origin, refusedRecently (a 7-day tally by reason), the site key the snippet must carry, allowed origins, and quota usage this hour.",
|
|
2209
2213
|
"",
|
|
2210
2214
|
"health='never' means nothing has EVER reached ingest for this key, so the request is not leaving the browser \u2014 check the snippet is in the deployed HTML and tell the user to add `data-debug` to the script tag, which makes the tracker log its resolved endpoint and every accepted 204 to the console.",
|
|
2211
2215
|
"",
|
|
@@ -2237,6 +2241,8 @@ defineTool({
|
|
|
2237
2241
|
"",
|
|
2238
2242
|
"Inputs: range (24h|7d|30d|90d|12mo, default 7d), domains (optional list; omit for every site).",
|
|
2239
2243
|
"",
|
|
2244
|
+
"Returns: the range asked for, and one row per site \u2014 domain, `current` and `previous` blocks of visitors / pageviews / bounceRate / avgVisitSeconds, the live visitor count, and `visitorsAreSummedDailies`, which says whether that visitor number is a distinct count or a sum of daily uniques. Quote that flag with any 90d or 12mo figure.",
|
|
2245
|
+
"",
|
|
2240
2246
|
"Example: get_analytics_summary({ range: '30d' }) \u2192 { range: '30d', sites: [{ domain: 'hoststack.dev', current: { visitors: 4210, pageviews: 11890, bounceRate: 47.2 }, previous: {\u2026}, live: 3, visitorsAreSummedDailies: false }] }."
|
|
2241
2247
|
].join("\n"),
|
|
2242
2248
|
input: {
|
|
@@ -2268,6 +2274,8 @@ defineTool({
|
|
|
2268
2274
|
"",
|
|
2269
2275
|
"Inputs: range (24h|7d|30d|90d|12mo, default 7d), domains (optional list; omit for every site), filters (optional map \u2014 country, browser, os, device, language, path, referrer, utmSource, utmMedium, utmCampaign, utmTerm, utmContent).",
|
|
2270
2276
|
"",
|
|
2277
|
+
"Returns: `source` ('raw' | 'rollup') and `filtersSupported`, which together say how much of the request was actually honoured; a `summary` block of current-vs-previous totals; a timeseries; and the breakdowns \u2014 topPaths, topReferrers, topEvents, browsers, operatingSystems, languages, screenSizes, campaigns, devices and countries. Read `source` and `filtersSupported` before quoting any of it.",
|
|
2278
|
+
"",
|
|
2271
2279
|
"Example: get_analytics_overview({ domains: ['hoststack.dev'], range: '7d', filters: { country: 'DK' } }) \u2192 { source: 'raw', filtersSupported: true, summary: { current: { visitors: 310, \u2026 } }, topPaths: [{ path: '/pricing', pageviews: 214 }], topReferrers: [{ referrer: 'Direct', visits: 190 }], \u2026 }."
|
|
2272
2280
|
].join("\n"),
|
|
2273
2281
|
input: {
|
|
@@ -2311,6 +2319,8 @@ defineTool({
|
|
|
2311
2319
|
"",
|
|
2312
2320
|
'Inputs: domain (required, bare hostname \u2014 "example.com", not a URL), name (optional display name, defaults to the domain).',
|
|
2313
2321
|
"",
|
|
2322
|
+
"Returns: the created site (id, domain, name, ingestKey, retentionDays) and the ready-made `snippet` to paste into the page \u2014 the key is in that snippet, so hand it over rather than describing it.",
|
|
2323
|
+
"",
|
|
2314
2324
|
`Example: create_analytics_site({ domain: 'micci.dk' }) \u2192 { site: { id: 3, domain: 'micci.dk', ingestKey: 'site_\u2026' }, snippet: '<script defer src="https://hoststack.dev/t.js" data-site-key="site_\u2026"></script>' }.`
|
|
2315
2325
|
].join("\n"),
|
|
2316
2326
|
input: {
|
|
@@ -2330,6 +2340,94 @@ defineTool({
|
|
|
2330
2340
|
});
|
|
2331
2341
|
}
|
|
2332
2342
|
});
|
|
2343
|
+
defineTool({
|
|
2344
|
+
name: "verify_site_domain",
|
|
2345
|
+
category: "analytics",
|
|
2346
|
+
description: [
|
|
2347
|
+
"Prove the team owns a site's domain, by publishing a TXT record.",
|
|
2348
|
+
"",
|
|
2349
|
+
"When to use: before turning on an uptime check for a site that HostStack does not host. Analytics itself needs no proof \u2014 the ingest key only labels events the site posts about itself \u2014 so do NOT send a user through this just to get a dashboard. It exists for the capabilities where the PLATFORM acts on the hostname.",
|
|
2350
|
+
"",
|
|
2351
|
+
"Call it once with no `check` to get the record to publish, then again with check=true once the record is live. The token is stable across calls, so it is safe to re-read the instructions while the user is mid-paste.",
|
|
2352
|
+
"",
|
|
2353
|
+
"A site whose domain is already verified on a service in this team is proven ALREADY and does not need this \u2014 `list_analytics_sites` reports that as domainProven. This is for the site with no service behind it, which can never have such a link.",
|
|
2354
|
+
"",
|
|
2355
|
+
`The record is read from the domain's own authoritative nameservers rather than through a cache, so "I just added it" works immediately instead of being masked by a negative-cache TTL.`,
|
|
2356
|
+
"",
|
|
2357
|
+
'Inputs: siteId (required), check (optional, default false \u2014 true means "look at DNS now and tell me the verdict").',
|
|
2358
|
+
"",
|
|
2359
|
+
"Returns: the record to publish plus `verified`. A `verified: false` is a successful check with a negative answer, not an error \u2014 most often the record simply has not propagated yet.",
|
|
2360
|
+
"",
|
|
2361
|
+
"Example: verify_site_domain({ siteId: 3 }) \u2192 { recordName: '_hoststack-verify.poststack.dev', recordType: 'TXT', recordValue: 'hoststack-verify=9f3c\u2026', verified: false }."
|
|
2362
|
+
].join("\n"),
|
|
2363
|
+
input: {
|
|
2364
|
+
siteId: z12.number().int().positive(),
|
|
2365
|
+
check: z12.boolean().optional().describe(
|
|
2366
|
+
"True to read DNS now and return the verdict. False just returns the record."
|
|
2367
|
+
)
|
|
2368
|
+
},
|
|
2369
|
+
handler: async (args2, ctx) => {
|
|
2370
|
+
const teamId = await ctx.resolveTeamId();
|
|
2371
|
+
const instructions = await ctx.api.get(`/api/analytics/${teamId}/sites/${args2.siteId}/verification`);
|
|
2372
|
+
if (!args2.check) {
|
|
2373
|
+
return respond({
|
|
2374
|
+
summary: instructions.verified ? "Already verified." : `Publish a ${instructions.recordType} record at ${instructions.recordName} with the value shown, then call again with check=true.`,
|
|
2375
|
+
data: shape(instructions)
|
|
2376
|
+
});
|
|
2377
|
+
}
|
|
2378
|
+
const verdict = await ctx.api.post(
|
|
2379
|
+
`/api/analytics/${teamId}/sites/${args2.siteId}/verify`,
|
|
2380
|
+
{}
|
|
2381
|
+
);
|
|
2382
|
+
return respond({
|
|
2383
|
+
summary: verdict.verified ? "Verified. This site can now be given an uptime check." : `Not verified yet \u2014 ${verdict.detail ?? "the record was not found"}.`,
|
|
2384
|
+
data: { record: shape(instructions), result: shape(verdict) }
|
|
2385
|
+
});
|
|
2386
|
+
}
|
|
2387
|
+
});
|
|
2388
|
+
defineTool({
|
|
2389
|
+
name: "set_site_uptime_check",
|
|
2390
|
+
category: "analytics",
|
|
2391
|
+
description: [
|
|
2392
|
+
"Watch a site HostStack does not host \u2014 request its URL on a schedule and alert when it stops answering.",
|
|
2393
|
+
"",
|
|
2394
|
+
"When to use: the user has a site running somewhere else (their own box, another provider) and wants to know when it goes down. This is the one observability capability an off-platform site cannot provide for itself: analytics is a script tag and error reporting is an HTTP POST, but an outside-in probe has to come from outside.",
|
|
2395
|
+
"",
|
|
2396
|
+
"For a site that IS a HostStack service, use set_uptime_check with its serviceId instead \u2014 that one follows the service if its domain changes.",
|
|
2397
|
+
"",
|
|
2398
|
+
"REQUIRES a proven domain. Call verify_site_domain first if `domainProven` is false; an unverified target is refused with 400. This is not paperwork: an uptime check makes the control plane fetch the hostname every interval, forever, from our IP, so it must be a name the team has shown it owns.",
|
|
2399
|
+
"",
|
|
2400
|
+
"Always https, and the host is the site's domain \u2014 `path` is a path, not a URL. Redirects are NOT followed: a 301 is an answer, and following one can walk the probe onto a parked page and report a dead site as healthy. Set expectedStatus to the redirect if one is expected.",
|
|
2401
|
+
"",
|
|
2402
|
+
'Inputs (all optional except siteId): enabled, path (default "/"), method (GET|HEAD), expectedStatus (default 200), timeoutMs (1000\u201360000, default 10000 \u2014 this is what catches a hung server), intervalSeconds (30\u20133600, default 60), failureThreshold (1\u201310, default 3).',
|
|
2403
|
+
"",
|
|
2404
|
+
"Returns: the stored check \u2014 enabled, the resolved target URL, method, expectedStatus, timeoutMs, intervalSeconds, failureThreshold \u2014 plus its live state: status ('up' | 'down' | 'unknown'), lastCheckedAt, lastStatusCode and consecutiveFailures. A freshly created check is 'unknown' until the first probe runs.",
|
|
2405
|
+
"",
|
|
2406
|
+
"Example: set_site_uptime_check({ siteId: 3, path: '/health', intervalSeconds: 60 }) \u2192 { check: { status: 'unknown', \u2026 } }."
|
|
2407
|
+
].join("\n"),
|
|
2408
|
+
input: {
|
|
2409
|
+
siteId: z12.number().int().positive(),
|
|
2410
|
+
enabled: z12.boolean().optional(),
|
|
2411
|
+
path: z12.string().max(500).optional().describe('Must start with /. Default "/".'),
|
|
2412
|
+
method: z12.enum(["GET", "HEAD"]).optional(),
|
|
2413
|
+
expectedStatus: z12.number().int().min(100).max(599).optional(),
|
|
2414
|
+
timeoutMs: z12.number().int().min(1e3).max(6e4).optional(),
|
|
2415
|
+
intervalSeconds: z12.number().int().min(30).max(3600).optional(),
|
|
2416
|
+
failureThreshold: z12.number().int().min(1).max(10).optional()
|
|
2417
|
+
},
|
|
2418
|
+
handler: async (args2, ctx) => {
|
|
2419
|
+
const teamId = await ctx.resolveTeamId();
|
|
2420
|
+
const { siteId, ...body } = args2;
|
|
2421
|
+
const response = await ctx.api.put(
|
|
2422
|
+
`/api/analytics/${teamId}/sites/${siteId}/uptime-check`,
|
|
2423
|
+
body
|
|
2424
|
+
);
|
|
2425
|
+
return respond({
|
|
2426
|
+
summary: "Uptime check saved. It will start reporting within a minute or two.",
|
|
2427
|
+
data: { check: shape(response.check) }
|
|
2428
|
+
});
|
|
2429
|
+
}
|
|
2430
|
+
});
|
|
2333
2431
|
|
|
2334
2432
|
// src/tools/errors.ts
|
|
2335
2433
|
import { z as z13 } from "zod";
|
|
@@ -2623,6 +2721,61 @@ defineTool({
|
|
|
2623
2721
|
}
|
|
2624
2722
|
});
|
|
2625
2723
|
|
|
2724
|
+
// src/tools/issue-reports.ts
|
|
2725
|
+
import { z as z14 } from "zod";
|
|
2726
|
+
defineTool({
|
|
2727
|
+
name: "report_issue",
|
|
2728
|
+
category: "support",
|
|
2729
|
+
description: [
|
|
2730
|
+
"File a bug or platform fault with the HostStack team. Use this when something on the PLATFORM is broken in a way you cannot fix from where you stand \u2014 a deploy that fails identically no matter what you change, a dev box that will not start, an API that returns the wrong thing, an error message that does not match what actually happened.",
|
|
2731
|
+
"",
|
|
2732
|
+
'When to use: you have already looked. This is not a substitute for reading logs (get_deploy_logs, get_service_logs) or for diagnose_deploy \u2014 it is what you do once you have a specific fault and no way to act on it. A report that says "the deploy failed" is worth less than no report; one that says "every deploy since 09:03 fails at container create with 404 No such image, but the image pulls fine by hand" is what gets fixed.',
|
|
2733
|
+
"",
|
|
2734
|
+
"When NOT to use: an application bug in the user's own code, a failing test, a misconfigured env var, or anything the user asked you to change. Those are your job, not a platform fault. Do not file the same issue twice \u2014 if you already reported it this session, say so instead.",
|
|
2735
|
+
"",
|
|
2736
|
+
"Diagnostic context is attached SERVER-SIDE, so you do not need to paste it: passing `serviceId` attaches the service and its most recent deploy, and the last 40 lines of that deploy log go with it. Pass `deployId` to pin a specific deploy instead of the newest. Describe what you observed and what you expected \u2014 the evidence is collected for you.",
|
|
2737
|
+
"",
|
|
2738
|
+
"Inputs: title (required, one line), description (required \u2014 what happened, what you expected, what you already ruled out), severity (low|normal|high|urgent, default normal), serviceId (optional), deployId (optional), databaseId (optional).",
|
|
2739
|
+
"",
|
|
2740
|
+
"Rate-limited to 5 reports per minute per team. Filing one notifies the HostStack team and opens a ticket the user can see and reply to.",
|
|
2741
|
+
"",
|
|
2742
|
+
"Returns: { ticket: { id, publicId } } \u2014 quote the publicId to the user so they can follow it up.",
|
|
2743
|
+
"",
|
|
2744
|
+
"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' } }"
|
|
2745
|
+
].join("\n"),
|
|
2746
|
+
input: {
|
|
2747
|
+
title: z14.string().min(1).max(300).describe("One-line summary of the fault."),
|
|
2748
|
+
description: z14.string().min(1).max(1e4).describe("What happened, what you expected instead, and what you already ruled out."),
|
|
2749
|
+
severity: z14.enum(["low", "normal", "high", "urgent"]).optional().describe(
|
|
2750
|
+
'Default normal. Use high/urgent only when something is DOWN or losing data \u2014 not for "this is annoying".'
|
|
2751
|
+
),
|
|
2752
|
+
serviceId: z14.number().int().positive().optional().describe(
|
|
2753
|
+
"The affected service. Attaches the service, its latest deploy, and that deploy log tail automatically."
|
|
2754
|
+
),
|
|
2755
|
+
deployId: z14.number().int().positive().optional().describe("Pin a specific deploy instead of the service\u2019s most recent one."),
|
|
2756
|
+
databaseId: z14.number().int().positive().optional().describe("The affected database.")
|
|
2757
|
+
},
|
|
2758
|
+
handler: async (args2, ctx) => {
|
|
2759
|
+
const teamId = await ctx.resolveTeamId();
|
|
2760
|
+
const body = {
|
|
2761
|
+
title: args2.title,
|
|
2762
|
+
description: args2.description,
|
|
2763
|
+
severity: args2.severity ?? "normal"
|
|
2764
|
+
};
|
|
2765
|
+
if (args2.serviceId !== void 0) body["serviceId"] = args2.serviceId;
|
|
2766
|
+
if (args2.deployId !== void 0) body["deployId"] = args2.deployId;
|
|
2767
|
+
if (args2.databaseId !== void 0) body["databaseId"] = args2.databaseId;
|
|
2768
|
+
const response = await ctx.api.post(
|
|
2769
|
+
`/api/issue-reports/${teamId}`,
|
|
2770
|
+
body
|
|
2771
|
+
);
|
|
2772
|
+
return respond({
|
|
2773
|
+
summary: `Issue reported as ${response.ticket.publicId}. Tell the user the reference so they can follow it up \u2014 and do not file this one again.`,
|
|
2774
|
+
data: shape(response.ticket)
|
|
2775
|
+
});
|
|
2776
|
+
}
|
|
2777
|
+
});
|
|
2778
|
+
|
|
2626
2779
|
// src/tools/meta.ts
|
|
2627
2780
|
var DEV_ENV_TOOL_NAMES = [
|
|
2628
2781
|
"create_dev_environment",
|
|
@@ -2688,7 +2841,7 @@ defineTool({
|
|
|
2688
2841
|
});
|
|
2689
2842
|
|
|
2690
2843
|
// src/tools/notifications.ts
|
|
2691
|
-
import { z as
|
|
2844
|
+
import { z as z15 } from "zod";
|
|
2692
2845
|
var NOTIFICATION_EVENTS = [
|
|
2693
2846
|
"deploy.started",
|
|
2694
2847
|
"deploy.succeeded",
|
|
@@ -2770,10 +2923,10 @@ defineTool({
|
|
|
2770
2923
|
"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'] })"
|
|
2771
2924
|
].join("\n"),
|
|
2772
2925
|
input: {
|
|
2773
|
-
type:
|
|
2774
|
-
name:
|
|
2775
|
-
webhook_url:
|
|
2776
|
-
events:
|
|
2926
|
+
type: z15.enum(["slack", "discord", "email"]).describe("Channel type."),
|
|
2927
|
+
name: z15.string().min(1).max(128).describe("Human-readable label."),
|
|
2928
|
+
webhook_url: z15.string().max(500).describe("Slack/Discord webhook URL or email address (when type=email)."),
|
|
2929
|
+
events: z15.array(z15.enum(NOTIFICATION_EVENTS)).describe(
|
|
2777
2930
|
"List of events the channel subscribes to. Empty list = subscribe to nothing."
|
|
2778
2931
|
)
|
|
2779
2932
|
},
|
|
@@ -2811,10 +2964,10 @@ defineTool({
|
|
|
2811
2964
|
"Example: update_notification_channel({ channel_id: 3, events: ['deploy.failed', 'service.restart_failed', 'git.auth_failed'] })"
|
|
2812
2965
|
].join("\n"),
|
|
2813
2966
|
input: {
|
|
2814
|
-
channel_id:
|
|
2815
|
-
name:
|
|
2816
|
-
active:
|
|
2817
|
-
events:
|
|
2967
|
+
channel_id: z15.number().int().positive().describe("Numeric channel id from list_notification_channels."),
|
|
2968
|
+
name: z15.string().min(1).max(128).optional().describe("New label."),
|
|
2969
|
+
active: z15.boolean().optional().describe("false silences without deleting."),
|
|
2970
|
+
events: z15.array(z15.enum(NOTIFICATION_EVENTS)).optional().describe("Replaces the full subscription list.")
|
|
2818
2971
|
},
|
|
2819
2972
|
handler: async (args2, ctx) => {
|
|
2820
2973
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -2853,7 +3006,7 @@ defineTool({
|
|
|
2853
3006
|
"Example: delete_notification_channel({ channel_id: 3 }) \u2192 { ok: true }"
|
|
2854
3007
|
].join("\n"),
|
|
2855
3008
|
input: {
|
|
2856
|
-
channel_id:
|
|
3009
|
+
channel_id: z15.number().int().positive().describe("Numeric channel id.")
|
|
2857
3010
|
},
|
|
2858
3011
|
handler: async (args2, ctx) => {
|
|
2859
3012
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -2880,7 +3033,7 @@ defineTool({
|
|
|
2880
3033
|
"Example: test_notification_channel({ channel_id: 3 }) \u2192 { success: true }"
|
|
2881
3034
|
].join("\n"),
|
|
2882
3035
|
input: {
|
|
2883
|
-
channel_id:
|
|
3036
|
+
channel_id: z15.number().int().positive().describe("Numeric channel id.")
|
|
2884
3037
|
},
|
|
2885
3038
|
handler: async (args2, ctx) => {
|
|
2886
3039
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -2895,7 +3048,7 @@ defineTool({
|
|
|
2895
3048
|
});
|
|
2896
3049
|
|
|
2897
3050
|
// src/tools/projects.ts
|
|
2898
|
-
import { z as
|
|
3051
|
+
import { z as z16 } from "zod";
|
|
2899
3052
|
var AVAILABLE_REGION_IDS = ["eu-central-1"];
|
|
2900
3053
|
defineTool({
|
|
2901
3054
|
name: "list_projects",
|
|
@@ -2936,9 +3089,9 @@ defineTool({
|
|
|
2936
3089
|
'Example: create_project({ name: "billing-api", description: "Stripe webhooks", region: "eu-central-1" }) \u2192 { project: { id: 12, publicId: "prj_\u2026", \u2026 } }'
|
|
2937
3090
|
].join("\n"),
|
|
2938
3091
|
input: {
|
|
2939
|
-
name:
|
|
2940
|
-
description:
|
|
2941
|
-
region:
|
|
3092
|
+
name: z16.string().min(1).max(60).describe("Project name (1\u201360 chars)."),
|
|
3093
|
+
description: z16.string().max(500).optional().describe("Short description (\u2264500 chars)."),
|
|
3094
|
+
region: z16.enum(AVAILABLE_REGION_IDS).optional().describe("Region: eu-central-1 (Falkenstein) \u2014 currently the only available region.")
|
|
2942
3095
|
},
|
|
2943
3096
|
handler: async (args2, ctx) => {
|
|
2944
3097
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -2971,9 +3124,9 @@ defineTool({
|
|
|
2971
3124
|
'Example: update_project({ project_id: "prj_abc", name: "billing-prod" }) \u2192 { project: { name: "billing-prod", \u2026 } }'
|
|
2972
3125
|
].join("\n"),
|
|
2973
3126
|
input: {
|
|
2974
|
-
project_id:
|
|
2975
|
-
name:
|
|
2976
|
-
description:
|
|
3127
|
+
project_id: z16.string().describe("Project publicId."),
|
|
3128
|
+
name: z16.string().min(1).max(60).optional().describe("New name (1\u201360 chars)."),
|
|
3129
|
+
description: z16.string().max(500).optional().describe("New description (\u2264500 chars).")
|
|
2977
3130
|
},
|
|
2978
3131
|
handler: async (args2, ctx) => {
|
|
2979
3132
|
if (args2.name === void 0 && args2.description === void 0) {
|
|
@@ -3007,7 +3160,7 @@ defineTool({
|
|
|
3007
3160
|
'Example: get_project({ project_id: "prj_abc" }) \u2192 { project: { id: 12, name: "billing", \u2026 } }'
|
|
3008
3161
|
].join("\n"),
|
|
3009
3162
|
input: {
|
|
3010
|
-
project_id:
|
|
3163
|
+
project_id: z16.string().describe("Project publicId (e.g. prj_abc123).")
|
|
3011
3164
|
},
|
|
3012
3165
|
handler: async (args2, ctx) => {
|
|
3013
3166
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -3018,8 +3171,156 @@ defineTool({
|
|
|
3018
3171
|
}
|
|
3019
3172
|
});
|
|
3020
3173
|
|
|
3174
|
+
// src/tools/dev-tasks.ts
|
|
3175
|
+
import { z as z17 } from "zod";
|
|
3176
|
+
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.";
|
|
3177
|
+
defineTool({
|
|
3178
|
+
name: "list_dev_tasks",
|
|
3179
|
+
category: "dev-tasks",
|
|
3180
|
+
description: [
|
|
3181
|
+
"List a project's agent task backlog - the same list the dashboard's Development \u2192 Tasks surface shows.",
|
|
3182
|
+
"",
|
|
3183
|
+
"When to use: to see what is already queued for a box before filing something (the backlog is how you avoid two agents being pointed at one job), or to find the task id for update_dev_task.",
|
|
3184
|
+
"",
|
|
3185
|
+
"Inputs:",
|
|
3186
|
+
' - project_id: numeric project id (from list_projects) or its "prj_\u2026" publicId.',
|
|
3187
|
+
"",
|
|
3188
|
+
"Returns: { summary, data: { items: DevTask[] } } - each task with publicId, title, status, serviceId (the box, or null for a loose idea), createdAt.",
|
|
3189
|
+
"",
|
|
3190
|
+
"`status` spans two worlds: `idea`/`done` are what a person sets, while `queued`/`running`/`needs_input`/`failed`/`cancelled` describe an agent run and belong to the runner.",
|
|
3191
|
+
"",
|
|
3192
|
+
'Example: list_dev_tasks({ project_id: 26 }) \u2192 { items: [{ publicId: "task_\u2026", status: "idea", title: "Fix the footprint join" }] }'
|
|
3193
|
+
].join("\n"),
|
|
3194
|
+
input: {
|
|
3195
|
+
project_id: z17.union([z17.number().int().positive(), z17.string()]).describe('Project \u2014 publicId ("prj_\u2026") or numeric id.')
|
|
3196
|
+
},
|
|
3197
|
+
handler: async (args2, ctx) => {
|
|
3198
|
+
const teamId = await ctx.resolveTeamId();
|
|
3199
|
+
const response = await ctx.hoststack.devTasks.list(teamId, args2.project_id);
|
|
3200
|
+
const data = shapeList(response, "tasks", shape);
|
|
3201
|
+
const open = data.items.filter(
|
|
3202
|
+
(t) => t && typeof t === "object" && "status" in t && !["done", "cancelled"].includes(String(t.status))
|
|
3203
|
+
).length;
|
|
3204
|
+
const summary = data.items.length === 0 ? "No tasks in this project." : `${data.items.length} task${data.items.length === 1 ? "" : "s"}, ${open} still open.${response.automodeEnabled ? "" : " Automode is off here, so a queued task waits for someone to start it."}`;
|
|
3205
|
+
return respond({ summary, data });
|
|
3206
|
+
}
|
|
3207
|
+
});
|
|
3208
|
+
defineTool({
|
|
3209
|
+
name: "get_dev_task",
|
|
3210
|
+
category: "dev-tasks",
|
|
3211
|
+
description: [
|
|
3212
|
+
"Get one task, including the full prompt body.",
|
|
3213
|
+
"",
|
|
3214
|
+
"When to use: to read what a task actually asks for before acting on it or marking it done - `list_dev_tasks` returns titles, not prompts.",
|
|
3215
|
+
"",
|
|
3216
|
+
"Inputs:",
|
|
3217
|
+
' - task_id: "task_\u2026" publicId or numeric id.',
|
|
3218
|
+
"",
|
|
3219
|
+
"Returns: { summary, data: DevTask }.",
|
|
3220
|
+
"",
|
|
3221
|
+
'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" } }'
|
|
3222
|
+
].join("\n"),
|
|
3223
|
+
input: {
|
|
3224
|
+
task_id: z17.union([z17.number().int().positive(), z17.string()]).describe('Task \u2014 publicId ("task_\u2026") or numeric id.')
|
|
3225
|
+
},
|
|
3226
|
+
handler: async (args2, ctx) => {
|
|
3227
|
+
const teamId = await ctx.resolveTeamId();
|
|
3228
|
+
const response = await ctx.hoststack.devTasks.get(teamId, args2.task_id);
|
|
3229
|
+
const data = shape(response.task);
|
|
3230
|
+
return respond({ summary: `Task ${response.task.publicId}: ${response.task.title}`, data });
|
|
3231
|
+
}
|
|
3232
|
+
});
|
|
3233
|
+
defineTool({
|
|
3234
|
+
name: "create_dev_task",
|
|
3235
|
+
category: "dev-tasks",
|
|
3236
|
+
description: [
|
|
3237
|
+
"File a task in a project's backlog, optionally pinned to a specific dev box.",
|
|
3238
|
+
"",
|
|
3239
|
+
`When to use: to hand work to a dev box other than the one you are in - a fix that belongs in an upstream service, a follow-up in a different repo, anything the box you are in cannot do itself. ${NOT_A_RUN}`,
|
|
3240
|
+
"",
|
|
3241
|
+
"Inputs:",
|
|
3242
|
+
' - project_id: numeric project id or "prj_\u2026" publicId.',
|
|
3243
|
+
" - title: one line, \u2264200 chars. This is what the backlog shows.",
|
|
3244
|
+
" - body: the prompt, markdown, \u226420 000 chars. Write it for someone who was not in this conversation: what is broken, where, how it was measured, and what to watch out for.",
|
|
3245
|
+
' - service_id: the dev box to pin it to ("svc_\u2026" or numeric). Omit for a loose idea in the project backlog.',
|
|
3246
|
+
"",
|
|
3247
|
+
"Returns: { summary, data: DevTask } - `publicId` is the id to quote back to the user.",
|
|
3248
|
+
"",
|
|
3249
|
+
'Example: create_dev_task({ project_id: 26, service_id: 51, title: "Footprint coverage", body: "The BBRUUID join returns 17 of 49 \u2026" })'
|
|
3250
|
+
].join("\n"),
|
|
3251
|
+
input: {
|
|
3252
|
+
project_id: z17.union([z17.number().int().positive(), z17.string()]).describe('Project \u2014 publicId ("prj_\u2026") or numeric id.'),
|
|
3253
|
+
title: z17.string().min(1).max(200).describe("One-line title, \u2264200 chars."),
|
|
3254
|
+
body: z17.string().max(2e4).optional().describe("The prompt handed to the agent. Markdown, \u226420 000 chars."),
|
|
3255
|
+
service_id: z17.union([z17.number().int().positive(), z17.string()]).optional().describe(
|
|
3256
|
+
'Dev box to pin it to \u2014 publicId ("svc_\u2026") or numeric id. Omit for a loose idea.'
|
|
3257
|
+
)
|
|
3258
|
+
},
|
|
3259
|
+
handler: async (args2, ctx) => {
|
|
3260
|
+
const teamId = await ctx.resolveTeamId();
|
|
3261
|
+
const serviceId = args2.service_id === void 0 ? void 0 : await ctx.hoststack.resolveId(args2.service_id, { kind: "service", teamId });
|
|
3262
|
+
const projectId = await ctx.hoststack.resolveId(args2.project_id, {
|
|
3263
|
+
kind: "project",
|
|
3264
|
+
teamId
|
|
3265
|
+
});
|
|
3266
|
+
const response = await ctx.hoststack.devTasks.create(teamId, {
|
|
3267
|
+
projectId,
|
|
3268
|
+
title: args2.title,
|
|
3269
|
+
...args2.body ? { body: args2.body } : {},
|
|
3270
|
+
...serviceId ? { serviceId } : {}
|
|
3271
|
+
});
|
|
3272
|
+
const data = shape(response.task);
|
|
3273
|
+
return respond({
|
|
3274
|
+
summary: `Filed ${response.task.publicId}: ${response.task.title}. ${NOT_A_RUN}`,
|
|
3275
|
+
data
|
|
3276
|
+
});
|
|
3277
|
+
}
|
|
3278
|
+
});
|
|
3279
|
+
defineTool({
|
|
3280
|
+
name: "update_dev_task",
|
|
3281
|
+
category: "dev-tasks",
|
|
3282
|
+
description: [
|
|
3283
|
+
"Edit a task, or move it between the two statuses a PERSON may set.",
|
|
3284
|
+
"",
|
|
3285
|
+
"When to use: to mark a task `done` once the work has landed (by hand, or in another branch - most work does not finish inside the task runner), to reopen it as an `idea`, to correct a title or prompt, or to pin a loose idea to a box.",
|
|
3286
|
+
"",
|
|
3287
|
+
"Inputs:",
|
|
3288
|
+
' - task_id: "task_\u2026" publicId or numeric id.',
|
|
3289
|
+
' - status: "done" or "idea". Only these two; `queued`/`running`/`failed`/`cancelled` belong to the runner and are rejected.',
|
|
3290
|
+
" - title / body / service_id: optional edits.",
|
|
3291
|
+
"",
|
|
3292
|
+
"Only mark a task done when it is actually resolved - the backlog is what someone reads to decide what still needs doing.",
|
|
3293
|
+
"",
|
|
3294
|
+
"Returns: { summary, data: DevTask }.",
|
|
3295
|
+
"",
|
|
3296
|
+
'Example: update_dev_task({ task_id: "task_hy1i2jdp\u2026", status: "done" }) \u2192 { data: { status: "done" } }'
|
|
3297
|
+
].join("\n"),
|
|
3298
|
+
input: {
|
|
3299
|
+
task_id: z17.union([z17.number().int().positive(), z17.string()]).describe('Task \u2014 publicId ("task_\u2026") or numeric id.'),
|
|
3300
|
+
status: z17.enum(["idea", "done"]).optional().describe("The only two a person may set. The runner owns the rest of the lifecycle."),
|
|
3301
|
+
title: z17.string().min(1).max(200).optional().describe("New title."),
|
|
3302
|
+
body: z17.string().max(2e4).optional().describe("New prompt body."),
|
|
3303
|
+
service_id: z17.union([z17.number().int().positive(), z17.string()]).nullable().optional().describe("Pin to a dev box, or null to unpin it back to a loose idea.")
|
|
3304
|
+
},
|
|
3305
|
+
handler: async (args2, ctx) => {
|
|
3306
|
+
const teamId = await ctx.resolveTeamId();
|
|
3307
|
+
const serviceId = args2.service_id === void 0 || args2.service_id === null ? args2.service_id : await ctx.hoststack.resolveId(args2.service_id, { kind: "service", teamId });
|
|
3308
|
+
const response = await ctx.hoststack.devTasks.update(teamId, args2.task_id, {
|
|
3309
|
+
...args2.status ? { status: args2.status } : {},
|
|
3310
|
+
...args2.title ? { title: args2.title } : {},
|
|
3311
|
+
...args2.body !== void 0 ? { body: args2.body } : {},
|
|
3312
|
+
...serviceId !== void 0 ? { serviceId } : {}
|
|
3313
|
+
});
|
|
3314
|
+
const data = shape(response.task);
|
|
3315
|
+
return respond({
|
|
3316
|
+
summary: `${response.task.publicId} is now ${response.task.status}: ${response.task.title}`,
|
|
3317
|
+
data
|
|
3318
|
+
});
|
|
3319
|
+
}
|
|
3320
|
+
});
|
|
3321
|
+
|
|
3021
3322
|
// src/tools/resource-links.ts
|
|
3022
|
-
import { z as
|
|
3323
|
+
import { z as z18 } from "zod";
|
|
3023
3324
|
var RESOURCE_LINK_TYPES = [
|
|
3024
3325
|
"database",
|
|
3025
3326
|
"object_storage",
|
|
@@ -3080,7 +3381,7 @@ defineTool({
|
|
|
3080
3381
|
'Example: list_service_resources({ service_id: "svc_abc" }) \u2192 { items: [{ id: 7, resourceType: "database", resourceId: 42, alias: "APP_DB" }] }'
|
|
3081
3382
|
].join("\n"),
|
|
3082
3383
|
input: {
|
|
3083
|
-
service_id:
|
|
3384
|
+
service_id: z18.union([z18.number().int().positive(), z18.string()]).describe('Service \u2014 publicId ("svc_\u2026") or numeric id.')
|
|
3084
3385
|
},
|
|
3085
3386
|
handler: async (args2, ctx) => {
|
|
3086
3387
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -3117,10 +3418,10 @@ defineTool({
|
|
|
3117
3418
|
'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" } }'
|
|
3118
3419
|
].join("\n"),
|
|
3119
3420
|
input: {
|
|
3120
|
-
service_id:
|
|
3121
|
-
resource_type:
|
|
3122
|
-
resource_id:
|
|
3123
|
-
alias:
|
|
3421
|
+
service_id: z18.union([z18.number().int().positive(), z18.string()]).describe('Consuming service \u2014 publicId ("svc_\u2026") or numeric id.'),
|
|
3422
|
+
resource_type: z18.enum(RESOURCE_LINK_TYPES).describe("Kind of resource being linked."),
|
|
3423
|
+
resource_id: z18.number().int().positive().describe("NUMERIC id of the resource (e.g. database.id) \u2014 not the publicId."),
|
|
3424
|
+
alias: z18.string().min(1).max(48).regex(
|
|
3124
3425
|
/^[A-Z][A-Z0-9_]*$/,
|
|
3125
3426
|
"Alias must be uppercase letters, digits and underscores, starting with a letter."
|
|
3126
3427
|
).describe('Uppercase env-var prefix, e.g. "APP_DB". Unique within the service.')
|
|
@@ -3156,8 +3457,8 @@ defineTool({
|
|
|
3156
3457
|
'Example: unlink_resource_from_service({ service_id: "svc_abc", link_id: 7 }) \u2192 { ok: true }'
|
|
3157
3458
|
].join("\n"),
|
|
3158
3459
|
input: {
|
|
3159
|
-
service_id:
|
|
3160
|
-
link_id:
|
|
3460
|
+
service_id: z18.union([z18.number().int().positive(), z18.string()]).describe('Service \u2014 publicId ("svc_\u2026") or numeric id.'),
|
|
3461
|
+
link_id: z18.number().int().positive().describe("Numeric linkId from list_service_resources (the link's own `id`).")
|
|
3161
3462
|
},
|
|
3162
3463
|
handler: async (args2, ctx) => {
|
|
3163
3464
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -3169,7 +3470,7 @@ defineTool({
|
|
|
3169
3470
|
});
|
|
3170
3471
|
|
|
3171
3472
|
// src/tools/services.ts
|
|
3172
|
-
import { z as
|
|
3473
|
+
import { z as z19 } from "zod";
|
|
3173
3474
|
|
|
3174
3475
|
// src/lib/app-templates.ts
|
|
3175
3476
|
var MCP_APP_TEMPLATES = [
|
|
@@ -3426,11 +3727,11 @@ defineTool({
|
|
|
3426
3727
|
'Example: list_services({ status: "failed" }) \u2192 only services that need attention.'
|
|
3427
3728
|
].join("\n"),
|
|
3428
3729
|
input: {
|
|
3429
|
-
project_id:
|
|
3430
|
-
environment_id:
|
|
3431
|
-
status:
|
|
3432
|
-
type:
|
|
3433
|
-
dev_environment:
|
|
3730
|
+
project_id: z19.union([z19.number().int().positive(), z19.string()]).optional().describe("Project filter \u2014 numeric id or publicId."),
|
|
3731
|
+
environment_id: z19.union([z19.number().int().positive(), z19.string()]).optional().describe("Environment filter \u2014 numeric id or publicId."),
|
|
3732
|
+
status: z19.enum(["active", "deploying", "suspended", "failed", "not_deployed"]).optional().describe("Filter by current runtime status."),
|
|
3733
|
+
type: z19.enum(["web_service", "private_service", "worker", "cron_job", "static_site"]).optional().describe("Filter by service type."),
|
|
3734
|
+
dev_environment: z19.boolean().optional().describe(
|
|
3434
3735
|
"Include agentic Dev Boxes in the results (excluded by default; see list_dev_environments)."
|
|
3435
3736
|
)
|
|
3436
3737
|
},
|
|
@@ -3503,29 +3804,29 @@ defineTool({
|
|
|
3503
3804
|
'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.'
|
|
3504
3805
|
].join("\n"),
|
|
3505
3806
|
input: {
|
|
3506
|
-
project_id:
|
|
3507
|
-
name:
|
|
3508
|
-
type:
|
|
3509
|
-
docker_image:
|
|
3807
|
+
project_id: z19.union([z19.number().int().positive(), z19.string()]).describe("Target project \u2014 numeric id or publicId."),
|
|
3808
|
+
name: z19.string().min(1).max(100).describe("Service name (1\u2013100 chars)."),
|
|
3809
|
+
type: z19.enum(SERVICE_TYPES).describe("Service type."),
|
|
3810
|
+
docker_image: z19.string().max(500).optional().describe(
|
|
3510
3811
|
"Pre-built APPLICATION image ref. Mutually exclusive with github_repo_id. Not for databases \u2014 use create_database for postgres/redis/mysql/mariadb/mongodb."
|
|
3511
3812
|
),
|
|
3512
|
-
github_repo_id:
|
|
3513
|
-
branch:
|
|
3514
|
-
install_command:
|
|
3515
|
-
build_command:
|
|
3516
|
-
start_command:
|
|
3517
|
-
cron_schedule:
|
|
3518
|
-
publish_path:
|
|
3519
|
-
runtime:
|
|
3520
|
-
port:
|
|
3813
|
+
github_repo_id: z19.number().int().positive().optional().describe("Linked GitHub repo numeric id. Mutually exclusive with docker_image."),
|
|
3814
|
+
branch: z19.string().max(200).optional().describe('Git branch (default "main").'),
|
|
3815
|
+
install_command: z19.string().max(1e3).optional().describe("Install shell command."),
|
|
3816
|
+
build_command: z19.string().max(1e3).optional().describe("Build shell command."),
|
|
3817
|
+
start_command: z19.string().max(1e3).optional().describe("Start shell command (required for web/private services without an image)."),
|
|
3818
|
+
cron_schedule: z19.string().max(100).optional().describe("Cron expression \u2014 required for cron_job."),
|
|
3819
|
+
publish_path: z19.string().max(500).optional().describe("Static-site output dir."),
|
|
3820
|
+
runtime: z19.string().max(50).optional().describe("Runtime hint (node/bun/python/\u2026)."),
|
|
3821
|
+
port: z19.number().int().min(1).max(65535).optional().describe(
|
|
3521
3822
|
"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."
|
|
3522
3823
|
),
|
|
3523
|
-
template_id:
|
|
3824
|
+
template_id: z19.string().max(64).optional().describe(
|
|
3524
3825
|
"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."
|
|
3525
3826
|
),
|
|
3526
|
-
plan:
|
|
3527
|
-
environment_id:
|
|
3528
|
-
auto_deploy:
|
|
3827
|
+
plan: z19.enum(SERVICE_PLANS).optional().describe('Service size (default "micro").'),
|
|
3828
|
+
environment_id: z19.union([z19.number().int().positive(), z19.string()]).optional().describe("Bind to a specific environment; defaults to Production."),
|
|
3829
|
+
auto_deploy: z19.boolean().optional().describe("Trigger the first deploy immediately (default true)."),
|
|
3529
3830
|
machine: machineInput
|
|
3530
3831
|
},
|
|
3531
3832
|
handler: async (args2, ctx) => {
|
|
@@ -3596,18 +3897,18 @@ defineTool({
|
|
|
3596
3897
|
'Example: create_dev_environment({ project_id: "prj_abc", name: "scratch", hoststack_api_key: "hs_live_\u2026" })'
|
|
3597
3898
|
].join("\n"),
|
|
3598
3899
|
input: {
|
|
3599
|
-
project_id:
|
|
3600
|
-
name:
|
|
3601
|
-
plan:
|
|
3900
|
+
project_id: z19.union([z19.number().int().positive(), z19.string()]).describe("Target project \u2014 numeric id or publicId."),
|
|
3901
|
+
name: z19.string().min(1).max(100).optional().describe('Service name (default "dev-environment").'),
|
|
3902
|
+
plan: z19.enum(SERVICE_PLANS).optional().describe(
|
|
3602
3903
|
'Box size (default "standard" \u2014 2 GB, the OOM-safe floor; a smaller plan is clamped up to "standard").'
|
|
3603
3904
|
),
|
|
3604
|
-
disk_gb:
|
|
3605
|
-
hoststack_api_key:
|
|
3606
|
-
poststack_api_key:
|
|
3607
|
-
repo_url:
|
|
3905
|
+
disk_gb: z19.number().int().min(10).max(10240).optional().describe("/workspace volume size in GB (default 10, min 10, max 10240)."),
|
|
3906
|
+
hoststack_api_key: z19.string().optional().describe("Value for HOSTSTACK_API_KEY (enables the hoststack MCP in-container)."),
|
|
3907
|
+
poststack_api_key: z19.string().optional().describe("Value for POSTSTACK_API_KEY (enables the poststack MCP in-container)."),
|
|
3908
|
+
repo_url: z19.string().max(500).optional().describe(
|
|
3608
3909
|
"Clone this git URL into /workspace on first boot (HTTPS, or SSH once a key is set)."
|
|
3609
3910
|
),
|
|
3610
|
-
branch:
|
|
3911
|
+
branch: z19.string().max(200).optional().describe("Branch to clone (with repo_url)."),
|
|
3611
3912
|
machine: machineInput
|
|
3612
3913
|
},
|
|
3613
3914
|
handler: async (args2, ctx) => {
|
|
@@ -3724,9 +4025,9 @@ defineTool({
|
|
|
3724
4025
|
'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.'
|
|
3725
4026
|
].join("\n"),
|
|
3726
4027
|
input: {
|
|
3727
|
-
service_id:
|
|
3728
|
-
include_database_clone:
|
|
3729
|
-
name:
|
|
4028
|
+
service_id: z19.union([z19.number().int().positive(), z19.string()]).describe("Source service to debug \u2014 numeric id or publicId."),
|
|
4029
|
+
include_database_clone: z19.boolean().optional().describe("Clone the linked database so the app runs on copied data (default true)."),
|
|
4030
|
+
name: z19.string().min(1).max(100).optional().describe('Dev box name (default "<source>-dev").')
|
|
3730
4031
|
},
|
|
3731
4032
|
handler: async (args2, ctx) => {
|
|
3732
4033
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -3767,7 +4068,7 @@ defineTool({
|
|
|
3767
4068
|
'Example: delete_dev_environment({ service_id: "svc_api_dev" }) \u2192 removes the dev box, its cloned database, and the /workspace volume.'
|
|
3768
4069
|
].join("\n"),
|
|
3769
4070
|
input: {
|
|
3770
|
-
service_id:
|
|
4071
|
+
service_id: z19.union([z19.number().int().positive(), z19.string()]).describe("The dev box to tear down \u2014 numeric id or publicId.")
|
|
3771
4072
|
},
|
|
3772
4073
|
handler: async (args2, ctx) => {
|
|
3773
4074
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -3803,8 +4104,8 @@ defineTool({
|
|
|
3803
4104
|
'Example: resize_dev_environment({ service_id: "svc_skyskraber_dev", size: "large" }) \u2192 bumps the box to the large tier, applied live.'
|
|
3804
4105
|
].join("\n"),
|
|
3805
4106
|
input: {
|
|
3806
|
-
service_id:
|
|
3807
|
-
size:
|
|
4107
|
+
service_id: z19.union([z19.number().int().positive(), z19.string()]).describe("The box to resize \u2014 numeric id or publicId."),
|
|
4108
|
+
size: z19.enum(SERVICE_PLANS).describe(
|
|
3808
4109
|
'Target size tier (service catalog size, e.g. "standard", "large", "xlarge").'
|
|
3809
4110
|
)
|
|
3810
4111
|
},
|
|
@@ -3947,21 +4248,21 @@ defineTool({
|
|
|
3947
4248
|
'Example: create_standalone_dev_environment({ name: "app-dev", source_kind: "github_repo", github_repo_id: 42, databases: ["postgres","redis"] })'
|
|
3948
4249
|
].join("\n"),
|
|
3949
4250
|
input: {
|
|
3950
|
-
name:
|
|
4251
|
+
name: z19.string().min(1).max(100).optional().describe(
|
|
3951
4252
|
'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.'
|
|
3952
4253
|
),
|
|
3953
|
-
source_kind:
|
|
3954
|
-
github_repo_id:
|
|
3955
|
-
clone_url:
|
|
3956
|
-
branch:
|
|
3957
|
-
databases:
|
|
3958
|
-
plan:
|
|
4254
|
+
source_kind: z19.enum(["github_repo", "url", "blank"]).describe("Where the code comes from."),
|
|
4255
|
+
github_repo_id: z19.number().int().positive().optional().describe('Connected GitHub repo id (required when source_kind="github_repo").'),
|
|
4256
|
+
clone_url: z19.string().url().optional().describe('http(s) git clone URL (required when source_kind="url").'),
|
|
4257
|
+
branch: z19.string().min(1).max(255).optional().describe("Branch to clone."),
|
|
4258
|
+
databases: z19.array(z19.enum(["postgres", "redis", "meilisearch"])).optional().describe("Companion services to attach (fresh + empty)."),
|
|
4259
|
+
plan: z19.enum(SERVICE_PLANS).optional().describe(
|
|
3959
4260
|
'Box size (default "standard" \u2014 2 GB; a smaller plan is floored to "standard").'
|
|
3960
4261
|
),
|
|
3961
|
-
agent_accounts:
|
|
3962
|
-
|
|
3963
|
-
provider:
|
|
3964
|
-
account_id:
|
|
4262
|
+
agent_accounts: z19.array(
|
|
4263
|
+
z19.object({
|
|
4264
|
+
provider: z19.enum(["claude", "codex", "opencode"]),
|
|
4265
|
+
account_id: z19.number().int().positive()
|
|
3965
4266
|
})
|
|
3966
4267
|
).max(3).optional().describe(
|
|
3967
4268
|
"Bind saved agent logins by account id per provider. Omit to inherit the box owner's default logins automatically."
|
|
@@ -4039,7 +4340,7 @@ defineTool({
|
|
|
4039
4340
|
'Example: get_service({ service_id: "svc_abc" }) \u2192 { service: { type: "web", status: "running", \u2026 }, config: { healthCheckGracePeriodSec: 120, \u2026 } }'
|
|
4040
4341
|
].join("\n"),
|
|
4041
4342
|
input: {
|
|
4042
|
-
service_id:
|
|
4343
|
+
service_id: z19.string().describe("Service publicId (e.g. svc_abc123).")
|
|
4043
4344
|
},
|
|
4044
4345
|
handler: async (args2, ctx) => {
|
|
4045
4346
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -4071,7 +4372,7 @@ defineTool({
|
|
|
4071
4372
|
'Example: get_service_metrics({ service_id: "svc_abc" }) \u2192 { metrics: { cpu: 0.42, memory: 0.71, \u2026 } }'
|
|
4072
4373
|
].join("\n"),
|
|
4073
4374
|
input: {
|
|
4074
|
-
service_id:
|
|
4375
|
+
service_id: z19.string().describe("Service publicId.")
|
|
4075
4376
|
},
|
|
4076
4377
|
handler: async (args2, ctx) => {
|
|
4077
4378
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -4100,9 +4401,9 @@ defineTool({
|
|
|
4100
4401
|
'Example: get_service_metrics_history({ service_id: "svc_abc", from: "-1h" }) \u2192 60-ish points for the last hour.'
|
|
4101
4402
|
].join("\n"),
|
|
4102
4403
|
input: {
|
|
4103
|
-
service_id:
|
|
4104
|
-
from:
|
|
4105
|
-
to:
|
|
4404
|
+
service_id: z19.string().describe("Service publicId."),
|
|
4405
|
+
from: z19.string().optional().describe('ISO-8601 lower bound or relative offset (e.g. "-1h", "-2d").'),
|
|
4406
|
+
to: z19.string().optional().describe("ISO-8601 upper bound; defaults to now.")
|
|
4106
4407
|
},
|
|
4107
4408
|
handler: async (args2, ctx) => {
|
|
4108
4409
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -4146,8 +4447,8 @@ defineTool({
|
|
|
4146
4447
|
'Example: update_service({ service_id: "svc_abc", name: "api-prod" }) \u2192 { service: { name: "api-prod", \u2026 } }'
|
|
4147
4448
|
].join("\n"),
|
|
4148
4449
|
input: {
|
|
4149
|
-
service_id:
|
|
4150
|
-
name:
|
|
4450
|
+
service_id: z19.string().describe("Service publicId."),
|
|
4451
|
+
name: z19.string().min(1).max(60).describe("New service name (1\u201360 chars).")
|
|
4151
4452
|
},
|
|
4152
4453
|
handler: async (args2, ctx) => {
|
|
4153
4454
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -4196,40 +4497,40 @@ defineTool({
|
|
|
4196
4497
|
'Example: update_service_config({ service_id: "svc_abc", health_check_grace_period_sec: 180 }) \u2192 { config: { healthCheckGracePeriodSec: 180, \u2026 } }'
|
|
4197
4498
|
].join("\n"),
|
|
4198
4499
|
input: {
|
|
4199
|
-
service_id:
|
|
4200
|
-
install_command:
|
|
4201
|
-
build_command:
|
|
4202
|
-
start_command:
|
|
4203
|
-
branch:
|
|
4204
|
-
root_directory:
|
|
4205
|
-
dockerfile_path:
|
|
4206
|
-
auto_deploy:
|
|
4207
|
-
health_check_path:
|
|
4208
|
-
health_check_enabled:
|
|
4209
|
-
health_check_interval:
|
|
4210
|
-
health_check_timeout:
|
|
4211
|
-
health_check_grace_period_sec:
|
|
4500
|
+
service_id: z19.string().describe("Service publicId."),
|
|
4501
|
+
install_command: z19.string().nullable().optional().describe("Install shell command. Null clears."),
|
|
4502
|
+
build_command: z19.string().nullable().optional().describe("Build shell command. Null clears."),
|
|
4503
|
+
start_command: z19.string().nullable().optional().describe("Start shell command. Null clears."),
|
|
4504
|
+
branch: z19.string().optional().describe("Git branch to track."),
|
|
4505
|
+
root_directory: z19.string().optional().describe("Build context root."),
|
|
4506
|
+
dockerfile_path: z19.string().nullable().optional().describe("Path to Dockerfile relative to root. Null clears."),
|
|
4507
|
+
auto_deploy: z19.boolean().optional().describe("Auto-deploy on push."),
|
|
4508
|
+
health_check_path: z19.string().nullable().optional().describe('HTTP health-check path (e.g. "/health"). Null = TCP-only check.'),
|
|
4509
|
+
health_check_enabled: z19.boolean().optional().describe("Toggle health checking on/off."),
|
|
4510
|
+
health_check_interval: z19.number().int().min(5).max(300).optional().describe("How often the check runs, in seconds (5\u2013300)."),
|
|
4511
|
+
health_check_timeout: z19.number().int().min(1).max(60).optional().describe("Single-attempt timeout in seconds (1\u201360)."),
|
|
4512
|
+
health_check_grace_period_sec: z19.number().int().min(1).max(1800).optional().describe(
|
|
4212
4513
|
"Startup grace period in seconds (1\u20131800). Raise this if the app needs more time to boot before health checks start counting failures."
|
|
4213
4514
|
),
|
|
4214
|
-
memory_mb:
|
|
4215
|
-
cpu_shares:
|
|
4216
|
-
disk_size_gb:
|
|
4217
|
-
port:
|
|
4218
|
-
protocol:
|
|
4219
|
-
restart_policy:
|
|
4220
|
-
deploy_strategy:
|
|
4515
|
+
memory_mb: z19.number().int().min(128).max(16384).optional().describe("Container memory cap in MB (128\u201316384)."),
|
|
4516
|
+
cpu_shares: z19.number().int().min(128).max(4096).optional().describe("Relative CPU weight (128\u20134096)."),
|
|
4517
|
+
disk_size_gb: z19.number().int().min(1).max(100).optional().describe("Ephemeral disk size in GB (1\u2013100)."),
|
|
4518
|
+
port: z19.number().int().min(1).max(65535).optional().describe("Container port the platform forwards traffic to."),
|
|
4519
|
+
protocol: z19.enum(["http", "tcp"]).optional().describe("Traffic protocol."),
|
|
4520
|
+
restart_policy: z19.enum(["always", "on-failure", "no"]).optional().describe("Docker restart policy."),
|
|
4521
|
+
deploy_strategy: z19.enum(["rolling", "recreate"]).optional().describe(
|
|
4221
4522
|
'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.'
|
|
4222
4523
|
),
|
|
4223
|
-
pre_deploy_command:
|
|
4224
|
-
instance_count:
|
|
4225
|
-
min_instances:
|
|
4226
|
-
max_instances:
|
|
4227
|
-
scale_cpu_threshold:
|
|
4228
|
-
scale_memory_threshold:
|
|
4229
|
-
log_filter_rules:
|
|
4230
|
-
|
|
4231
|
-
pattern:
|
|
4232
|
-
action:
|
|
4524
|
+
pre_deploy_command: z19.string().optional().describe("Shell command run before the new release accepts traffic."),
|
|
4525
|
+
instance_count: z19.number().int().positive().max(50).optional().describe("Pin min and max instances to this value (1\u201350)."),
|
|
4526
|
+
min_instances: z19.number().int().min(0).max(50).optional().describe("Autoscale lower bound. Use with max_instances for a range."),
|
|
4527
|
+
max_instances: z19.number().int().min(1).max(50).optional().describe("Autoscale upper bound. Use with min_instances for a range."),
|
|
4528
|
+
scale_cpu_threshold: z19.number().int().min(10).max(100).optional().describe("Autoscale CPU trigger percentage (10\u2013100)."),
|
|
4529
|
+
scale_memory_threshold: z19.number().int().min(10).max(100).optional().describe("Autoscale memory trigger percentage (10\u2013100)."),
|
|
4530
|
+
log_filter_rules: z19.array(
|
|
4531
|
+
z19.object({
|
|
4532
|
+
pattern: z19.string().min(1).max(200),
|
|
4533
|
+
action: z19.enum(["drop", "downgrade"])
|
|
4233
4534
|
})
|
|
4234
4535
|
).max(50).optional().describe(
|
|
4235
4536
|
"Runtime-log filter rules. Empty array [] clears all rules. Each pattern is case-insensitive substring match against the message."
|
|
@@ -4326,7 +4627,7 @@ defineTool({
|
|
|
4326
4627
|
'Example: suspend_service({ service_id: "svc_dev" }) \u2192 { ok: true }'
|
|
4327
4628
|
].join("\n"),
|
|
4328
4629
|
input: {
|
|
4329
|
-
service_id:
|
|
4630
|
+
service_id: z19.string().describe("Service publicId.")
|
|
4330
4631
|
},
|
|
4331
4632
|
handler: async (args2, ctx) => {
|
|
4332
4633
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -4350,7 +4651,7 @@ defineTool({
|
|
|
4350
4651
|
'Example: resume_service({ service_id: "svc_dev" }) \u2192 { ok: true }'
|
|
4351
4652
|
].join("\n"),
|
|
4352
4653
|
input: {
|
|
4353
|
-
service_id:
|
|
4654
|
+
service_id: z19.string().describe("Service publicId.")
|
|
4354
4655
|
},
|
|
4355
4656
|
handler: async (args2, ctx) => {
|
|
4356
4657
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -4376,7 +4677,7 @@ defineTool({
|
|
|
4376
4677
|
'Example: delete_service({ service_id: "svc_abandoned" }) \u2192 { ok: true }'
|
|
4377
4678
|
].join("\n"),
|
|
4378
4679
|
input: {
|
|
4379
|
-
service_id:
|
|
4680
|
+
service_id: z19.string().describe("Service publicId.")
|
|
4380
4681
|
},
|
|
4381
4682
|
handler: async (args2, ctx) => {
|
|
4382
4683
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -4413,16 +4714,16 @@ defineTool({
|
|
|
4413
4714
|
' - Just count error lines without fetching them: get_service_logs({ service_id: "svc_abc", level: "error", since: "-5m", count_only: true }) \u2192 { count: 47 }'
|
|
4414
4715
|
].join("\n"),
|
|
4415
4716
|
input: {
|
|
4416
|
-
service_id:
|
|
4417
|
-
lines:
|
|
4418
|
-
since:
|
|
4419
|
-
until:
|
|
4420
|
-
stream:
|
|
4421
|
-
level:
|
|
4717
|
+
service_id: z19.string().describe("Service publicId."),
|
|
4718
|
+
lines: z19.number().int().positive().max(1e3).optional().describe("Tail size; default 200, hard cap 1000."),
|
|
4719
|
+
since: z19.string().optional().describe('ISO-8601 timestamp or relative offset (e.g. "-5m", "-1h").'),
|
|
4720
|
+
until: z19.string().optional().describe("ISO-8601 timestamp or relative offset upper bound."),
|
|
4721
|
+
stream: z19.enum(["stdout", "stderr"]).optional().describe("Restrict to one stream."),
|
|
4722
|
+
level: z19.enum(["stdout", "stderr", "trace", "debug", "info", "warn", "error", "fatal"]).optional().describe(
|
|
4422
4723
|
"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)."
|
|
4423
4724
|
),
|
|
4424
|
-
search:
|
|
4425
|
-
count_only:
|
|
4725
|
+
search: z19.string().max(100).optional().describe("Case-insensitive substring filter."),
|
|
4726
|
+
count_only: z19.boolean().optional().describe("When true, return only { count } \u2014 skips the log payload.")
|
|
4426
4727
|
},
|
|
4427
4728
|
handler: async (args2, ctx) => {
|
|
4428
4729
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -4471,14 +4772,14 @@ defineTool({
|
|
|
4471
4772
|
'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 } } }.'
|
|
4472
4773
|
].join("\n"),
|
|
4473
4774
|
input: {
|
|
4474
|
-
service_ids:
|
|
4475
|
-
lines_per_service:
|
|
4476
|
-
since:
|
|
4477
|
-
until:
|
|
4478
|
-
stream:
|
|
4479
|
-
level:
|
|
4480
|
-
search:
|
|
4481
|
-
count_only:
|
|
4775
|
+
service_ids: z19.array(z19.string()).min(1).max(10).describe("Service publicIds (1\u201310). Hard cap 10 to bound parallel work."),
|
|
4776
|
+
lines_per_service: z19.number().int().positive().max(500).optional().describe("Tail size per service; default 100, hard cap 500."),
|
|
4777
|
+
since: z19.string().optional().describe('ISO-8601 timestamp or relative offset (e.g. "-5m", "-1h").'),
|
|
4778
|
+
until: z19.string().optional().describe("ISO-8601 timestamp or relative offset upper bound."),
|
|
4779
|
+
stream: z19.enum(["stdout", "stderr"]).optional().describe("Restrict to one stream."),
|
|
4780
|
+
level: z19.enum(["stdout", "stderr", "trace", "debug", "info", "warn", "error", "fatal"]).optional().describe("Structured log level filter (same as get_service_logs)."),
|
|
4781
|
+
search: z19.string().max(100).optional().describe("Case-insensitive substring filter."),
|
|
4782
|
+
count_only: z19.boolean().optional().describe("When true, return only counts per service \u2014 skips the log payload.")
|
|
4482
4783
|
},
|
|
4483
4784
|
handler: async (args2, ctx) => {
|
|
4484
4785
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -4524,7 +4825,7 @@ defineTool({
|
|
|
4524
4825
|
});
|
|
4525
4826
|
|
|
4526
4827
|
// src/tools/uptime.ts
|
|
4527
|
-
import { z as
|
|
4828
|
+
import { z as z20 } from "zod";
|
|
4528
4829
|
var STATUS_MEANING = [
|
|
4529
4830
|
"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."
|
|
4530
4831
|
].join("\n");
|
|
@@ -4544,7 +4845,7 @@ defineTool({
|
|
|
4544
4845
|
"",
|
|
4545
4846
|
"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' } }."
|
|
4546
4847
|
].join("\n"),
|
|
4547
|
-
input: { serviceId:
|
|
4848
|
+
input: { serviceId: z20.number().int().positive() },
|
|
4548
4849
|
handler: async (args2, ctx) => {
|
|
4549
4850
|
const teamId = await ctx.resolveTeamId();
|
|
4550
4851
|
const response = await ctx.api.get(
|
|
@@ -4573,6 +4874,8 @@ defineTool({
|
|
|
4573
4874
|
"",
|
|
4574
4875
|
'Only service types with a public URL can be checked (web services and static sites). A worker, cron job or private service is refused with 400 \u2014 a check on one could only ever report "no domain to check", which reads as a broken feature rather than an inapplicable one.',
|
|
4575
4876
|
"",
|
|
4877
|
+
"For a site HostStack does NOT host, there is no serviceId to pass: use set_site_uptime_check with its analytics siteId instead. That path needs the domain proven first (verify_site_domain), because the host it probes comes from the site row rather than from a domain the platform already vouches for.",
|
|
4878
|
+
"",
|
|
4576
4879
|
"IMPORTANT: changing the shape of a check RESETS its accumulated state (status, consecutive failures, open alert). A check whose path or expected status just changed has not observed the new check failing, so carrying failures forward would alert about a condition that was never measured. Pass the full shape you want, not a partial edit of an unknown current state \u2014 read it with get_uptime_check first if that matters.",
|
|
4577
4880
|
"",
|
|
4578
4881
|
"`path` is a path, not a URL: the host comes from the service's primary domain at probe time, so moving the service to a new domain moves the check with it.",
|
|
@@ -4588,14 +4891,14 @@ defineTool({
|
|
|
4588
4891
|
"Example: set_uptime_check({ serviceId: 48, path: '/healthz', intervalSeconds: 60, failureThreshold: 3 }) \u2192 { check: { status: 'unknown', \u2026 } }."
|
|
4589
4892
|
].join("\n"),
|
|
4590
4893
|
input: {
|
|
4591
|
-
serviceId:
|
|
4592
|
-
enabled:
|
|
4593
|
-
path:
|
|
4594
|
-
method:
|
|
4595
|
-
expectedStatus:
|
|
4596
|
-
timeoutMs:
|
|
4597
|
-
intervalSeconds:
|
|
4598
|
-
failureThreshold:
|
|
4894
|
+
serviceId: z20.number().int().positive(),
|
|
4895
|
+
enabled: z20.boolean().optional(),
|
|
4896
|
+
path: z20.string().max(500).optional().describe('Must start with /. Default "/".'),
|
|
4897
|
+
method: z20.enum(["GET", "HEAD"]).optional(),
|
|
4898
|
+
expectedStatus: z20.number().int().min(100).max(599).optional(),
|
|
4899
|
+
timeoutMs: z20.number().int().min(1e3).max(6e4).optional(),
|
|
4900
|
+
intervalSeconds: z20.number().int().min(30).max(3600).optional(),
|
|
4901
|
+
failureThreshold: z20.number().int().min(1).max(10).optional()
|
|
4599
4902
|
},
|
|
4600
4903
|
handler: async (args2, ctx) => {
|
|
4601
4904
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -4624,7 +4927,7 @@ defineTool({
|
|
|
4624
4927
|
"",
|
|
4625
4928
|
"Example: delete_uptime_check({ serviceId: 48 }) \u2192 { success: true }."
|
|
4626
4929
|
].join("\n"),
|
|
4627
|
-
input: { serviceId:
|
|
4930
|
+
input: { serviceId: z20.number().int().positive() },
|
|
4628
4931
|
handler: async (args2, ctx) => {
|
|
4629
4932
|
const teamId = await ctx.resolveTeamId();
|
|
4630
4933
|
await ctx.api.delete(`/api/services/${teamId}/${args2.serviceId}/uptime-check`);
|
|
@@ -4633,7 +4936,7 @@ defineTool({
|
|
|
4633
4936
|
});
|
|
4634
4937
|
|
|
4635
4938
|
// src/tools/volumes.ts
|
|
4636
|
-
import { z as
|
|
4939
|
+
import { z as z21 } from "zod";
|
|
4637
4940
|
var MIN_VOLUME_SIZE_GB = 10;
|
|
4638
4941
|
defineTool({
|
|
4639
4942
|
name: "list_volumes",
|
|
@@ -4651,7 +4954,7 @@ defineTool({
|
|
|
4651
4954
|
'Example: list_volumes({ service_id: "svc_abc" }) \u2192 { items: [{ name: "data", mountPath: "/var/data", sizeGb: 10, status: "active" }] }'
|
|
4652
4955
|
].join("\n"),
|
|
4653
4956
|
input: {
|
|
4654
|
-
service_id:
|
|
4957
|
+
service_id: z21.string().describe("Service publicId (e.g. svc_abc123).")
|
|
4655
4958
|
},
|
|
4656
4959
|
handler: async (args2, ctx) => {
|
|
4657
4960
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -4680,15 +4983,15 @@ defineTool({
|
|
|
4680
4983
|
'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" } }'
|
|
4681
4984
|
].join("\n"),
|
|
4682
4985
|
input: {
|
|
4683
|
-
service_id:
|
|
4684
|
-
name:
|
|
4685
|
-
mount_path:
|
|
4986
|
+
service_id: z21.string().describe("Service publicId."),
|
|
4987
|
+
name: z21.string().min(1).max(64).regex(/^[a-z0-9-]+$/).describe("Volume name (lowercase alphanumeric + hyphens)."),
|
|
4988
|
+
mount_path: z21.string().startsWith("/").max(500).describe("In-container mount path (absolute)."),
|
|
4686
4989
|
// 10 GB is the real floor: the block-storage backend rejects anything
|
|
4687
4990
|
// smaller. Advertising 1 GB here (and defaulting to it) meant taking the
|
|
4688
4991
|
// defaults produced a volume that provisioned with `Hetzner API error:
|
|
4689
4992
|
// 422` on the NEXT deploy, with nothing tying the failure back to the
|
|
4690
4993
|
// size. Reject it at the call instead.
|
|
4691
|
-
size_gb:
|
|
4994
|
+
size_gb: z21.number().int().min(MIN_VOLUME_SIZE_GB).max(100).optional().describe(
|
|
4692
4995
|
`Disk size in GB (minimum ${MIN_VOLUME_SIZE_GB}, default ${MIN_VOLUME_SIZE_GB}, max 100 via MCP).`
|
|
4693
4996
|
)
|
|
4694
4997
|
},
|
|
@@ -4726,10 +5029,10 @@ defineTool({
|
|
|
4726
5029
|
'Example: update_volume({ service_id: "svc_abc", volume_id: "vol_xyz", size_gb: 20 }) \u2192 { volume: { sizeGb: 20, \u2026 } }'
|
|
4727
5030
|
].join("\n"),
|
|
4728
5031
|
input: {
|
|
4729
|
-
service_id:
|
|
4730
|
-
volume_id:
|
|
4731
|
-
mount_path:
|
|
4732
|
-
size_gb:
|
|
5032
|
+
service_id: z21.string().describe("Service publicId."),
|
|
5033
|
+
volume_id: z21.string().describe("Volume publicId (e.g. vol_\u2026)."),
|
|
5034
|
+
mount_path: z21.string().startsWith("/").max(500).optional().describe("New mount path."),
|
|
5035
|
+
size_gb: z21.number().int().min(MIN_VOLUME_SIZE_GB).max(100).optional().describe(`New size in GB (minimum ${MIN_VOLUME_SIZE_GB}, grow-only).`)
|
|
4733
5036
|
},
|
|
4734
5037
|
handler: async (args2, ctx) => {
|
|
4735
5038
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -4767,8 +5070,8 @@ defineTool({
|
|
|
4767
5070
|
'Example: delete_volume({ service_id: "svc_abc", volume_id: "vol_xyz" }) \u2192 { ok: true }'
|
|
4768
5071
|
].join("\n"),
|
|
4769
5072
|
input: {
|
|
4770
|
-
service_id:
|
|
4771
|
-
volume_id:
|
|
5073
|
+
service_id: z21.string().describe("Service publicId."),
|
|
5074
|
+
volume_id: z21.string().describe("Volume publicId.")
|
|
4772
5075
|
},
|
|
4773
5076
|
handler: async (args2, ctx) => {
|
|
4774
5077
|
const teamId = await ctx.resolveTeamId();
|