@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/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.18.0" : "0.0.0-dev";
6
+ var MCP_VERSION = true ? "0.19.0" : "0.0.0-dev";
7
7
  var USER_AGENT = `hoststack-mcp/${MCP_VERSION}`;
8
8
 
9
9
  // src/api-client.ts
@@ -1394,7 +1394,7 @@ var DNS_RECORD_TYPES = [
1394
1394
  async function resolveZonePublicId(hoststack, teamId, input) {
1395
1395
  if (input.zone_id) {
1396
1396
  const { zones: zones2 } = await hoststack.dns.listZones(teamId);
1397
- const match = zones2.find((z20) => z20.publicId === input.zone_id);
1397
+ const match = zones2.find((z22) => z22.publicId === input.zone_id);
1398
1398
  if (!match) {
1399
1399
  throw new Error(`Zone ${input.zone_id} not found on this team.`);
1400
1400
  }
@@ -1408,7 +1408,7 @@ async function resolveZonePublicId(hoststack, teamId, input) {
1408
1408
  const labels = fqdn.split(".");
1409
1409
  for (let i = 0; i < labels.length - 1; i++) {
1410
1410
  const candidate = labels.slice(i).join(".");
1411
- const match = zones.find((z20) => z20.domainName.toLowerCase() === candidate);
1411
+ const match = zones.find((z22) => z22.domainName.toLowerCase() === candidate);
1412
1412
  if (match && match.status !== "deleting") {
1413
1413
  return { publicId: match.publicId, domainName: match.domainName };
1414
1414
  }
@@ -1869,6 +1869,7 @@ defineTool({
1869
1869
  " - value: new value (will be encrypted at rest if is_secret=true).",
1870
1870
  " - 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.",
1871
1871
  ' - 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.',
1872
+ ' 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.',
1872
1873
  "",
1873
1874
  'Returns: { envVar: EnvVar, action: "created" | "updated" }.',
1874
1875
  "",
@@ -1879,7 +1880,9 @@ defineTool({
1879
1880
  key: z10.string().min(1).max(128).describe("Env-var key."),
1880
1881
  value: z10.string().describe("New value."),
1881
1882
  is_secret: z10.boolean().optional().describe("Mark as secret (encrypted, masked on read). Default true."),
1882
- target: z10.enum(["build", "runtime", "both"]).optional().describe("Injection target: build, runtime, or both. Default both.")
1883
+ target: z10.enum(["build", "runtime", "both"]).optional().describe(
1884
+ '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.'
1885
+ )
1883
1886
  },
1884
1887
  handler: async (args, ctx) => {
1885
1888
  const teamId = await ctx.resolveTeamId();
@@ -1962,6 +1965,7 @@ defineTool({
1962
1965
  "Inputs:",
1963
1966
  " - service_id: publicId of the service.",
1964
1967
  ' - 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.',
1968
+ ' 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.',
1965
1969
  "",
1966
1970
  "Returns: { ok: true }. Re-list with list_env_vars to confirm the new state.",
1967
1971
  "",
@@ -2206,7 +2210,7 @@ defineTool({
2206
2210
  "",
2207
2211
  "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.",
2208
2212
  "",
2209
- "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.",
2213
+ "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.",
2210
2214
  "",
2211
2215
  "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.",
2212
2216
  "",
@@ -2238,6 +2242,8 @@ defineTool({
2238
2242
  "",
2239
2243
  "Inputs: range (24h|7d|30d|90d|12mo, default 7d), domains (optional list; omit for every site).",
2240
2244
  "",
2245
+ "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.",
2246
+ "",
2241
2247
  "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 }] }."
2242
2248
  ].join("\n"),
2243
2249
  input: {
@@ -2269,6 +2275,8 @@ defineTool({
2269
2275
  "",
2270
2276
  "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).",
2271
2277
  "",
2278
+ "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.",
2279
+ "",
2272
2280
  "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 }."
2273
2281
  ].join("\n"),
2274
2282
  input: {
@@ -2312,6 +2320,8 @@ defineTool({
2312
2320
  "",
2313
2321
  'Inputs: domain (required, bare hostname \u2014 "example.com", not a URL), name (optional display name, defaults to the domain).',
2314
2322
  "",
2323
+ "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.",
2324
+ "",
2315
2325
  `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>' }.`
2316
2326
  ].join("\n"),
2317
2327
  input: {
@@ -2331,6 +2341,94 @@ defineTool({
2331
2341
  });
2332
2342
  }
2333
2343
  });
2344
+ defineTool({
2345
+ name: "verify_site_domain",
2346
+ category: "analytics",
2347
+ description: [
2348
+ "Prove the team owns a site's domain, by publishing a TXT record.",
2349
+ "",
2350
+ "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.",
2351
+ "",
2352
+ "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.",
2353
+ "",
2354
+ "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.",
2355
+ "",
2356
+ `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.`,
2357
+ "",
2358
+ 'Inputs: siteId (required), check (optional, default false \u2014 true means "look at DNS now and tell me the verdict").',
2359
+ "",
2360
+ "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.",
2361
+ "",
2362
+ "Example: verify_site_domain({ siteId: 3 }) \u2192 { recordName: '_hoststack-verify.poststack.dev', recordType: 'TXT', recordValue: 'hoststack-verify=9f3c\u2026', verified: false }."
2363
+ ].join("\n"),
2364
+ input: {
2365
+ siteId: z12.number().int().positive(),
2366
+ check: z12.boolean().optional().describe(
2367
+ "True to read DNS now and return the verdict. False just returns the record."
2368
+ )
2369
+ },
2370
+ handler: async (args, ctx) => {
2371
+ const teamId = await ctx.resolveTeamId();
2372
+ const instructions = await ctx.api.get(`/api/analytics/${teamId}/sites/${args.siteId}/verification`);
2373
+ if (!args.check) {
2374
+ return respond({
2375
+ summary: instructions.verified ? "Already verified." : `Publish a ${instructions.recordType} record at ${instructions.recordName} with the value shown, then call again with check=true.`,
2376
+ data: shape(instructions)
2377
+ });
2378
+ }
2379
+ const verdict = await ctx.api.post(
2380
+ `/api/analytics/${teamId}/sites/${args.siteId}/verify`,
2381
+ {}
2382
+ );
2383
+ return respond({
2384
+ summary: verdict.verified ? "Verified. This site can now be given an uptime check." : `Not verified yet \u2014 ${verdict.detail ?? "the record was not found"}.`,
2385
+ data: { record: shape(instructions), result: shape(verdict) }
2386
+ });
2387
+ }
2388
+ });
2389
+ defineTool({
2390
+ name: "set_site_uptime_check",
2391
+ category: "analytics",
2392
+ description: [
2393
+ "Watch a site HostStack does not host \u2014 request its URL on a schedule and alert when it stops answering.",
2394
+ "",
2395
+ "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.",
2396
+ "",
2397
+ "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.",
2398
+ "",
2399
+ "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.",
2400
+ "",
2401
+ "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.",
2402
+ "",
2403
+ '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).',
2404
+ "",
2405
+ "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.",
2406
+ "",
2407
+ "Example: set_site_uptime_check({ siteId: 3, path: '/health', intervalSeconds: 60 }) \u2192 { check: { status: 'unknown', \u2026 } }."
2408
+ ].join("\n"),
2409
+ input: {
2410
+ siteId: z12.number().int().positive(),
2411
+ enabled: z12.boolean().optional(),
2412
+ path: z12.string().max(500).optional().describe('Must start with /. Default "/".'),
2413
+ method: z12.enum(["GET", "HEAD"]).optional(),
2414
+ expectedStatus: z12.number().int().min(100).max(599).optional(),
2415
+ timeoutMs: z12.number().int().min(1e3).max(6e4).optional(),
2416
+ intervalSeconds: z12.number().int().min(30).max(3600).optional(),
2417
+ failureThreshold: z12.number().int().min(1).max(10).optional()
2418
+ },
2419
+ handler: async (args, ctx) => {
2420
+ const teamId = await ctx.resolveTeamId();
2421
+ const { siteId, ...body } = args;
2422
+ const response = await ctx.api.put(
2423
+ `/api/analytics/${teamId}/sites/${siteId}/uptime-check`,
2424
+ body
2425
+ );
2426
+ return respond({
2427
+ summary: "Uptime check saved. It will start reporting within a minute or two.",
2428
+ data: { check: shape(response.check) }
2429
+ });
2430
+ }
2431
+ });
2334
2432
 
2335
2433
  // src/tools/errors.ts
2336
2434
  import { z as z13 } from "zod";
@@ -2624,6 +2722,61 @@ defineTool({
2624
2722
  }
2625
2723
  });
2626
2724
 
2725
+ // src/tools/issue-reports.ts
2726
+ import { z as z14 } from "zod";
2727
+ defineTool({
2728
+ name: "report_issue",
2729
+ category: "support",
2730
+ description: [
2731
+ "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.",
2732
+ "",
2733
+ '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.',
2734
+ "",
2735
+ "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.",
2736
+ "",
2737
+ "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.",
2738
+ "",
2739
+ "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).",
2740
+ "",
2741
+ "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.",
2742
+ "",
2743
+ "Returns: { ticket: { id, publicId } } \u2014 quote the publicId to the user so they can follow it up.",
2744
+ "",
2745
+ "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' } }"
2746
+ ].join("\n"),
2747
+ input: {
2748
+ title: z14.string().min(1).max(300).describe("One-line summary of the fault."),
2749
+ description: z14.string().min(1).max(1e4).describe("What happened, what you expected instead, and what you already ruled out."),
2750
+ severity: z14.enum(["low", "normal", "high", "urgent"]).optional().describe(
2751
+ 'Default normal. Use high/urgent only when something is DOWN or losing data \u2014 not for "this is annoying".'
2752
+ ),
2753
+ serviceId: z14.number().int().positive().optional().describe(
2754
+ "The affected service. Attaches the service, its latest deploy, and that deploy log tail automatically."
2755
+ ),
2756
+ deployId: z14.number().int().positive().optional().describe("Pin a specific deploy instead of the service\u2019s most recent one."),
2757
+ databaseId: z14.number().int().positive().optional().describe("The affected database.")
2758
+ },
2759
+ handler: async (args, ctx) => {
2760
+ const teamId = await ctx.resolveTeamId();
2761
+ const body = {
2762
+ title: args.title,
2763
+ description: args.description,
2764
+ severity: args.severity ?? "normal"
2765
+ };
2766
+ if (args.serviceId !== void 0) body["serviceId"] = args.serviceId;
2767
+ if (args.deployId !== void 0) body["deployId"] = args.deployId;
2768
+ if (args.databaseId !== void 0) body["databaseId"] = args.databaseId;
2769
+ const response = await ctx.api.post(
2770
+ `/api/issue-reports/${teamId}`,
2771
+ body
2772
+ );
2773
+ return respond({
2774
+ 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.`,
2775
+ data: shape(response.ticket)
2776
+ });
2777
+ }
2778
+ });
2779
+
2627
2780
  // src/tools/meta.ts
2628
2781
  var DEV_ENV_TOOL_NAMES = [
2629
2782
  "create_dev_environment",
@@ -2689,7 +2842,7 @@ defineTool({
2689
2842
  });
2690
2843
 
2691
2844
  // src/tools/notifications.ts
2692
- import { z as z14 } from "zod";
2845
+ import { z as z15 } from "zod";
2693
2846
  var NOTIFICATION_EVENTS = [
2694
2847
  "deploy.started",
2695
2848
  "deploy.succeeded",
@@ -2771,10 +2924,10 @@ defineTool({
2771
2924
  "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'] })"
2772
2925
  ].join("\n"),
2773
2926
  input: {
2774
- type: z14.enum(["slack", "discord", "email"]).describe("Channel type."),
2775
- name: z14.string().min(1).max(128).describe("Human-readable label."),
2776
- webhook_url: z14.string().max(500).describe("Slack/Discord webhook URL or email address (when type=email)."),
2777
- events: z14.array(z14.enum(NOTIFICATION_EVENTS)).describe(
2927
+ type: z15.enum(["slack", "discord", "email"]).describe("Channel type."),
2928
+ name: z15.string().min(1).max(128).describe("Human-readable label."),
2929
+ webhook_url: z15.string().max(500).describe("Slack/Discord webhook URL or email address (when type=email)."),
2930
+ events: z15.array(z15.enum(NOTIFICATION_EVENTS)).describe(
2778
2931
  "List of events the channel subscribes to. Empty list = subscribe to nothing."
2779
2932
  )
2780
2933
  },
@@ -2812,10 +2965,10 @@ defineTool({
2812
2965
  "Example: update_notification_channel({ channel_id: 3, events: ['deploy.failed', 'service.restart_failed', 'git.auth_failed'] })"
2813
2966
  ].join("\n"),
2814
2967
  input: {
2815
- channel_id: z14.number().int().positive().describe("Numeric channel id from list_notification_channels."),
2816
- name: z14.string().min(1).max(128).optional().describe("New label."),
2817
- active: z14.boolean().optional().describe("false silences without deleting."),
2818
- events: z14.array(z14.enum(NOTIFICATION_EVENTS)).optional().describe("Replaces the full subscription list.")
2968
+ channel_id: z15.number().int().positive().describe("Numeric channel id from list_notification_channels."),
2969
+ name: z15.string().min(1).max(128).optional().describe("New label."),
2970
+ active: z15.boolean().optional().describe("false silences without deleting."),
2971
+ events: z15.array(z15.enum(NOTIFICATION_EVENTS)).optional().describe("Replaces the full subscription list.")
2819
2972
  },
2820
2973
  handler: async (args, ctx) => {
2821
2974
  const teamId = await ctx.resolveTeamId();
@@ -2854,7 +3007,7 @@ defineTool({
2854
3007
  "Example: delete_notification_channel({ channel_id: 3 }) \u2192 { ok: true }"
2855
3008
  ].join("\n"),
2856
3009
  input: {
2857
- channel_id: z14.number().int().positive().describe("Numeric channel id.")
3010
+ channel_id: z15.number().int().positive().describe("Numeric channel id.")
2858
3011
  },
2859
3012
  handler: async (args, ctx) => {
2860
3013
  const teamId = await ctx.resolveTeamId();
@@ -2881,7 +3034,7 @@ defineTool({
2881
3034
  "Example: test_notification_channel({ channel_id: 3 }) \u2192 { success: true }"
2882
3035
  ].join("\n"),
2883
3036
  input: {
2884
- channel_id: z14.number().int().positive().describe("Numeric channel id.")
3037
+ channel_id: z15.number().int().positive().describe("Numeric channel id.")
2885
3038
  },
2886
3039
  handler: async (args, ctx) => {
2887
3040
  const teamId = await ctx.resolveTeamId();
@@ -2896,7 +3049,7 @@ defineTool({
2896
3049
  });
2897
3050
 
2898
3051
  // src/tools/projects.ts
2899
- import { z as z15 } from "zod";
3052
+ import { z as z16 } from "zod";
2900
3053
  var AVAILABLE_REGION_IDS = ["eu-central-1"];
2901
3054
  defineTool({
2902
3055
  name: "list_projects",
@@ -2937,9 +3090,9 @@ defineTool({
2937
3090
  'Example: create_project({ name: "billing-api", description: "Stripe webhooks", region: "eu-central-1" }) \u2192 { project: { id: 12, publicId: "prj_\u2026", \u2026 } }'
2938
3091
  ].join("\n"),
2939
3092
  input: {
2940
- name: z15.string().min(1).max(60).describe("Project name (1\u201360 chars)."),
2941
- description: z15.string().max(500).optional().describe("Short description (\u2264500 chars)."),
2942
- region: z15.enum(AVAILABLE_REGION_IDS).optional().describe("Region: eu-central-1 (Falkenstein) \u2014 currently the only available region.")
3093
+ name: z16.string().min(1).max(60).describe("Project name (1\u201360 chars)."),
3094
+ description: z16.string().max(500).optional().describe("Short description (\u2264500 chars)."),
3095
+ region: z16.enum(AVAILABLE_REGION_IDS).optional().describe("Region: eu-central-1 (Falkenstein) \u2014 currently the only available region.")
2943
3096
  },
2944
3097
  handler: async (args, ctx) => {
2945
3098
  const teamId = await ctx.resolveTeamId();
@@ -2972,9 +3125,9 @@ defineTool({
2972
3125
  'Example: update_project({ project_id: "prj_abc", name: "billing-prod" }) \u2192 { project: { name: "billing-prod", \u2026 } }'
2973
3126
  ].join("\n"),
2974
3127
  input: {
2975
- project_id: z15.string().describe("Project publicId."),
2976
- name: z15.string().min(1).max(60).optional().describe("New name (1\u201360 chars)."),
2977
- description: z15.string().max(500).optional().describe("New description (\u2264500 chars).")
3128
+ project_id: z16.string().describe("Project publicId."),
3129
+ name: z16.string().min(1).max(60).optional().describe("New name (1\u201360 chars)."),
3130
+ description: z16.string().max(500).optional().describe("New description (\u2264500 chars).")
2978
3131
  },
2979
3132
  handler: async (args, ctx) => {
2980
3133
  if (args.name === void 0 && args.description === void 0) {
@@ -3008,7 +3161,7 @@ defineTool({
3008
3161
  'Example: get_project({ project_id: "prj_abc" }) \u2192 { project: { id: 12, name: "billing", \u2026 } }'
3009
3162
  ].join("\n"),
3010
3163
  input: {
3011
- project_id: z15.string().describe("Project publicId (e.g. prj_abc123).")
3164
+ project_id: z16.string().describe("Project publicId (e.g. prj_abc123).")
3012
3165
  },
3013
3166
  handler: async (args, ctx) => {
3014
3167
  const teamId = await ctx.resolveTeamId();
@@ -3019,8 +3172,156 @@ defineTool({
3019
3172
  }
3020
3173
  });
3021
3174
 
3175
+ // src/tools/dev-tasks.ts
3176
+ import { z as z17 } from "zod";
3177
+ 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.";
3178
+ defineTool({
3179
+ name: "list_dev_tasks",
3180
+ category: "dev-tasks",
3181
+ description: [
3182
+ "List a project's agent task backlog - the same list the dashboard's Development \u2192 Tasks surface shows.",
3183
+ "",
3184
+ "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.",
3185
+ "",
3186
+ "Inputs:",
3187
+ ' - project_id: numeric project id (from list_projects) or its "prj_\u2026" publicId.',
3188
+ "",
3189
+ "Returns: { summary, data: { items: DevTask[] } } - each task with publicId, title, status, serviceId (the box, or null for a loose idea), createdAt.",
3190
+ "",
3191
+ "`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.",
3192
+ "",
3193
+ 'Example: list_dev_tasks({ project_id: 26 }) \u2192 { items: [{ publicId: "task_\u2026", status: "idea", title: "Fix the footprint join" }] }'
3194
+ ].join("\n"),
3195
+ input: {
3196
+ project_id: z17.union([z17.number().int().positive(), z17.string()]).describe('Project \u2014 publicId ("prj_\u2026") or numeric id.')
3197
+ },
3198
+ handler: async (args, ctx) => {
3199
+ const teamId = await ctx.resolveTeamId();
3200
+ const response = await ctx.hoststack.devTasks.list(teamId, args.project_id);
3201
+ const data = shapeList(response, "tasks", shape);
3202
+ const open = data.items.filter(
3203
+ (t) => t && typeof t === "object" && "status" in t && !["done", "cancelled"].includes(String(t.status))
3204
+ ).length;
3205
+ 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."}`;
3206
+ return respond({ summary, data });
3207
+ }
3208
+ });
3209
+ defineTool({
3210
+ name: "get_dev_task",
3211
+ category: "dev-tasks",
3212
+ description: [
3213
+ "Get one task, including the full prompt body.",
3214
+ "",
3215
+ "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.",
3216
+ "",
3217
+ "Inputs:",
3218
+ ' - task_id: "task_\u2026" publicId or numeric id.',
3219
+ "",
3220
+ "Returns: { summary, data: DevTask }.",
3221
+ "",
3222
+ '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" } }'
3223
+ ].join("\n"),
3224
+ input: {
3225
+ task_id: z17.union([z17.number().int().positive(), z17.string()]).describe('Task \u2014 publicId ("task_\u2026") or numeric id.')
3226
+ },
3227
+ handler: async (args, ctx) => {
3228
+ const teamId = await ctx.resolveTeamId();
3229
+ const response = await ctx.hoststack.devTasks.get(teamId, args.task_id);
3230
+ const data = shape(response.task);
3231
+ return respond({ summary: `Task ${response.task.publicId}: ${response.task.title}`, data });
3232
+ }
3233
+ });
3234
+ defineTool({
3235
+ name: "create_dev_task",
3236
+ category: "dev-tasks",
3237
+ description: [
3238
+ "File a task in a project's backlog, optionally pinned to a specific dev box.",
3239
+ "",
3240
+ `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}`,
3241
+ "",
3242
+ "Inputs:",
3243
+ ' - project_id: numeric project id or "prj_\u2026" publicId.',
3244
+ " - title: one line, \u2264200 chars. This is what the backlog shows.",
3245
+ " - 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.",
3246
+ ' - service_id: the dev box to pin it to ("svc_\u2026" or numeric). Omit for a loose idea in the project backlog.',
3247
+ "",
3248
+ "Returns: { summary, data: DevTask } - `publicId` is the id to quote back to the user.",
3249
+ "",
3250
+ 'Example: create_dev_task({ project_id: 26, service_id: 51, title: "Footprint coverage", body: "The BBRUUID join returns 17 of 49 \u2026" })'
3251
+ ].join("\n"),
3252
+ input: {
3253
+ project_id: z17.union([z17.number().int().positive(), z17.string()]).describe('Project \u2014 publicId ("prj_\u2026") or numeric id.'),
3254
+ title: z17.string().min(1).max(200).describe("One-line title, \u2264200 chars."),
3255
+ body: z17.string().max(2e4).optional().describe("The prompt handed to the agent. Markdown, \u226420 000 chars."),
3256
+ service_id: z17.union([z17.number().int().positive(), z17.string()]).optional().describe(
3257
+ 'Dev box to pin it to \u2014 publicId ("svc_\u2026") or numeric id. Omit for a loose idea.'
3258
+ )
3259
+ },
3260
+ handler: async (args, ctx) => {
3261
+ const teamId = await ctx.resolveTeamId();
3262
+ const serviceId = args.service_id === void 0 ? void 0 : await ctx.hoststack.resolveId(args.service_id, { kind: "service", teamId });
3263
+ const projectId = await ctx.hoststack.resolveId(args.project_id, {
3264
+ kind: "project",
3265
+ teamId
3266
+ });
3267
+ const response = await ctx.hoststack.devTasks.create(teamId, {
3268
+ projectId,
3269
+ title: args.title,
3270
+ ...args.body ? { body: args.body } : {},
3271
+ ...serviceId ? { serviceId } : {}
3272
+ });
3273
+ const data = shape(response.task);
3274
+ return respond({
3275
+ summary: `Filed ${response.task.publicId}: ${response.task.title}. ${NOT_A_RUN}`,
3276
+ data
3277
+ });
3278
+ }
3279
+ });
3280
+ defineTool({
3281
+ name: "update_dev_task",
3282
+ category: "dev-tasks",
3283
+ description: [
3284
+ "Edit a task, or move it between the two statuses a PERSON may set.",
3285
+ "",
3286
+ "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.",
3287
+ "",
3288
+ "Inputs:",
3289
+ ' - task_id: "task_\u2026" publicId or numeric id.',
3290
+ ' - status: "done" or "idea". Only these two; `queued`/`running`/`failed`/`cancelled` belong to the runner and are rejected.',
3291
+ " - title / body / service_id: optional edits.",
3292
+ "",
3293
+ "Only mark a task done when it is actually resolved - the backlog is what someone reads to decide what still needs doing.",
3294
+ "",
3295
+ "Returns: { summary, data: DevTask }.",
3296
+ "",
3297
+ 'Example: update_dev_task({ task_id: "task_hy1i2jdp\u2026", status: "done" }) \u2192 { data: { status: "done" } }'
3298
+ ].join("\n"),
3299
+ input: {
3300
+ task_id: z17.union([z17.number().int().positive(), z17.string()]).describe('Task \u2014 publicId ("task_\u2026") or numeric id.'),
3301
+ status: z17.enum(["idea", "done"]).optional().describe("The only two a person may set. The runner owns the rest of the lifecycle."),
3302
+ title: z17.string().min(1).max(200).optional().describe("New title."),
3303
+ body: z17.string().max(2e4).optional().describe("New prompt body."),
3304
+ 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.")
3305
+ },
3306
+ handler: async (args, ctx) => {
3307
+ const teamId = await ctx.resolveTeamId();
3308
+ const serviceId = args.service_id === void 0 || args.service_id === null ? args.service_id : await ctx.hoststack.resolveId(args.service_id, { kind: "service", teamId });
3309
+ const response = await ctx.hoststack.devTasks.update(teamId, args.task_id, {
3310
+ ...args.status ? { status: args.status } : {},
3311
+ ...args.title ? { title: args.title } : {},
3312
+ ...args.body !== void 0 ? { body: args.body } : {},
3313
+ ...serviceId !== void 0 ? { serviceId } : {}
3314
+ });
3315
+ const data = shape(response.task);
3316
+ return respond({
3317
+ summary: `${response.task.publicId} is now ${response.task.status}: ${response.task.title}`,
3318
+ data
3319
+ });
3320
+ }
3321
+ });
3322
+
3022
3323
  // src/tools/resource-links.ts
3023
- import { z as z16 } from "zod";
3324
+ import { z as z18 } from "zod";
3024
3325
  var RESOURCE_LINK_TYPES = [
3025
3326
  "database",
3026
3327
  "object_storage",
@@ -3081,7 +3382,7 @@ defineTool({
3081
3382
  'Example: list_service_resources({ service_id: "svc_abc" }) \u2192 { items: [{ id: 7, resourceType: "database", resourceId: 42, alias: "APP_DB" }] }'
3082
3383
  ].join("\n"),
3083
3384
  input: {
3084
- service_id: z16.union([z16.number().int().positive(), z16.string()]).describe('Service \u2014 publicId ("svc_\u2026") or numeric id.')
3385
+ service_id: z18.union([z18.number().int().positive(), z18.string()]).describe('Service \u2014 publicId ("svc_\u2026") or numeric id.')
3085
3386
  },
3086
3387
  handler: async (args, ctx) => {
3087
3388
  const teamId = await ctx.resolveTeamId();
@@ -3118,10 +3419,10 @@ defineTool({
3118
3419
  '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" } }'
3119
3420
  ].join("\n"),
3120
3421
  input: {
3121
- service_id: z16.union([z16.number().int().positive(), z16.string()]).describe('Consuming service \u2014 publicId ("svc_\u2026") or numeric id.'),
3122
- resource_type: z16.enum(RESOURCE_LINK_TYPES).describe("Kind of resource being linked."),
3123
- resource_id: z16.number().int().positive().describe("NUMERIC id of the resource (e.g. database.id) \u2014 not the publicId."),
3124
- alias: z16.string().min(1).max(48).regex(
3422
+ service_id: z18.union([z18.number().int().positive(), z18.string()]).describe('Consuming service \u2014 publicId ("svc_\u2026") or numeric id.'),
3423
+ resource_type: z18.enum(RESOURCE_LINK_TYPES).describe("Kind of resource being linked."),
3424
+ resource_id: z18.number().int().positive().describe("NUMERIC id of the resource (e.g. database.id) \u2014 not the publicId."),
3425
+ alias: z18.string().min(1).max(48).regex(
3125
3426
  /^[A-Z][A-Z0-9_]*$/,
3126
3427
  "Alias must be uppercase letters, digits and underscores, starting with a letter."
3127
3428
  ).describe('Uppercase env-var prefix, e.g. "APP_DB". Unique within the service.')
@@ -3157,8 +3458,8 @@ defineTool({
3157
3458
  'Example: unlink_resource_from_service({ service_id: "svc_abc", link_id: 7 }) \u2192 { ok: true }'
3158
3459
  ].join("\n"),
3159
3460
  input: {
3160
- service_id: z16.union([z16.number().int().positive(), z16.string()]).describe('Service \u2014 publicId ("svc_\u2026") or numeric id.'),
3161
- link_id: z16.number().int().positive().describe("Numeric linkId from list_service_resources (the link's own `id`).")
3461
+ service_id: z18.union([z18.number().int().positive(), z18.string()]).describe('Service \u2014 publicId ("svc_\u2026") or numeric id.'),
3462
+ link_id: z18.number().int().positive().describe("Numeric linkId from list_service_resources (the link's own `id`).")
3162
3463
  },
3163
3464
  handler: async (args, ctx) => {
3164
3465
  const teamId = await ctx.resolveTeamId();
@@ -3170,7 +3471,7 @@ defineTool({
3170
3471
  });
3171
3472
 
3172
3473
  // src/tools/services.ts
3173
- import { z as z17 } from "zod";
3474
+ import { z as z19 } from "zod";
3174
3475
 
3175
3476
  // src/lib/app-templates.ts
3176
3477
  var MCP_APP_TEMPLATES = [
@@ -3427,11 +3728,11 @@ defineTool({
3427
3728
  'Example: list_services({ status: "failed" }) \u2192 only services that need attention.'
3428
3729
  ].join("\n"),
3429
3730
  input: {
3430
- project_id: z17.union([z17.number().int().positive(), z17.string()]).optional().describe("Project filter \u2014 numeric id or publicId."),
3431
- environment_id: z17.union([z17.number().int().positive(), z17.string()]).optional().describe("Environment filter \u2014 numeric id or publicId."),
3432
- status: z17.enum(["active", "deploying", "suspended", "failed", "not_deployed"]).optional().describe("Filter by current runtime status."),
3433
- type: z17.enum(["web_service", "private_service", "worker", "cron_job", "static_site"]).optional().describe("Filter by service type."),
3434
- dev_environment: z17.boolean().optional().describe(
3731
+ project_id: z19.union([z19.number().int().positive(), z19.string()]).optional().describe("Project filter \u2014 numeric id or publicId."),
3732
+ environment_id: z19.union([z19.number().int().positive(), z19.string()]).optional().describe("Environment filter \u2014 numeric id or publicId."),
3733
+ status: z19.enum(["active", "deploying", "suspended", "failed", "not_deployed"]).optional().describe("Filter by current runtime status."),
3734
+ type: z19.enum(["web_service", "private_service", "worker", "cron_job", "static_site"]).optional().describe("Filter by service type."),
3735
+ dev_environment: z19.boolean().optional().describe(
3435
3736
  "Include agentic Dev Boxes in the results (excluded by default; see list_dev_environments)."
3436
3737
  )
3437
3738
  },
@@ -3504,29 +3805,29 @@ defineTool({
3504
3805
  '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.'
3505
3806
  ].join("\n"),
3506
3807
  input: {
3507
- project_id: z17.union([z17.number().int().positive(), z17.string()]).describe("Target project \u2014 numeric id or publicId."),
3508
- name: z17.string().min(1).max(100).describe("Service name (1\u2013100 chars)."),
3509
- type: z17.enum(SERVICE_TYPES).describe("Service type."),
3510
- docker_image: z17.string().max(500).optional().describe(
3808
+ project_id: z19.union([z19.number().int().positive(), z19.string()]).describe("Target project \u2014 numeric id or publicId."),
3809
+ name: z19.string().min(1).max(100).describe("Service name (1\u2013100 chars)."),
3810
+ type: z19.enum(SERVICE_TYPES).describe("Service type."),
3811
+ docker_image: z19.string().max(500).optional().describe(
3511
3812
  "Pre-built APPLICATION image ref. Mutually exclusive with github_repo_id. Not for databases \u2014 use create_database for postgres/redis/mysql/mariadb/mongodb."
3512
3813
  ),
3513
- github_repo_id: z17.number().int().positive().optional().describe("Linked GitHub repo numeric id. Mutually exclusive with docker_image."),
3514
- branch: z17.string().max(200).optional().describe('Git branch (default "main").'),
3515
- install_command: z17.string().max(1e3).optional().describe("Install shell command."),
3516
- build_command: z17.string().max(1e3).optional().describe("Build shell command."),
3517
- start_command: z17.string().max(1e3).optional().describe("Start shell command (required for web/private services without an image)."),
3518
- cron_schedule: z17.string().max(100).optional().describe("Cron expression \u2014 required for cron_job."),
3519
- publish_path: z17.string().max(500).optional().describe("Static-site output dir."),
3520
- runtime: z17.string().max(50).optional().describe("Runtime hint (node/bun/python/\u2026)."),
3521
- port: z17.number().int().min(1).max(65535).optional().describe(
3814
+ github_repo_id: z19.number().int().positive().optional().describe("Linked GitHub repo numeric id. Mutually exclusive with docker_image."),
3815
+ branch: z19.string().max(200).optional().describe('Git branch (default "main").'),
3816
+ install_command: z19.string().max(1e3).optional().describe("Install shell command."),
3817
+ build_command: z19.string().max(1e3).optional().describe("Build shell command."),
3818
+ start_command: z19.string().max(1e3).optional().describe("Start shell command (required for web/private services without an image)."),
3819
+ cron_schedule: z19.string().max(100).optional().describe("Cron expression \u2014 required for cron_job."),
3820
+ publish_path: z19.string().max(500).optional().describe("Static-site output dir."),
3821
+ runtime: z19.string().max(50).optional().describe("Runtime hint (node/bun/python/\u2026)."),
3822
+ port: z19.number().int().min(1).max(65535).optional().describe(
3522
3823
  "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."
3523
3824
  ),
3524
- template_id: z17.string().max(64).optional().describe(
3825
+ template_id: z19.string().max(64).optional().describe(
3525
3826
  "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."
3526
3827
  ),
3527
- plan: z17.enum(SERVICE_PLANS).optional().describe('Service size (default "micro").'),
3528
- environment_id: z17.union([z17.number().int().positive(), z17.string()]).optional().describe("Bind to a specific environment; defaults to Production."),
3529
- auto_deploy: z17.boolean().optional().describe("Trigger the first deploy immediately (default true)."),
3828
+ plan: z19.enum(SERVICE_PLANS).optional().describe('Service size (default "micro").'),
3829
+ environment_id: z19.union([z19.number().int().positive(), z19.string()]).optional().describe("Bind to a specific environment; defaults to Production."),
3830
+ auto_deploy: z19.boolean().optional().describe("Trigger the first deploy immediately (default true)."),
3530
3831
  machine: machineInput
3531
3832
  },
3532
3833
  handler: async (args, ctx) => {
@@ -3597,18 +3898,18 @@ defineTool({
3597
3898
  'Example: create_dev_environment({ project_id: "prj_abc", name: "scratch", hoststack_api_key: "hs_live_\u2026" })'
3598
3899
  ].join("\n"),
3599
3900
  input: {
3600
- project_id: z17.union([z17.number().int().positive(), z17.string()]).describe("Target project \u2014 numeric id or publicId."),
3601
- name: z17.string().min(1).max(100).optional().describe('Service name (default "dev-environment").'),
3602
- plan: z17.enum(SERVICE_PLANS).optional().describe(
3901
+ project_id: z19.union([z19.number().int().positive(), z19.string()]).describe("Target project \u2014 numeric id or publicId."),
3902
+ name: z19.string().min(1).max(100).optional().describe('Service name (default "dev-environment").'),
3903
+ plan: z19.enum(SERVICE_PLANS).optional().describe(
3603
3904
  'Box size (default "standard" \u2014 2 GB, the OOM-safe floor; a smaller plan is clamped up to "standard").'
3604
3905
  ),
3605
- disk_gb: z17.number().int().min(10).max(10240).optional().describe("/workspace volume size in GB (default 10, min 10, max 10240)."),
3606
- hoststack_api_key: z17.string().optional().describe("Value for HOSTSTACK_API_KEY (enables the hoststack MCP in-container)."),
3607
- poststack_api_key: z17.string().optional().describe("Value for POSTSTACK_API_KEY (enables the poststack MCP in-container)."),
3608
- repo_url: z17.string().max(500).optional().describe(
3906
+ disk_gb: z19.number().int().min(10).max(10240).optional().describe("/workspace volume size in GB (default 10, min 10, max 10240)."),
3907
+ hoststack_api_key: z19.string().optional().describe("Value for HOSTSTACK_API_KEY (enables the hoststack MCP in-container)."),
3908
+ poststack_api_key: z19.string().optional().describe("Value for POSTSTACK_API_KEY (enables the poststack MCP in-container)."),
3909
+ repo_url: z19.string().max(500).optional().describe(
3609
3910
  "Clone this git URL into /workspace on first boot (HTTPS, or SSH once a key is set)."
3610
3911
  ),
3611
- branch: z17.string().max(200).optional().describe("Branch to clone (with repo_url)."),
3912
+ branch: z19.string().max(200).optional().describe("Branch to clone (with repo_url)."),
3612
3913
  machine: machineInput
3613
3914
  },
3614
3915
  handler: async (args, ctx) => {
@@ -3725,9 +4026,9 @@ defineTool({
3725
4026
  '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.'
3726
4027
  ].join("\n"),
3727
4028
  input: {
3728
- service_id: z17.union([z17.number().int().positive(), z17.string()]).describe("Source service to debug \u2014 numeric id or publicId."),
3729
- include_database_clone: z17.boolean().optional().describe("Clone the linked database so the app runs on copied data (default true)."),
3730
- name: z17.string().min(1).max(100).optional().describe('Dev box name (default "<source>-dev").')
4029
+ service_id: z19.union([z19.number().int().positive(), z19.string()]).describe("Source service to debug \u2014 numeric id or publicId."),
4030
+ include_database_clone: z19.boolean().optional().describe("Clone the linked database so the app runs on copied data (default true)."),
4031
+ name: z19.string().min(1).max(100).optional().describe('Dev box name (default "<source>-dev").')
3731
4032
  },
3732
4033
  handler: async (args, ctx) => {
3733
4034
  const teamId = await ctx.resolveTeamId();
@@ -3768,7 +4069,7 @@ defineTool({
3768
4069
  'Example: delete_dev_environment({ service_id: "svc_api_dev" }) \u2192 removes the dev box, its cloned database, and the /workspace volume.'
3769
4070
  ].join("\n"),
3770
4071
  input: {
3771
- service_id: z17.union([z17.number().int().positive(), z17.string()]).describe("The dev box to tear down \u2014 numeric id or publicId.")
4072
+ service_id: z19.union([z19.number().int().positive(), z19.string()]).describe("The dev box to tear down \u2014 numeric id or publicId.")
3772
4073
  },
3773
4074
  handler: async (args, ctx) => {
3774
4075
  const teamId = await ctx.resolveTeamId();
@@ -3804,8 +4105,8 @@ defineTool({
3804
4105
  'Example: resize_dev_environment({ service_id: "svc_skyskraber_dev", size: "large" }) \u2192 bumps the box to the large tier, applied live.'
3805
4106
  ].join("\n"),
3806
4107
  input: {
3807
- service_id: z17.union([z17.number().int().positive(), z17.string()]).describe("The box to resize \u2014 numeric id or publicId."),
3808
- size: z17.enum(SERVICE_PLANS).describe(
4108
+ service_id: z19.union([z19.number().int().positive(), z19.string()]).describe("The box to resize \u2014 numeric id or publicId."),
4109
+ size: z19.enum(SERVICE_PLANS).describe(
3809
4110
  'Target size tier (service catalog size, e.g. "standard", "large", "xlarge").'
3810
4111
  )
3811
4112
  },
@@ -3948,21 +4249,21 @@ defineTool({
3948
4249
  'Example: create_standalone_dev_environment({ name: "app-dev", source_kind: "github_repo", github_repo_id: 42, databases: ["postgres","redis"] })'
3949
4250
  ].join("\n"),
3950
4251
  input: {
3951
- name: z17.string().min(1).max(100).optional().describe(
4252
+ name: z19.string().min(1).max(100).optional().describe(
3952
4253
  '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.'
3953
4254
  ),
3954
- source_kind: z17.enum(["github_repo", "url", "blank"]).describe("Where the code comes from."),
3955
- github_repo_id: z17.number().int().positive().optional().describe('Connected GitHub repo id (required when source_kind="github_repo").'),
3956
- clone_url: z17.string().url().optional().describe('http(s) git clone URL (required when source_kind="url").'),
3957
- branch: z17.string().min(1).max(255).optional().describe("Branch to clone."),
3958
- databases: z17.array(z17.enum(["postgres", "redis", "meilisearch"])).optional().describe("Companion services to attach (fresh + empty)."),
3959
- plan: z17.enum(SERVICE_PLANS).optional().describe(
4255
+ source_kind: z19.enum(["github_repo", "url", "blank"]).describe("Where the code comes from."),
4256
+ github_repo_id: z19.number().int().positive().optional().describe('Connected GitHub repo id (required when source_kind="github_repo").'),
4257
+ clone_url: z19.string().url().optional().describe('http(s) git clone URL (required when source_kind="url").'),
4258
+ branch: z19.string().min(1).max(255).optional().describe("Branch to clone."),
4259
+ databases: z19.array(z19.enum(["postgres", "redis", "meilisearch"])).optional().describe("Companion services to attach (fresh + empty)."),
4260
+ plan: z19.enum(SERVICE_PLANS).optional().describe(
3960
4261
  'Box size (default "standard" \u2014 2 GB; a smaller plan is floored to "standard").'
3961
4262
  ),
3962
- agent_accounts: z17.array(
3963
- z17.object({
3964
- provider: z17.enum(["claude", "codex", "opencode"]),
3965
- account_id: z17.number().int().positive()
4263
+ agent_accounts: z19.array(
4264
+ z19.object({
4265
+ provider: z19.enum(["claude", "codex", "opencode"]),
4266
+ account_id: z19.number().int().positive()
3966
4267
  })
3967
4268
  ).max(3).optional().describe(
3968
4269
  "Bind saved agent logins by account id per provider. Omit to inherit the box owner's default logins automatically."
@@ -4040,7 +4341,7 @@ defineTool({
4040
4341
  'Example: get_service({ service_id: "svc_abc" }) \u2192 { service: { type: "web", status: "running", \u2026 }, config: { healthCheckGracePeriodSec: 120, \u2026 } }'
4041
4342
  ].join("\n"),
4042
4343
  input: {
4043
- service_id: z17.string().describe("Service publicId (e.g. svc_abc123).")
4344
+ service_id: z19.string().describe("Service publicId (e.g. svc_abc123).")
4044
4345
  },
4045
4346
  handler: async (args, ctx) => {
4046
4347
  const teamId = await ctx.resolveTeamId();
@@ -4072,7 +4373,7 @@ defineTool({
4072
4373
  'Example: get_service_metrics({ service_id: "svc_abc" }) \u2192 { metrics: { cpu: 0.42, memory: 0.71, \u2026 } }'
4073
4374
  ].join("\n"),
4074
4375
  input: {
4075
- service_id: z17.string().describe("Service publicId.")
4376
+ service_id: z19.string().describe("Service publicId.")
4076
4377
  },
4077
4378
  handler: async (args, ctx) => {
4078
4379
  const teamId = await ctx.resolveTeamId();
@@ -4101,9 +4402,9 @@ defineTool({
4101
4402
  'Example: get_service_metrics_history({ service_id: "svc_abc", from: "-1h" }) \u2192 60-ish points for the last hour.'
4102
4403
  ].join("\n"),
4103
4404
  input: {
4104
- service_id: z17.string().describe("Service publicId."),
4105
- from: z17.string().optional().describe('ISO-8601 lower bound or relative offset (e.g. "-1h", "-2d").'),
4106
- to: z17.string().optional().describe("ISO-8601 upper bound; defaults to now.")
4405
+ service_id: z19.string().describe("Service publicId."),
4406
+ from: z19.string().optional().describe('ISO-8601 lower bound or relative offset (e.g. "-1h", "-2d").'),
4407
+ to: z19.string().optional().describe("ISO-8601 upper bound; defaults to now.")
4107
4408
  },
4108
4409
  handler: async (args, ctx) => {
4109
4410
  const teamId = await ctx.resolveTeamId();
@@ -4147,8 +4448,8 @@ defineTool({
4147
4448
  'Example: update_service({ service_id: "svc_abc", name: "api-prod" }) \u2192 { service: { name: "api-prod", \u2026 } }'
4148
4449
  ].join("\n"),
4149
4450
  input: {
4150
- service_id: z17.string().describe("Service publicId."),
4151
- name: z17.string().min(1).max(60).describe("New service name (1\u201360 chars).")
4451
+ service_id: z19.string().describe("Service publicId."),
4452
+ name: z19.string().min(1).max(60).describe("New service name (1\u201360 chars).")
4152
4453
  },
4153
4454
  handler: async (args, ctx) => {
4154
4455
  const teamId = await ctx.resolveTeamId();
@@ -4197,40 +4498,40 @@ defineTool({
4197
4498
  'Example: update_service_config({ service_id: "svc_abc", health_check_grace_period_sec: 180 }) \u2192 { config: { healthCheckGracePeriodSec: 180, \u2026 } }'
4198
4499
  ].join("\n"),
4199
4500
  input: {
4200
- service_id: z17.string().describe("Service publicId."),
4201
- install_command: z17.string().nullable().optional().describe("Install shell command. Null clears."),
4202
- build_command: z17.string().nullable().optional().describe("Build shell command. Null clears."),
4203
- start_command: z17.string().nullable().optional().describe("Start shell command. Null clears."),
4204
- branch: z17.string().optional().describe("Git branch to track."),
4205
- root_directory: z17.string().optional().describe("Build context root."),
4206
- dockerfile_path: z17.string().nullable().optional().describe("Path to Dockerfile relative to root. Null clears."),
4207
- auto_deploy: z17.boolean().optional().describe("Auto-deploy on push."),
4208
- health_check_path: z17.string().nullable().optional().describe('HTTP health-check path (e.g. "/health"). Null = TCP-only check.'),
4209
- health_check_enabled: z17.boolean().optional().describe("Toggle health checking on/off."),
4210
- health_check_interval: z17.number().int().min(5).max(300).optional().describe("How often the check runs, in seconds (5\u2013300)."),
4211
- health_check_timeout: z17.number().int().min(1).max(60).optional().describe("Single-attempt timeout in seconds (1\u201360)."),
4212
- health_check_grace_period_sec: z17.number().int().min(1).max(1800).optional().describe(
4501
+ service_id: z19.string().describe("Service publicId."),
4502
+ install_command: z19.string().nullable().optional().describe("Install shell command. Null clears."),
4503
+ build_command: z19.string().nullable().optional().describe("Build shell command. Null clears."),
4504
+ start_command: z19.string().nullable().optional().describe("Start shell command. Null clears."),
4505
+ branch: z19.string().optional().describe("Git branch to track."),
4506
+ root_directory: z19.string().optional().describe("Build context root."),
4507
+ dockerfile_path: z19.string().nullable().optional().describe("Path to Dockerfile relative to root. Null clears."),
4508
+ auto_deploy: z19.boolean().optional().describe("Auto-deploy on push."),
4509
+ health_check_path: z19.string().nullable().optional().describe('HTTP health-check path (e.g. "/health"). Null = TCP-only check.'),
4510
+ health_check_enabled: z19.boolean().optional().describe("Toggle health checking on/off."),
4511
+ health_check_interval: z19.number().int().min(5).max(300).optional().describe("How often the check runs, in seconds (5\u2013300)."),
4512
+ health_check_timeout: z19.number().int().min(1).max(60).optional().describe("Single-attempt timeout in seconds (1\u201360)."),
4513
+ health_check_grace_period_sec: z19.number().int().min(1).max(1800).optional().describe(
4213
4514
  "Startup grace period in seconds (1\u20131800). Raise this if the app needs more time to boot before health checks start counting failures."
4214
4515
  ),
4215
- memory_mb: z17.number().int().min(128).max(16384).optional().describe("Container memory cap in MB (128\u201316384)."),
4216
- cpu_shares: z17.number().int().min(128).max(4096).optional().describe("Relative CPU weight (128\u20134096)."),
4217
- disk_size_gb: z17.number().int().min(1).max(100).optional().describe("Ephemeral disk size in GB (1\u2013100)."),
4218
- port: z17.number().int().min(1).max(65535).optional().describe("Container port the platform forwards traffic to."),
4219
- protocol: z17.enum(["http", "tcp"]).optional().describe("Traffic protocol."),
4220
- restart_policy: z17.enum(["always", "on-failure", "no"]).optional().describe("Docker restart policy."),
4221
- deploy_strategy: z17.enum(["rolling", "recreate"]).optional().describe(
4516
+ memory_mb: z19.number().int().min(128).max(16384).optional().describe("Container memory cap in MB (128\u201316384)."),
4517
+ cpu_shares: z19.number().int().min(128).max(4096).optional().describe("Relative CPU weight (128\u20134096)."),
4518
+ disk_size_gb: z19.number().int().min(1).max(100).optional().describe("Ephemeral disk size in GB (1\u2013100)."),
4519
+ port: z19.number().int().min(1).max(65535).optional().describe("Container port the platform forwards traffic to."),
4520
+ protocol: z19.enum(["http", "tcp"]).optional().describe("Traffic protocol."),
4521
+ restart_policy: z19.enum(["always", "on-failure", "no"]).optional().describe("Docker restart policy."),
4522
+ deploy_strategy: z19.enum(["rolling", "recreate"]).optional().describe(
4222
4523
  '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.'
4223
4524
  ),
4224
- pre_deploy_command: z17.string().optional().describe("Shell command run before the new release accepts traffic."),
4225
- instance_count: z17.number().int().positive().max(50).optional().describe("Pin min and max instances to this value (1\u201350)."),
4226
- min_instances: z17.number().int().min(0).max(50).optional().describe("Autoscale lower bound. Use with max_instances for a range."),
4227
- max_instances: z17.number().int().min(1).max(50).optional().describe("Autoscale upper bound. Use with min_instances for a range."),
4228
- scale_cpu_threshold: z17.number().int().min(10).max(100).optional().describe("Autoscale CPU trigger percentage (10\u2013100)."),
4229
- scale_memory_threshold: z17.number().int().min(10).max(100).optional().describe("Autoscale memory trigger percentage (10\u2013100)."),
4230
- log_filter_rules: z17.array(
4231
- z17.object({
4232
- pattern: z17.string().min(1).max(200),
4233
- action: z17.enum(["drop", "downgrade"])
4525
+ pre_deploy_command: z19.string().optional().describe("Shell command run before the new release accepts traffic."),
4526
+ instance_count: z19.number().int().positive().max(50).optional().describe("Pin min and max instances to this value (1\u201350)."),
4527
+ min_instances: z19.number().int().min(0).max(50).optional().describe("Autoscale lower bound. Use with max_instances for a range."),
4528
+ max_instances: z19.number().int().min(1).max(50).optional().describe("Autoscale upper bound. Use with min_instances for a range."),
4529
+ scale_cpu_threshold: z19.number().int().min(10).max(100).optional().describe("Autoscale CPU trigger percentage (10\u2013100)."),
4530
+ scale_memory_threshold: z19.number().int().min(10).max(100).optional().describe("Autoscale memory trigger percentage (10\u2013100)."),
4531
+ log_filter_rules: z19.array(
4532
+ z19.object({
4533
+ pattern: z19.string().min(1).max(200),
4534
+ action: z19.enum(["drop", "downgrade"])
4234
4535
  })
4235
4536
  ).max(50).optional().describe(
4236
4537
  "Runtime-log filter rules. Empty array [] clears all rules. Each pattern is case-insensitive substring match against the message."
@@ -4327,7 +4628,7 @@ defineTool({
4327
4628
  'Example: suspend_service({ service_id: "svc_dev" }) \u2192 { ok: true }'
4328
4629
  ].join("\n"),
4329
4630
  input: {
4330
- service_id: z17.string().describe("Service publicId.")
4631
+ service_id: z19.string().describe("Service publicId.")
4331
4632
  },
4332
4633
  handler: async (args, ctx) => {
4333
4634
  const teamId = await ctx.resolveTeamId();
@@ -4351,7 +4652,7 @@ defineTool({
4351
4652
  'Example: resume_service({ service_id: "svc_dev" }) \u2192 { ok: true }'
4352
4653
  ].join("\n"),
4353
4654
  input: {
4354
- service_id: z17.string().describe("Service publicId.")
4655
+ service_id: z19.string().describe("Service publicId.")
4355
4656
  },
4356
4657
  handler: async (args, ctx) => {
4357
4658
  const teamId = await ctx.resolveTeamId();
@@ -4377,7 +4678,7 @@ defineTool({
4377
4678
  'Example: delete_service({ service_id: "svc_abandoned" }) \u2192 { ok: true }'
4378
4679
  ].join("\n"),
4379
4680
  input: {
4380
- service_id: z17.string().describe("Service publicId.")
4681
+ service_id: z19.string().describe("Service publicId.")
4381
4682
  },
4382
4683
  handler: async (args, ctx) => {
4383
4684
  const teamId = await ctx.resolveTeamId();
@@ -4414,16 +4715,16 @@ defineTool({
4414
4715
  ' - Just count error lines without fetching them: get_service_logs({ service_id: "svc_abc", level: "error", since: "-5m", count_only: true }) \u2192 { count: 47 }'
4415
4716
  ].join("\n"),
4416
4717
  input: {
4417
- service_id: z17.string().describe("Service publicId."),
4418
- lines: z17.number().int().positive().max(1e3).optional().describe("Tail size; default 200, hard cap 1000."),
4419
- since: z17.string().optional().describe('ISO-8601 timestamp or relative offset (e.g. "-5m", "-1h").'),
4420
- until: z17.string().optional().describe("ISO-8601 timestamp or relative offset upper bound."),
4421
- stream: z17.enum(["stdout", "stderr"]).optional().describe("Restrict to one stream."),
4422
- level: z17.enum(["stdout", "stderr", "trace", "debug", "info", "warn", "error", "fatal"]).optional().describe(
4718
+ service_id: z19.string().describe("Service publicId."),
4719
+ lines: z19.number().int().positive().max(1e3).optional().describe("Tail size; default 200, hard cap 1000."),
4720
+ since: z19.string().optional().describe('ISO-8601 timestamp or relative offset (e.g. "-5m", "-1h").'),
4721
+ until: z19.string().optional().describe("ISO-8601 timestamp or relative offset upper bound."),
4722
+ stream: z19.enum(["stdout", "stderr"]).optional().describe("Restrict to one stream."),
4723
+ level: z19.enum(["stdout", "stderr", "trace", "debug", "info", "warn", "error", "fatal"]).optional().describe(
4423
4724
  "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)."
4424
4725
  ),
4425
- search: z17.string().max(100).optional().describe("Case-insensitive substring filter."),
4426
- count_only: z17.boolean().optional().describe("When true, return only { count } \u2014 skips the log payload.")
4726
+ search: z19.string().max(100).optional().describe("Case-insensitive substring filter."),
4727
+ count_only: z19.boolean().optional().describe("When true, return only { count } \u2014 skips the log payload.")
4427
4728
  },
4428
4729
  handler: async (args, ctx) => {
4429
4730
  const teamId = await ctx.resolveTeamId();
@@ -4472,14 +4773,14 @@ defineTool({
4472
4773
  '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 } } }.'
4473
4774
  ].join("\n"),
4474
4775
  input: {
4475
- service_ids: z17.array(z17.string()).min(1).max(10).describe("Service publicIds (1\u201310). Hard cap 10 to bound parallel work."),
4476
- lines_per_service: z17.number().int().positive().max(500).optional().describe("Tail size per service; default 100, hard cap 500."),
4477
- since: z17.string().optional().describe('ISO-8601 timestamp or relative offset (e.g. "-5m", "-1h").'),
4478
- until: z17.string().optional().describe("ISO-8601 timestamp or relative offset upper bound."),
4479
- stream: z17.enum(["stdout", "stderr"]).optional().describe("Restrict to one stream."),
4480
- level: z17.enum(["stdout", "stderr", "trace", "debug", "info", "warn", "error", "fatal"]).optional().describe("Structured log level filter (same as get_service_logs)."),
4481
- search: z17.string().max(100).optional().describe("Case-insensitive substring filter."),
4482
- count_only: z17.boolean().optional().describe("When true, return only counts per service \u2014 skips the log payload.")
4776
+ service_ids: z19.array(z19.string()).min(1).max(10).describe("Service publicIds (1\u201310). Hard cap 10 to bound parallel work."),
4777
+ lines_per_service: z19.number().int().positive().max(500).optional().describe("Tail size per service; default 100, hard cap 500."),
4778
+ since: z19.string().optional().describe('ISO-8601 timestamp or relative offset (e.g. "-5m", "-1h").'),
4779
+ until: z19.string().optional().describe("ISO-8601 timestamp or relative offset upper bound."),
4780
+ stream: z19.enum(["stdout", "stderr"]).optional().describe("Restrict to one stream."),
4781
+ level: z19.enum(["stdout", "stderr", "trace", "debug", "info", "warn", "error", "fatal"]).optional().describe("Structured log level filter (same as get_service_logs)."),
4782
+ search: z19.string().max(100).optional().describe("Case-insensitive substring filter."),
4783
+ count_only: z19.boolean().optional().describe("When true, return only counts per service \u2014 skips the log payload.")
4483
4784
  },
4484
4785
  handler: async (args, ctx) => {
4485
4786
  const teamId = await ctx.resolveTeamId();
@@ -4525,7 +4826,7 @@ defineTool({
4525
4826
  });
4526
4827
 
4527
4828
  // src/tools/uptime.ts
4528
- import { z as z18 } from "zod";
4829
+ import { z as z20 } from "zod";
4529
4830
  var STATUS_MEANING = [
4530
4831
  "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."
4531
4832
  ].join("\n");
@@ -4545,7 +4846,7 @@ defineTool({
4545
4846
  "",
4546
4847
  "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' } }."
4547
4848
  ].join("\n"),
4548
- input: { serviceId: z18.number().int().positive() },
4849
+ input: { serviceId: z20.number().int().positive() },
4549
4850
  handler: async (args, ctx) => {
4550
4851
  const teamId = await ctx.resolveTeamId();
4551
4852
  const response = await ctx.api.get(
@@ -4574,6 +4875,8 @@ defineTool({
4574
4875
  "",
4575
4876
  '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.',
4576
4877
  "",
4878
+ "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.",
4879
+ "",
4577
4880
  "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.",
4578
4881
  "",
4579
4882
  "`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.",
@@ -4589,14 +4892,14 @@ defineTool({
4589
4892
  "Example: set_uptime_check({ serviceId: 48, path: '/healthz', intervalSeconds: 60, failureThreshold: 3 }) \u2192 { check: { status: 'unknown', \u2026 } }."
4590
4893
  ].join("\n"),
4591
4894
  input: {
4592
- serviceId: z18.number().int().positive(),
4593
- enabled: z18.boolean().optional(),
4594
- path: z18.string().max(500).optional().describe('Must start with /. Default "/".'),
4595
- method: z18.enum(["GET", "HEAD"]).optional(),
4596
- expectedStatus: z18.number().int().min(100).max(599).optional(),
4597
- timeoutMs: z18.number().int().min(1e3).max(6e4).optional(),
4598
- intervalSeconds: z18.number().int().min(30).max(3600).optional(),
4599
- failureThreshold: z18.number().int().min(1).max(10).optional()
4895
+ serviceId: z20.number().int().positive(),
4896
+ enabled: z20.boolean().optional(),
4897
+ path: z20.string().max(500).optional().describe('Must start with /. Default "/".'),
4898
+ method: z20.enum(["GET", "HEAD"]).optional(),
4899
+ expectedStatus: z20.number().int().min(100).max(599).optional(),
4900
+ timeoutMs: z20.number().int().min(1e3).max(6e4).optional(),
4901
+ intervalSeconds: z20.number().int().min(30).max(3600).optional(),
4902
+ failureThreshold: z20.number().int().min(1).max(10).optional()
4600
4903
  },
4601
4904
  handler: async (args, ctx) => {
4602
4905
  const teamId = await ctx.resolveTeamId();
@@ -4625,7 +4928,7 @@ defineTool({
4625
4928
  "",
4626
4929
  "Example: delete_uptime_check({ serviceId: 48 }) \u2192 { success: true }."
4627
4930
  ].join("\n"),
4628
- input: { serviceId: z18.number().int().positive() },
4931
+ input: { serviceId: z20.number().int().positive() },
4629
4932
  handler: async (args, ctx) => {
4630
4933
  const teamId = await ctx.resolveTeamId();
4631
4934
  await ctx.api.delete(`/api/services/${teamId}/${args.serviceId}/uptime-check`);
@@ -4634,7 +4937,7 @@ defineTool({
4634
4937
  });
4635
4938
 
4636
4939
  // src/tools/volumes.ts
4637
- import { z as z19 } from "zod";
4940
+ import { z as z21 } from "zod";
4638
4941
  var MIN_VOLUME_SIZE_GB = 10;
4639
4942
  defineTool({
4640
4943
  name: "list_volumes",
@@ -4652,7 +4955,7 @@ defineTool({
4652
4955
  'Example: list_volumes({ service_id: "svc_abc" }) \u2192 { items: [{ name: "data", mountPath: "/var/data", sizeGb: 10, status: "active" }] }'
4653
4956
  ].join("\n"),
4654
4957
  input: {
4655
- service_id: z19.string().describe("Service publicId (e.g. svc_abc123).")
4958
+ service_id: z21.string().describe("Service publicId (e.g. svc_abc123).")
4656
4959
  },
4657
4960
  handler: async (args, ctx) => {
4658
4961
  const teamId = await ctx.resolveTeamId();
@@ -4681,15 +4984,15 @@ defineTool({
4681
4984
  '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" } }'
4682
4985
  ].join("\n"),
4683
4986
  input: {
4684
- service_id: z19.string().describe("Service publicId."),
4685
- name: z19.string().min(1).max(64).regex(/^[a-z0-9-]+$/).describe("Volume name (lowercase alphanumeric + hyphens)."),
4686
- mount_path: z19.string().startsWith("/").max(500).describe("In-container mount path (absolute)."),
4987
+ service_id: z21.string().describe("Service publicId."),
4988
+ name: z21.string().min(1).max(64).regex(/^[a-z0-9-]+$/).describe("Volume name (lowercase alphanumeric + hyphens)."),
4989
+ mount_path: z21.string().startsWith("/").max(500).describe("In-container mount path (absolute)."),
4687
4990
  // 10 GB is the real floor: the block-storage backend rejects anything
4688
4991
  // smaller. Advertising 1 GB here (and defaulting to it) meant taking the
4689
4992
  // defaults produced a volume that provisioned with `Hetzner API error:
4690
4993
  // 422` on the NEXT deploy, with nothing tying the failure back to the
4691
4994
  // size. Reject it at the call instead.
4692
- size_gb: z19.number().int().min(MIN_VOLUME_SIZE_GB).max(100).optional().describe(
4995
+ size_gb: z21.number().int().min(MIN_VOLUME_SIZE_GB).max(100).optional().describe(
4693
4996
  `Disk size in GB (minimum ${MIN_VOLUME_SIZE_GB}, default ${MIN_VOLUME_SIZE_GB}, max 100 via MCP).`
4694
4997
  )
4695
4998
  },
@@ -4727,10 +5030,10 @@ defineTool({
4727
5030
  'Example: update_volume({ service_id: "svc_abc", volume_id: "vol_xyz", size_gb: 20 }) \u2192 { volume: { sizeGb: 20, \u2026 } }'
4728
5031
  ].join("\n"),
4729
5032
  input: {
4730
- service_id: z19.string().describe("Service publicId."),
4731
- volume_id: z19.string().describe("Volume publicId (e.g. vol_\u2026)."),
4732
- mount_path: z19.string().startsWith("/").max(500).optional().describe("New mount path."),
4733
- size_gb: z19.number().int().min(MIN_VOLUME_SIZE_GB).max(100).optional().describe(`New size in GB (minimum ${MIN_VOLUME_SIZE_GB}, grow-only).`)
5033
+ service_id: z21.string().describe("Service publicId."),
5034
+ volume_id: z21.string().describe("Volume publicId (e.g. vol_\u2026)."),
5035
+ mount_path: z21.string().startsWith("/").max(500).optional().describe("New mount path."),
5036
+ 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).`)
4734
5037
  },
4735
5038
  handler: async (args, ctx) => {
4736
5039
  const teamId = await ctx.resolveTeamId();
@@ -4768,8 +5071,8 @@ defineTool({
4768
5071
  'Example: delete_volume({ service_id: "svc_abc", volume_id: "vol_xyz" }) \u2192 { ok: true }'
4769
5072
  ].join("\n"),
4770
5073
  input: {
4771
- service_id: z19.string().describe("Service publicId."),
4772
- volume_id: z19.string().describe("Volume publicId.")
5074
+ service_id: z21.string().describe("Service publicId."),
5075
+ volume_id: z21.string().describe("Volume publicId.")
4773
5076
  },
4774
5077
  handler: async (args, ctx) => {
4775
5078
  const teamId = await ctx.resolveTeamId();