@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.
@@ -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.18.0" : "0.0.0-dev";
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((z20) => z20.publicId === input.zone_id);
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((z20) => z20.domainName.toLowerCase() === candidate);
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("Injection target: build, runtime, or both. Default both.")
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 z14 } from "zod";
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: z14.enum(["slack", "discord", "email"]).describe("Channel type."),
2774
- name: z14.string().min(1).max(128).describe("Human-readable label."),
2775
- webhook_url: z14.string().max(500).describe("Slack/Discord webhook URL or email address (when type=email)."),
2776
- events: z14.array(z14.enum(NOTIFICATION_EVENTS)).describe(
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: z14.number().int().positive().describe("Numeric channel id from list_notification_channels."),
2815
- name: z14.string().min(1).max(128).optional().describe("New label."),
2816
- active: z14.boolean().optional().describe("false silences without deleting."),
2817
- events: z14.array(z14.enum(NOTIFICATION_EVENTS)).optional().describe("Replaces the full subscription list.")
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: z14.number().int().positive().describe("Numeric 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: z14.number().int().positive().describe("Numeric 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 z15 } from "zod";
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: z15.string().min(1).max(60).describe("Project name (1\u201360 chars)."),
2940
- description: z15.string().max(500).optional().describe("Short description (\u2264500 chars)."),
2941
- region: z15.enum(AVAILABLE_REGION_IDS).optional().describe("Region: eu-central-1 (Falkenstein) \u2014 currently the only available 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: z15.string().describe("Project publicId."),
2975
- name: z15.string().min(1).max(60).optional().describe("New name (1\u201360 chars)."),
2976
- description: z15.string().max(500).optional().describe("New description (\u2264500 chars).")
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: z15.string().describe("Project publicId (e.g. prj_abc123).")
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 z16 } from "zod";
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: z16.union([z16.number().int().positive(), z16.string()]).describe('Service \u2014 publicId ("svc_\u2026") or numeric 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: z16.union([z16.number().int().positive(), z16.string()]).describe('Consuming service \u2014 publicId ("svc_\u2026") or numeric id.'),
3121
- resource_type: z16.enum(RESOURCE_LINK_TYPES).describe("Kind of resource being linked."),
3122
- resource_id: z16.number().int().positive().describe("NUMERIC id of the resource (e.g. database.id) \u2014 not the publicId."),
3123
- alias: z16.string().min(1).max(48).regex(
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: z16.union([z16.number().int().positive(), z16.string()]).describe('Service \u2014 publicId ("svc_\u2026") or numeric id.'),
3160
- link_id: z16.number().int().positive().describe("Numeric linkId from list_service_resources (the link's own `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 z17 } from "zod";
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: z17.union([z17.number().int().positive(), z17.string()]).optional().describe("Project filter \u2014 numeric id or publicId."),
3430
- environment_id: z17.union([z17.number().int().positive(), z17.string()]).optional().describe("Environment filter \u2014 numeric id or publicId."),
3431
- status: z17.enum(["active", "deploying", "suspended", "failed", "not_deployed"]).optional().describe("Filter by current runtime status."),
3432
- type: z17.enum(["web_service", "private_service", "worker", "cron_job", "static_site"]).optional().describe("Filter by service type."),
3433
- dev_environment: z17.boolean().optional().describe(
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: z17.union([z17.number().int().positive(), z17.string()]).describe("Target project \u2014 numeric id or publicId."),
3507
- name: z17.string().min(1).max(100).describe("Service name (1\u2013100 chars)."),
3508
- type: z17.enum(SERVICE_TYPES).describe("Service type."),
3509
- docker_image: z17.string().max(500).optional().describe(
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: z17.number().int().positive().optional().describe("Linked GitHub repo numeric id. Mutually exclusive with docker_image."),
3513
- branch: z17.string().max(200).optional().describe('Git branch (default "main").'),
3514
- install_command: z17.string().max(1e3).optional().describe("Install shell command."),
3515
- build_command: z17.string().max(1e3).optional().describe("Build shell command."),
3516
- start_command: z17.string().max(1e3).optional().describe("Start shell command (required for web/private services without an image)."),
3517
- cron_schedule: z17.string().max(100).optional().describe("Cron expression \u2014 required for cron_job."),
3518
- publish_path: z17.string().max(500).optional().describe("Static-site output dir."),
3519
- runtime: z17.string().max(50).optional().describe("Runtime hint (node/bun/python/\u2026)."),
3520
- port: z17.number().int().min(1).max(65535).optional().describe(
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: z17.string().max(64).optional().describe(
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: z17.enum(SERVICE_PLANS).optional().describe('Service size (default "micro").'),
3527
- environment_id: z17.union([z17.number().int().positive(), z17.string()]).optional().describe("Bind to a specific environment; defaults to Production."),
3528
- auto_deploy: z17.boolean().optional().describe("Trigger the first deploy immediately (default true)."),
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: z17.union([z17.number().int().positive(), z17.string()]).describe("Target project \u2014 numeric id or publicId."),
3600
- name: z17.string().min(1).max(100).optional().describe('Service name (default "dev-environment").'),
3601
- plan: z17.enum(SERVICE_PLANS).optional().describe(
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: z17.number().int().min(10).max(10240).optional().describe("/workspace volume size in GB (default 10, min 10, max 10240)."),
3605
- hoststack_api_key: z17.string().optional().describe("Value for HOSTSTACK_API_KEY (enables the hoststack MCP in-container)."),
3606
- poststack_api_key: z17.string().optional().describe("Value for POSTSTACK_API_KEY (enables the poststack MCP in-container)."),
3607
- repo_url: z17.string().max(500).optional().describe(
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: z17.string().max(200).optional().describe("Branch to clone (with repo_url)."),
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: z17.union([z17.number().int().positive(), z17.string()]).describe("Source service to debug \u2014 numeric id or publicId."),
3728
- include_database_clone: z17.boolean().optional().describe("Clone the linked database so the app runs on copied data (default true)."),
3729
- name: z17.string().min(1).max(100).optional().describe('Dev box name (default "<source>-dev").')
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: z17.union([z17.number().int().positive(), z17.string()]).describe("The dev box to tear down \u2014 numeric id or publicId.")
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: z17.union([z17.number().int().positive(), z17.string()]).describe("The box to resize \u2014 numeric id or publicId."),
3807
- size: z17.enum(SERVICE_PLANS).describe(
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: z17.string().min(1).max(100).optional().describe(
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: z17.enum(["github_repo", "url", "blank"]).describe("Where the code comes from."),
3954
- github_repo_id: z17.number().int().positive().optional().describe('Connected GitHub repo id (required when source_kind="github_repo").'),
3955
- clone_url: z17.string().url().optional().describe('http(s) git clone URL (required when source_kind="url").'),
3956
- branch: z17.string().min(1).max(255).optional().describe("Branch to clone."),
3957
- databases: z17.array(z17.enum(["postgres", "redis", "meilisearch"])).optional().describe("Companion services to attach (fresh + empty)."),
3958
- plan: z17.enum(SERVICE_PLANS).optional().describe(
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: z17.array(
3962
- z17.object({
3963
- provider: z17.enum(["claude", "codex", "opencode"]),
3964
- account_id: z17.number().int().positive()
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: z17.string().describe("Service publicId (e.g. svc_abc123).")
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: z17.string().describe("Service publicId.")
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: z17.string().describe("Service publicId."),
4104
- from: z17.string().optional().describe('ISO-8601 lower bound or relative offset (e.g. "-1h", "-2d").'),
4105
- to: z17.string().optional().describe("ISO-8601 upper bound; defaults to now.")
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: z17.string().describe("Service publicId."),
4150
- name: z17.string().min(1).max(60).describe("New service name (1\u201360 chars).")
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: z17.string().describe("Service publicId."),
4200
- install_command: z17.string().nullable().optional().describe("Install shell command. Null clears."),
4201
- build_command: z17.string().nullable().optional().describe("Build shell command. Null clears."),
4202
- start_command: z17.string().nullable().optional().describe("Start shell command. Null clears."),
4203
- branch: z17.string().optional().describe("Git branch to track."),
4204
- root_directory: z17.string().optional().describe("Build context root."),
4205
- dockerfile_path: z17.string().nullable().optional().describe("Path to Dockerfile relative to root. Null clears."),
4206
- auto_deploy: z17.boolean().optional().describe("Auto-deploy on push."),
4207
- health_check_path: z17.string().nullable().optional().describe('HTTP health-check path (e.g. "/health"). Null = TCP-only check.'),
4208
- health_check_enabled: z17.boolean().optional().describe("Toggle health checking on/off."),
4209
- health_check_interval: z17.number().int().min(5).max(300).optional().describe("How often the check runs, in seconds (5\u2013300)."),
4210
- health_check_timeout: z17.number().int().min(1).max(60).optional().describe("Single-attempt timeout in seconds (1\u201360)."),
4211
- health_check_grace_period_sec: z17.number().int().min(1).max(1800).optional().describe(
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: z17.number().int().min(128).max(16384).optional().describe("Container memory cap in MB (128\u201316384)."),
4215
- cpu_shares: z17.number().int().min(128).max(4096).optional().describe("Relative CPU weight (128\u20134096)."),
4216
- disk_size_gb: z17.number().int().min(1).max(100).optional().describe("Ephemeral disk size in GB (1\u2013100)."),
4217
- port: z17.number().int().min(1).max(65535).optional().describe("Container port the platform forwards traffic to."),
4218
- protocol: z17.enum(["http", "tcp"]).optional().describe("Traffic protocol."),
4219
- restart_policy: z17.enum(["always", "on-failure", "no"]).optional().describe("Docker restart policy."),
4220
- deploy_strategy: z17.enum(["rolling", "recreate"]).optional().describe(
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: z17.string().optional().describe("Shell command run before the new release accepts traffic."),
4224
- instance_count: z17.number().int().positive().max(50).optional().describe("Pin min and max instances to this value (1\u201350)."),
4225
- min_instances: z17.number().int().min(0).max(50).optional().describe("Autoscale lower bound. Use with max_instances for a range."),
4226
- max_instances: z17.number().int().min(1).max(50).optional().describe("Autoscale upper bound. Use with min_instances for a range."),
4227
- scale_cpu_threshold: z17.number().int().min(10).max(100).optional().describe("Autoscale CPU trigger percentage (10\u2013100)."),
4228
- scale_memory_threshold: z17.number().int().min(10).max(100).optional().describe("Autoscale memory trigger percentage (10\u2013100)."),
4229
- log_filter_rules: z17.array(
4230
- z17.object({
4231
- pattern: z17.string().min(1).max(200),
4232
- action: z17.enum(["drop", "downgrade"])
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: z17.string().describe("Service publicId.")
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: z17.string().describe("Service publicId.")
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: z17.string().describe("Service publicId.")
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: z17.string().describe("Service publicId."),
4417
- lines: z17.number().int().positive().max(1e3).optional().describe("Tail size; default 200, hard cap 1000."),
4418
- since: z17.string().optional().describe('ISO-8601 timestamp or relative offset (e.g. "-5m", "-1h").'),
4419
- until: z17.string().optional().describe("ISO-8601 timestamp or relative offset upper bound."),
4420
- stream: z17.enum(["stdout", "stderr"]).optional().describe("Restrict to one stream."),
4421
- level: z17.enum(["stdout", "stderr", "trace", "debug", "info", "warn", "error", "fatal"]).optional().describe(
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: z17.string().max(100).optional().describe("Case-insensitive substring filter."),
4425
- count_only: z17.boolean().optional().describe("When true, return only { count } \u2014 skips the log payload.")
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: z17.array(z17.string()).min(1).max(10).describe("Service publicIds (1\u201310). Hard cap 10 to bound parallel work."),
4475
- lines_per_service: z17.number().int().positive().max(500).optional().describe("Tail size per service; default 100, hard cap 500."),
4476
- since: z17.string().optional().describe('ISO-8601 timestamp or relative offset (e.g. "-5m", "-1h").'),
4477
- until: z17.string().optional().describe("ISO-8601 timestamp or relative offset upper bound."),
4478
- stream: z17.enum(["stdout", "stderr"]).optional().describe("Restrict to one stream."),
4479
- level: z17.enum(["stdout", "stderr", "trace", "debug", "info", "warn", "error", "fatal"]).optional().describe("Structured log level filter (same as get_service_logs)."),
4480
- search: z17.string().max(100).optional().describe("Case-insensitive substring filter."),
4481
- count_only: z17.boolean().optional().describe("When true, return only counts per service \u2014 skips the log payload.")
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 z18 } from "zod";
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: z18.number().int().positive() },
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: z18.number().int().positive(),
4592
- enabled: z18.boolean().optional(),
4593
- path: z18.string().max(500).optional().describe('Must start with /. Default "/".'),
4594
- method: z18.enum(["GET", "HEAD"]).optional(),
4595
- expectedStatus: z18.number().int().min(100).max(599).optional(),
4596
- timeoutMs: z18.number().int().min(1e3).max(6e4).optional(),
4597
- intervalSeconds: z18.number().int().min(30).max(3600).optional(),
4598
- failureThreshold: z18.number().int().min(1).max(10).optional()
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: z18.number().int().positive() },
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 z19 } from "zod";
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: z19.string().describe("Service publicId (e.g. svc_abc123).")
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: z19.string().describe("Service publicId."),
4684
- name: z19.string().min(1).max(64).regex(/^[a-z0-9-]+$/).describe("Volume name (lowercase alphanumeric + hyphens)."),
4685
- mount_path: z19.string().startsWith("/").max(500).describe("In-container mount path (absolute)."),
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: z19.number().int().min(MIN_VOLUME_SIZE_GB).max(100).optional().describe(
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: z19.string().describe("Service publicId."),
4730
- volume_id: z19.string().describe("Volume publicId (e.g. vol_\u2026)."),
4731
- mount_path: z19.string().startsWith("/").max(500).optional().describe("New mount path."),
4732
- 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).`)
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: z19.string().describe("Service publicId."),
4771
- volume_id: z19.string().describe("Volume publicId.")
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();