@hoststack.dev/mcp 0.24.0 → 0.25.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.24.0" : "0.0.0-dev";
11
+ var MCP_VERSION = true ? "0.25.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((z22) => z22.publicId === input.zone_id);
1396
+ const match = zones2.find((z23) => z23.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((z22) => z22.domainName.toLowerCase() === candidate);
1410
+ const match = zones.find((z23) => z23.domainName.toLowerCase() === candidate);
1411
1411
  if (match && match.status !== "deleting") {
1412
1412
  return { publicId: match.publicId, domainName: match.domainName };
1413
1413
  }
@@ -2893,19 +2893,69 @@ defineTool({
2893
2893
  });
2894
2894
 
2895
2895
  // src/tools/github.ts
2896
+ import { z as z14 } from "zod";
2897
+ function shapeRepo(repo) {
2898
+ return {
2899
+ id: repo.id,
2900
+ fullName: repo.fullName,
2901
+ defaultBranch: repo.defaultBranch,
2902
+ private: repo.isPrivate,
2903
+ url: repo.htmlUrl,
2904
+ ...repo.language ? { language: repo.language } : {}
2905
+ };
2906
+ }
2907
+ defineTool({
2908
+ name: "list_github_repos",
2909
+ category: "github",
2910
+ description: [
2911
+ "Every GitHub repository this team can deploy, with the `id` create_service takes \u2014 and the `owner/name` you can pass instead of it.",
2912
+ "",
2913
+ "When to use: before create_service or create_standalone_dev_environment with a repo source. `github_repo_id` is a HostStack row id, NOT the id GitHub shows \u2014 it appears in no clone URL and no GitHub API response, so it cannot be derived and must not be guessed. (Guessing used to be survivable-looking: the create returned 200 and the mistake surfaced one step later as `Invalid git clone URL` from the deploy. It is now refused at create time.)",
2914
+ "",
2915
+ "Inputs:",
2916
+ ' - q (optional): case-insensitive substring of the full name ("owner/name"). Omit for everything.',
2917
+ "",
2918
+ "Returns: { items: [{ id, fullName, defaultBranch, private, url, language? }], total } \u2014 `total` is the count before `q` filtered it.",
2919
+ "",
2920
+ "If the repo you just pushed is missing, call sync_github_repos first: HostStack cannot see a brand-new repository until an installation re-sync runs.",
2921
+ "",
2922
+ 'Example: list_github_repos({ q: "stenshoppen" }) \u2192 { items: [{ id: 143, fullName: "acme/stenshoppen", defaultBranch: "master", private: true }], total: 45 }',
2923
+ 'Then either: create_service({ \u2026, github_repo: "acme/stenshoppen" }) \u2014 or the id, if you prefer.'
2924
+ ].join("\n"),
2925
+ input: {
2926
+ q: z14.string().min(1).max(200).optional().describe('Case-insensitive substring of "owner/name". Omit to list everything.')
2927
+ },
2928
+ handler: async (args2, ctx) => {
2929
+ const teamId = await ctx.resolveTeamId();
2930
+ const { repos } = await ctx.api.get(
2931
+ `/api/github/${teamId}/repos`
2932
+ );
2933
+ const needle = args2.q?.trim().toLowerCase();
2934
+ const matched = needle ? repos.filter((r) => r.fullName.toLowerCase().includes(needle)) : repos;
2935
+ const items = matched.map(shapeRepo).sort((a, b) => a.fullName.localeCompare(b.fullName));
2936
+ if (repos.length === 0) {
2937
+ return respond({
2938
+ summary: "No GitHub repositories are connected to this team. Install the HostStack GitHub App from the dashboard (Settings \u2192 GitHub), then call sync_github_repos.",
2939
+ data: { items, total: 0 }
2940
+ });
2941
+ }
2942
+ const summary = needle ? items.length === 0 ? `No connected repository matches "${args2.q}" (${repos.length} connected). Push it, then call sync_github_repos.` : `${items.length} of ${repos.length} connected repositories match "${args2.q}".` : `${items.length} connected repositor${items.length === 1 ? "y" : "ies"}.`;
2943
+ return respond({ summary, data: { items, total: repos.length } });
2944
+ }
2945
+ });
2896
2946
  defineTool({
2897
2947
  name: "sync_github_repos",
2898
2948
  category: "github",
2899
2949
  description: [
2900
- "Re-sync the repository list from GitHub for every connected GitHub App installation.",
2950
+ "Re-sync the repository list from GitHub for every connected GitHub App installation, and return what that now exposes.",
2901
2951
  "",
2902
- "When to use: right after pushing a brand-new repository, HostStack cannot see it until an installation re-sync runs \u2014 so create_service with a github_repo_id would fail. Call this first. (Same operation as the dashboard's refresh icon on the repo picker.)",
2952
+ "When to use: right after pushing a brand-new repository, HostStack cannot see it until an installation re-sync runs \u2014 so create_service with a github_repo_id or github_repo would fail. Call this first. (Same operation as the dashboard's refresh icon on the repo picker.) To read the list WITHOUT re-syncing, call list_github_repos.",
2903
2953
  "",
2904
2954
  "No inputs.",
2905
2955
  "",
2906
- "Returns: { installations: number, repos: [{ account, count }], totalRepos } \u2014 the accounts synced and how many repos each now exposes.",
2956
+ "Returns: { installations: number, accounts: [{ account, count }], totalRepos, repos: [{ id, fullName }] } \u2014 `repos` carries the ids create_service takes, so a sync is enough to act on; it does not have to be followed by a list.",
2907
2957
  "",
2908
- 'Example: sync_github_repos({}) \u2192 { installations: 1, repos: [{ account: "acme", count: 12 }], totalRepos: 12 }'
2958
+ 'Example: sync_github_repos({}) \u2192 { installations: 1, accounts: [{ account: "acme", count: 12 }], totalRepos: 12, repos: [{ id: 143, fullName: "acme/stenshoppen" }, \u2026] }'
2909
2959
  ].join("\n"),
2910
2960
  input: {},
2911
2961
  handler: async (_args, ctx) => {
@@ -2919,23 +2969,29 @@ defineTool({
2919
2969
  );
2920
2970
  }
2921
2971
  const perAccount = [];
2922
- let totalRepos = 0;
2972
+ const synced = [];
2923
2973
  for (const inst of installations) {
2924
2974
  const { repos } = await ctx.api.post(
2925
2975
  `/api/github/${teamId}/installations/${inst.id}/sync`
2926
2976
  );
2927
2977
  perAccount.push({ account: inst.accountLogin, count: repos.length });
2928
- totalRepos += repos.length;
2978
+ for (const repo of repos) synced.push({ id: repo.id, fullName: repo.fullName });
2929
2979
  }
2980
+ synced.sort((a, b) => a.fullName.localeCompare(b.fullName));
2930
2981
  return respond({
2931
- summary: `Synced ${totalRepos} repositories across ${installations.length} installation(s).`,
2932
- data: { installations: installations.length, repos: perAccount, totalRepos }
2982
+ summary: `Synced ${synced.length} repositories across ${installations.length} installation(s).`,
2983
+ data: {
2984
+ installations: installations.length,
2985
+ accounts: perAccount,
2986
+ totalRepos: synced.length,
2987
+ repos: synced
2988
+ }
2933
2989
  });
2934
2990
  }
2935
2991
  });
2936
2992
 
2937
2993
  // src/tools/issue-reports.ts
2938
- import { z as z14 } from "zod";
2994
+ import { z as z15 } from "zod";
2939
2995
  defineTool({
2940
2996
  name: "report_issue",
2941
2997
  category: "support",
@@ -2957,16 +3013,16 @@ defineTool({
2957
3013
  "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' } }"
2958
3014
  ].join("\n"),
2959
3015
  input: {
2960
- title: z14.string().min(1).max(300).describe("One-line summary of the fault."),
2961
- description: z14.string().min(1).max(1e4).describe("What happened, what you expected instead, and what you already ruled out."),
2962
- severity: z14.enum(["low", "normal", "high", "urgent"]).optional().describe(
3016
+ title: z15.string().min(1).max(300).describe("One-line summary of the fault."),
3017
+ description: z15.string().min(1).max(1e4).describe("What happened, what you expected instead, and what you already ruled out."),
3018
+ severity: z15.enum(["low", "normal", "high", "urgent"]).optional().describe(
2963
3019
  'Default normal. Use high/urgent only when something is DOWN or losing data \u2014 not for "this is annoying".'
2964
3020
  ),
2965
- serviceId: z14.number().int().positive().optional().describe(
3021
+ serviceId: z15.number().int().positive().optional().describe(
2966
3022
  "The affected service. Attaches the service, its latest deploy, and that deploy log tail automatically."
2967
3023
  ),
2968
- deployId: z14.number().int().positive().optional().describe("Pin a specific deploy instead of the service\u2019s most recent one."),
2969
- databaseId: z14.number().int().positive().optional().describe("The affected database.")
3024
+ deployId: z15.number().int().positive().optional().describe("Pin a specific deploy instead of the service\u2019s most recent one."),
3025
+ databaseId: z15.number().int().positive().optional().describe("The affected database.")
2970
3026
  },
2971
3027
  handler: async (args2, ctx) => {
2972
3028
  const teamId = await ctx.resolveTeamId();
@@ -3054,7 +3110,7 @@ defineTool({
3054
3110
  });
3055
3111
 
3056
3112
  // src/tools/notifications.ts
3057
- import { z as z15 } from "zod";
3113
+ import { z as z16 } from "zod";
3058
3114
  var NOTIFICATION_EVENTS = [
3059
3115
  "deploy.started",
3060
3116
  "deploy.succeeded",
@@ -3080,6 +3136,7 @@ var NOTIFICATION_EVENTS = [
3080
3136
  "error.issue_regressed",
3081
3137
  "git.auth_failed",
3082
3138
  "cron.execution_failed",
3139
+ "cron.schedule_missed",
3083
3140
  "workflow.failed",
3084
3141
  "devenv.agent.needs_input",
3085
3142
  "devenv.agent.finished",
@@ -3148,10 +3205,10 @@ defineTool({
3148
3205
  "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'] })"
3149
3206
  ].join("\n"),
3150
3207
  input: {
3151
- type: z15.enum(["slack", "discord", "email"]).describe("Channel type."),
3152
- name: z15.string().min(1).max(128).describe("Human-readable label."),
3153
- webhook_url: z15.string().max(500).describe("Slack/Discord webhook URL or email address (when type=email)."),
3154
- events: z15.array(z15.enum(NOTIFICATION_EVENTS)).describe(
3208
+ type: z16.enum(["slack", "discord", "email"]).describe("Channel type."),
3209
+ name: z16.string().min(1).max(128).describe("Human-readable label."),
3210
+ webhook_url: z16.string().max(500).describe("Slack/Discord webhook URL or email address (when type=email)."),
3211
+ events: z16.array(z16.enum(NOTIFICATION_EVENTS)).describe(
3155
3212
  "List of events the channel subscribes to. Empty list = subscribe to nothing."
3156
3213
  )
3157
3214
  },
@@ -3189,10 +3246,10 @@ defineTool({
3189
3246
  "Example: update_notification_channel({ channel_id: 3, events: ['deploy.failed', 'service.restart_failed', 'git.auth_failed'] })"
3190
3247
  ].join("\n"),
3191
3248
  input: {
3192
- channel_id: z15.number().int().positive().describe("Numeric channel id from list_notification_channels."),
3193
- name: z15.string().min(1).max(128).optional().describe("New label."),
3194
- active: z15.boolean().optional().describe("false silences without deleting."),
3195
- events: z15.array(z15.enum(NOTIFICATION_EVENTS)).optional().describe("Replaces the full subscription list.")
3249
+ channel_id: z16.number().int().positive().describe("Numeric channel id from list_notification_channels."),
3250
+ name: z16.string().min(1).max(128).optional().describe("New label."),
3251
+ active: z16.boolean().optional().describe("false silences without deleting."),
3252
+ events: z16.array(z16.enum(NOTIFICATION_EVENTS)).optional().describe("Replaces the full subscription list.")
3196
3253
  },
3197
3254
  handler: async (args2, ctx) => {
3198
3255
  const teamId = await ctx.resolveTeamId();
@@ -3231,7 +3288,7 @@ defineTool({
3231
3288
  "Example: delete_notification_channel({ channel_id: 3 }) \u2192 { ok: true }"
3232
3289
  ].join("\n"),
3233
3290
  input: {
3234
- channel_id: z15.number().int().positive().describe("Numeric channel id.")
3291
+ channel_id: z16.number().int().positive().describe("Numeric channel id.")
3235
3292
  },
3236
3293
  handler: async (args2, ctx) => {
3237
3294
  const teamId = await ctx.resolveTeamId();
@@ -3258,7 +3315,7 @@ defineTool({
3258
3315
  "Example: test_notification_channel({ channel_id: 3 }) \u2192 { success: true }"
3259
3316
  ].join("\n"),
3260
3317
  input: {
3261
- channel_id: z15.number().int().positive().describe("Numeric channel id.")
3318
+ channel_id: z16.number().int().positive().describe("Numeric channel id.")
3262
3319
  },
3263
3320
  handler: async (args2, ctx) => {
3264
3321
  const teamId = await ctx.resolveTeamId();
@@ -3273,7 +3330,7 @@ defineTool({
3273
3330
  });
3274
3331
 
3275
3332
  // src/tools/projects.ts
3276
- import { z as z16 } from "zod";
3333
+ import { z as z17 } from "zod";
3277
3334
  var AVAILABLE_REGION_IDS = ["eu-central-1"];
3278
3335
  defineTool({
3279
3336
  name: "list_projects",
@@ -3314,9 +3371,9 @@ defineTool({
3314
3371
  'Example: create_project({ name: "billing-api", description: "Stripe webhooks", region: "eu-central-1" }) \u2192 { project: { id: 12, publicId: "prj_\u2026", \u2026 } }'
3315
3372
  ].join("\n"),
3316
3373
  input: {
3317
- name: z16.string().min(1).max(60).describe("Project name (1\u201360 chars)."),
3318
- description: z16.string().max(500).optional().describe("Short description (\u2264500 chars)."),
3319
- region: z16.enum(AVAILABLE_REGION_IDS).optional().describe("Region: eu-central-1 (Falkenstein) \u2014 currently the only available region.")
3374
+ name: z17.string().min(1).max(60).describe("Project name (1\u201360 chars)."),
3375
+ description: z17.string().max(500).optional().describe("Short description (\u2264500 chars)."),
3376
+ region: z17.enum(AVAILABLE_REGION_IDS).optional().describe("Region: eu-central-1 (Falkenstein) \u2014 currently the only available region.")
3320
3377
  },
3321
3378
  handler: async (args2, ctx) => {
3322
3379
  const teamId = await ctx.resolveTeamId();
@@ -3349,9 +3406,9 @@ defineTool({
3349
3406
  'Example: update_project({ project_id: "prj_abc", name: "billing-prod" }) \u2192 { project: { name: "billing-prod", \u2026 } }'
3350
3407
  ].join("\n"),
3351
3408
  input: {
3352
- project_id: z16.string().describe("Project publicId."),
3353
- name: z16.string().min(1).max(60).optional().describe("New name (1\u201360 chars)."),
3354
- description: z16.string().max(500).optional().describe("New description (\u2264500 chars).")
3409
+ project_id: z17.string().describe("Project publicId."),
3410
+ name: z17.string().min(1).max(60).optional().describe("New name (1\u201360 chars)."),
3411
+ description: z17.string().max(500).optional().describe("New description (\u2264500 chars).")
3355
3412
  },
3356
3413
  handler: async (args2, ctx) => {
3357
3414
  if (args2.name === void 0 && args2.description === void 0) {
@@ -3385,7 +3442,7 @@ defineTool({
3385
3442
  'Example: get_project({ project_id: "prj_abc" }) \u2192 { project: { id: 12, name: "billing", \u2026 } }'
3386
3443
  ].join("\n"),
3387
3444
  input: {
3388
- project_id: z16.string().describe("Project publicId (e.g. prj_abc123).")
3445
+ project_id: z17.string().describe("Project publicId (e.g. prj_abc123).")
3389
3446
  },
3390
3447
  handler: async (args2, ctx) => {
3391
3448
  const teamId = await ctx.resolveTeamId();
@@ -3397,7 +3454,7 @@ defineTool({
3397
3454
  });
3398
3455
 
3399
3456
  // src/tools/dev-tasks.ts
3400
- import { z as z17 } from "zod";
3457
+ import { z as z18 } from "zod";
3401
3458
  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.";
3402
3459
  defineTool({
3403
3460
  name: "list_dev_tasks",
@@ -3417,7 +3474,7 @@ defineTool({
3417
3474
  'Example: list_dev_tasks({ project_id: 26 }) \u2192 { items: [{ publicId: "task_\u2026", status: "idea", title: "Fix the footprint join" }] }'
3418
3475
  ].join("\n"),
3419
3476
  input: {
3420
- project_id: z17.union([z17.number().int().positive(), z17.string()]).describe('Project \u2014 publicId ("prj_\u2026") or numeric id.')
3477
+ project_id: z18.union([z18.number().int().positive(), z18.string()]).describe('Project \u2014 publicId ("prj_\u2026") or numeric id.')
3421
3478
  },
3422
3479
  handler: async (args2, ctx) => {
3423
3480
  const teamId = await ctx.resolveTeamId();
@@ -3446,7 +3503,7 @@ defineTool({
3446
3503
  '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" } }'
3447
3504
  ].join("\n"),
3448
3505
  input: {
3449
- task_id: z17.union([z17.number().int().positive(), z17.string()]).describe('Task \u2014 publicId ("task_\u2026") or numeric id.')
3506
+ task_id: z18.union([z18.number().int().positive(), z18.string()]).describe('Task \u2014 publicId ("task_\u2026") or numeric id.')
3450
3507
  },
3451
3508
  handler: async (args2, ctx) => {
3452
3509
  const teamId = await ctx.resolveTeamId();
@@ -3474,10 +3531,10 @@ defineTool({
3474
3531
  'Example: create_dev_task({ project_id: 26, service_id: 51, title: "Footprint coverage", body: "The BBRUUID join returns 17 of 49 \u2026" })'
3475
3532
  ].join("\n"),
3476
3533
  input: {
3477
- project_id: z17.union([z17.number().int().positive(), z17.string()]).describe('Project \u2014 publicId ("prj_\u2026") or numeric id.'),
3478
- title: z17.string().min(1).max(200).describe("One-line title, \u2264200 chars."),
3479
- body: z17.string().max(2e4).optional().describe("The prompt handed to the agent. Markdown, \u226420 000 chars."),
3480
- service_id: z17.union([z17.number().int().positive(), z17.string()]).optional().describe(
3534
+ project_id: z18.union([z18.number().int().positive(), z18.string()]).describe('Project \u2014 publicId ("prj_\u2026") or numeric id.'),
3535
+ title: z18.string().min(1).max(200).describe("One-line title, \u2264200 chars."),
3536
+ body: z18.string().max(2e4).optional().describe("The prompt handed to the agent. Markdown, \u226420 000 chars."),
3537
+ service_id: z18.union([z18.number().int().positive(), z18.string()]).optional().describe(
3481
3538
  'Dev box to pin it to \u2014 publicId ("svc_\u2026") or numeric id. Omit for a loose idea.'
3482
3539
  )
3483
3540
  },
@@ -3523,13 +3580,13 @@ defineTool({
3523
3580
  'Example: update_dev_task({ task_id: "task_hy1i2jdp\u2026", status: "done" }) \u2192 { data: { status: "done" } }'
3524
3581
  ].join("\n"),
3525
3582
  input: {
3526
- task_id: z17.union([z17.number().int().positive(), z17.string()]).describe('Task \u2014 publicId ("task_\u2026") or numeric id.'),
3527
- status: z17.enum(["idea", "done"]).optional().describe("The only two a person may set. The runner owns the rest of the lifecycle."),
3528
- title: z17.string().min(1).max(200).optional().describe("New title."),
3529
- body: z17.string().max(2e4).optional().describe("New prompt body."),
3530
- 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."),
3531
- provider: z17.enum(["claude", "codex", "opencode"]).nullable().optional().describe("Which agent runs this task, or null to leave it to the box."),
3532
- permission_mode: z17.enum(["acceptEdits", "bypassPermissions", "default"]).optional().describe(
3583
+ task_id: z18.union([z18.number().int().positive(), z18.string()]).describe('Task \u2014 publicId ("task_\u2026") or numeric id.'),
3584
+ status: z18.enum(["idea", "done"]).optional().describe("The only two a person may set. The runner owns the rest of the lifecycle."),
3585
+ title: z18.string().min(1).max(200).optional().describe("New title."),
3586
+ body: z18.string().max(2e4).optional().describe("New prompt body."),
3587
+ service_id: z18.union([z18.number().int().positive(), z18.string()]).nullable().optional().describe("Pin to a dev box, or null to unpin it back to a loose idea."),
3588
+ provider: z18.enum(["claude", "codex", "opencode"]).nullable().optional().describe("Which agent runs this task, or null to leave it to the box."),
3589
+ permission_mode: z18.enum(["acceptEdits", "bypassPermissions", "default"]).optional().describe(
3533
3590
  "How the agent may act. bypassPermissions asks for nothing \u2014 set it only on request."
3534
3591
  )
3535
3592
  },
@@ -3554,7 +3611,7 @@ defineTool({
3554
3611
  });
3555
3612
 
3556
3613
  // src/tools/resource-links.ts
3557
- import { z as z18 } from "zod";
3614
+ import { z as z19 } from "zod";
3558
3615
  var RESOURCE_LINK_TYPES = [
3559
3616
  "database",
3560
3617
  "object_storage",
@@ -3615,7 +3672,7 @@ defineTool({
3615
3672
  'Example: list_service_resources({ service_id: "svc_abc" }) \u2192 { items: [{ id: 7, resourceType: "database", resourceId: 42, alias: "APP_DB" }] }'
3616
3673
  ].join("\n"),
3617
3674
  input: {
3618
- service_id: z18.union([z18.number().int().positive(), z18.string()]).describe('Service \u2014 publicId ("svc_\u2026") or numeric id.')
3675
+ service_id: z19.union([z19.number().int().positive(), z19.string()]).describe('Service \u2014 publicId ("svc_\u2026") or numeric id.')
3619
3676
  },
3620
3677
  handler: async (args2, ctx) => {
3621
3678
  const teamId = await ctx.resolveTeamId();
@@ -3652,10 +3709,10 @@ defineTool({
3652
3709
  '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" } }'
3653
3710
  ].join("\n"),
3654
3711
  input: {
3655
- service_id: z18.union([z18.number().int().positive(), z18.string()]).describe('Consuming service \u2014 publicId ("svc_\u2026") or numeric id.'),
3656
- resource_type: z18.enum(RESOURCE_LINK_TYPES).describe("Kind of resource being linked."),
3657
- resource_id: z18.number().int().positive().describe("NUMERIC id of the resource (e.g. database.id) \u2014 not the publicId."),
3658
- alias: z18.string().min(1).max(48).regex(
3712
+ service_id: z19.union([z19.number().int().positive(), z19.string()]).describe('Consuming service \u2014 publicId ("svc_\u2026") or numeric id.'),
3713
+ resource_type: z19.enum(RESOURCE_LINK_TYPES).describe("Kind of resource being linked."),
3714
+ resource_id: z19.number().int().positive().describe("NUMERIC id of the resource (e.g. database.id) \u2014 not the publicId."),
3715
+ alias: z19.string().min(1).max(48).regex(
3659
3716
  /^[A-Z][A-Z0-9_]*$/,
3660
3717
  "Alias must be uppercase letters, digits and underscores, starting with a letter."
3661
3718
  ).describe('Uppercase env-var prefix, e.g. "APP_DB". Unique within the service.')
@@ -3691,8 +3748,8 @@ defineTool({
3691
3748
  'Example: unlink_resource_from_service({ service_id: "svc_abc", link_id: 7 }) \u2192 { ok: true }'
3692
3749
  ].join("\n"),
3693
3750
  input: {
3694
- service_id: z18.union([z18.number().int().positive(), z18.string()]).describe('Service \u2014 publicId ("svc_\u2026") or numeric id.'),
3695
- link_id: z18.number().int().positive().describe("Numeric linkId from list_service_resources (the link's own `id`).")
3751
+ service_id: z19.union([z19.number().int().positive(), z19.string()]).describe('Service \u2014 publicId ("svc_\u2026") or numeric id.'),
3752
+ link_id: z19.number().int().positive().describe("Numeric linkId from list_service_resources (the link's own `id`).")
3696
3753
  },
3697
3754
  handler: async (args2, ctx) => {
3698
3755
  const teamId = await ctx.resolveTeamId();
@@ -3704,7 +3761,7 @@ defineTool({
3704
3761
  });
3705
3762
 
3706
3763
  // src/tools/services.ts
3707
- import { z as z19 } from "zod";
3764
+ import { z as z20 } from "zod";
3708
3765
 
3709
3766
  // src/lib/app-templates.ts
3710
3767
  var MCP_APP_TEMPLATES = [
@@ -3956,16 +4013,16 @@ defineTool({
3956
4013
  ' - type: "web_service" | "private_service" | "worker" | "cron_job" | "static_site".',
3957
4014
  " - dev_environment: include Dev Boxes in the results (excluded by default).",
3958
4015
  "",
3959
- "Returns: { items: Service[] } \u2014 each service includes id, publicId, name, type, status, projectId, repoUrl, branch, runtime, createdAt.",
4016
+ "Returns: { items: Service[] } \u2014 each service includes id, publicId, name, type, status, projectId, branch, runtime, createdAt, and, for a source-built service, repoProvider/repoFullName/repoUrl (what it actually builds \u2014 the stored id alone names no repository).",
3960
4017
  "",
3961
4018
  'Example: list_services({ status: "failed" }) \u2192 only services that need attention.'
3962
4019
  ].join("\n"),
3963
4020
  input: {
3964
- project_id: z19.union([z19.number().int().positive(), z19.string()]).optional().describe("Project filter \u2014 numeric id or publicId."),
3965
- environment_id: z19.union([z19.number().int().positive(), z19.string()]).optional().describe("Environment filter \u2014 numeric id or publicId."),
3966
- status: z19.enum(["active", "deploying", "suspended", "failed", "not_deployed"]).optional().describe("Filter by current runtime status."),
3967
- type: z19.enum(["web_service", "private_service", "worker", "cron_job", "static_site"]).optional().describe("Filter by service type."),
3968
- dev_environment: z19.boolean().optional().describe(
4021
+ project_id: z20.union([z20.number().int().positive(), z20.string()]).optional().describe("Project filter \u2014 numeric id or publicId."),
4022
+ environment_id: z20.union([z20.number().int().positive(), z20.string()]).optional().describe("Environment filter \u2014 numeric id or publicId."),
4023
+ status: z20.enum(["active", "deploying", "suspended", "failed", "not_deployed"]).optional().describe("Filter by current runtime status."),
4024
+ type: z20.enum(["web_service", "private_service", "worker", "cron_job", "static_site"]).optional().describe("Filter by service type."),
4025
+ dev_environment: z20.boolean().optional().describe(
3969
4026
  "Include agentic Dev Boxes in the results (excluded by default; see list_dev_environments)."
3970
4027
  )
3971
4028
  },
@@ -4006,7 +4063,7 @@ defineTool({
4006
4063
  name: "create_service",
4007
4064
  category: "services",
4008
4065
  description: [
4009
- "Create a new service in a project. Source is either a connected git repo (github_repo_id) OR a pre-built docker_image \u2014 never both.",
4066
+ 'Create a new service in a project. Source is either a connected git repo (github_repo "owner/name", or github_repo_id) OR a pre-built docker_image \u2014 never both.',
4010
4067
  "",
4011
4068
  "When to use: the user wants to deploy something new. For a one-command AI dev environment specifically, prefer create_dev_environment (it also attaches the /workspace volume and sets the MCP keys).",
4012
4069
  "",
@@ -4019,7 +4076,8 @@ defineTool({
4019
4076
  " - name: service name (1\u2013100 chars).",
4020
4077
  ' - type: "web_service" | "private_service" | "worker" | "cron_job" | "static_site".',
4021
4078
  " - docker_image (optional): pre-built image ref to deploy instead of building from source.",
4022
- " - github_repo_id (optional): connect a previously-linked GitHub repo by numeric id.",
4079
+ ' - github_repo (optional): connect a connected GitHub repo by name \u2014 "owner/name". Preferred over github_repo_id: it is what you actually know, and a wrong name is refused here instead of failing the deploy.',
4080
+ " - github_repo_id (optional): the same repo by its HostStack numeric id (from list_github_repos). NOT GitHub's id. Mutually exclusive with github_repo.",
4023
4081
  " - git_repo_url (optional): a PUBLIC https git URL, for a repo that is not connected through GitHub. Private repos do not work here \u2014 they need a provider connection, because this field carries no credentials and embedding them in the URL is rejected. Mutually exclusive with github_repo_id and docker_image.",
4024
4082
  ' - branch (optional): git branch (default "main").',
4025
4083
  " - install_command / build_command / start_command (optional): build/run shell commands. start_command is required for web/private services without a docker_image.",
@@ -4035,36 +4093,42 @@ defineTool({
4035
4093
  "",
4036
4094
  "Returns: { service: Service, deployId: number | null }.",
4037
4095
  "",
4038
- 'Example: create_service({ project_id: "prj_abc", name: "api", type: "web_service", github_repo_id: 42 }) \u2192 { service: { publicId: "svc_\u2026" }, deployId: 1234 }',
4096
+ 'Example: create_service({ project_id: "prj_abc", name: "api", type: "web_service", github_repo: "acme/api" }) \u2192 { service: { publicId: "svc_\u2026", repoUrl: "https://github.com/acme/api" }, deployId: 1234 }',
4097
+ "Repo not listed by list_github_repos? Call sync_github_repos \u2014 a repository pushed since the last sync is invisible until then.",
4039
4098
  '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.'
4040
4099
  ].join("\n"),
4041
4100
  input: {
4042
- project_id: z19.union([z19.number().int().positive(), z19.string()]).describe("Target project \u2014 numeric id or publicId."),
4043
- name: z19.string().min(1).max(100).describe("Service name (1\u2013100 chars)."),
4044
- type: z19.enum(SERVICE_TYPES).describe("Service type."),
4045
- docker_image: z19.string().max(500).optional().describe(
4101
+ project_id: z20.union([z20.number().int().positive(), z20.string()]).describe("Target project \u2014 numeric id or publicId."),
4102
+ name: z20.string().min(1).max(100).describe("Service name (1\u2013100 chars)."),
4103
+ type: z20.enum(SERVICE_TYPES).describe("Service type."),
4104
+ docker_image: z20.string().max(500).optional().describe(
4046
4105
  "Pre-built APPLICATION image ref. Mutually exclusive with github_repo_id. Not for databases \u2014 use create_database for postgres/redis/mysql/mariadb/mongodb."
4047
4106
  ),
4048
- github_repo_id: z19.number().int().positive().optional().describe("Linked GitHub repo numeric id. Mutually exclusive with docker_image."),
4049
- git_repo_url: z19.string().max(2048).optional().describe(
4107
+ github_repo: z20.string().min(1).max(255).optional().describe(
4108
+ 'Connected GitHub repo as "owner/name" (see list_github_repos). Resolved server-side. Mutually exclusive with github_repo_id and docker_image.'
4109
+ ),
4110
+ github_repo_id: z20.number().int().positive().optional().describe(
4111
+ "HostStack row id of a connected GitHub repo (list_github_repos) \u2014 not GitHub's own id. Prefer github_repo. Mutually exclusive with docker_image."
4112
+ ),
4113
+ git_repo_url: z20.string().max(2048).optional().describe(
4050
4114
  "Public https git URL. No embedded credentials \u2014 private repos need a provider connection. Mutually exclusive with github_repo_id and docker_image."
4051
4115
  ),
4052
- branch: z19.string().max(200).optional().describe('Git branch (default "main").'),
4053
- install_command: z19.string().max(1e3).optional().describe("Install shell command."),
4054
- build_command: z19.string().max(1e3).optional().describe("Build shell command."),
4055
- start_command: z19.string().max(1e3).optional().describe("Start shell command (required for web/private services without an image)."),
4056
- cron_schedule: z19.string().max(100).optional().describe("Cron expression \u2014 required for cron_job."),
4057
- publish_path: z19.string().max(500).optional().describe("Static-site output dir."),
4058
- runtime: z19.string().max(50).optional().describe("Runtime hint (node/bun/python/\u2026)."),
4059
- port: z19.number().int().min(1).max(65535).optional().describe(
4116
+ branch: z20.string().max(200).optional().describe('Git branch (default "main").'),
4117
+ install_command: z20.string().max(1e3).optional().describe("Install shell command."),
4118
+ build_command: z20.string().max(1e3).optional().describe("Build shell command."),
4119
+ start_command: z20.string().max(1e3).optional().describe("Start shell command (required for web/private services without an image)."),
4120
+ cron_schedule: z20.string().max(100).optional().describe("Cron expression \u2014 required for cron_job."),
4121
+ publish_path: z20.string().max(500).optional().describe("Static-site output dir."),
4122
+ runtime: z20.string().max(50).optional().describe("Runtime hint (node/bun/python/\u2026)."),
4123
+ port: z20.number().int().min(1).max(65535).optional().describe(
4060
4124
  "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."
4061
4125
  ),
4062
- template_id: z19.string().max(64).optional().describe(
4126
+ template_id: z20.string().max(64).optional().describe(
4063
4127
  "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."
4064
4128
  ),
4065
- plan: z19.enum(SERVICE_PLANS).optional().describe('Service size (default "micro").'),
4066
- environment_id: z19.union([z19.number().int().positive(), z19.string()]).optional().describe("Bind to a specific environment; defaults to Production."),
4067
- auto_deploy: z19.boolean().optional().describe("Trigger the first deploy immediately (default true)."),
4129
+ plan: z20.enum(SERVICE_PLANS).optional().describe('Service size (default "micro").'),
4130
+ environment_id: z20.union([z20.number().int().positive(), z20.string()]).optional().describe("Bind to a specific environment; defaults to Production."),
4131
+ auto_deploy: z20.boolean().optional().describe("Trigger the first deploy immediately (default true)."),
4068
4132
  machine: machineInput
4069
4133
  },
4070
4134
  handler: async (args2, ctx) => {
@@ -4079,6 +4143,7 @@ defineTool({
4079
4143
  projectId
4080
4144
  };
4081
4145
  if (args2.docker_image !== void 0) input.dockerImage = args2.docker_image;
4146
+ if (args2.github_repo !== void 0) input.githubRepo = args2.github_repo;
4082
4147
  if (args2.github_repo_id !== void 0) input.githubRepoId = args2.github_repo_id;
4083
4148
  if (args2.git_repo_url !== void 0) input.gitRepoUrl = args2.git_repo_url;
4084
4149
  if (args2.branch !== void 0) input.branch = args2.branch;
@@ -4136,18 +4201,18 @@ defineTool({
4136
4201
  'Example: create_dev_environment({ project_id: "prj_abc", name: "scratch", hoststack_api_key: "hs_live_\u2026" })'
4137
4202
  ].join("\n"),
4138
4203
  input: {
4139
- project_id: z19.union([z19.number().int().positive(), z19.string()]).describe("Target project \u2014 numeric id or publicId."),
4140
- name: z19.string().min(1).max(100).optional().describe('Service name (default "dev-environment").'),
4141
- plan: z19.enum(SERVICE_PLANS).optional().describe(
4204
+ project_id: z20.union([z20.number().int().positive(), z20.string()]).describe("Target project \u2014 numeric id or publicId."),
4205
+ name: z20.string().min(1).max(100).optional().describe('Service name (default "dev-environment").'),
4206
+ plan: z20.enum(SERVICE_PLANS).optional().describe(
4142
4207
  'Box size (default "standard" \u2014 2 GB, the OOM-safe floor; a smaller plan is clamped up to "standard").'
4143
4208
  ),
4144
- disk_gb: z19.number().int().min(10).max(10240).optional().describe("/workspace volume size in GB (default 10, min 10, max 10240)."),
4145
- hoststack_api_key: z19.string().optional().describe("Value for HOSTSTACK_API_KEY (enables the hoststack MCP in-container)."),
4146
- poststack_api_key: z19.string().optional().describe("Value for POSTSTACK_API_KEY (enables the poststack MCP in-container)."),
4147
- repo_url: z19.string().max(500).optional().describe(
4209
+ disk_gb: z20.number().int().min(10).max(10240).optional().describe("/workspace volume size in GB (default 10, min 10, max 10240)."),
4210
+ hoststack_api_key: z20.string().optional().describe("Value for HOSTSTACK_API_KEY (enables the hoststack MCP in-container)."),
4211
+ poststack_api_key: z20.string().optional().describe("Value for POSTSTACK_API_KEY (enables the poststack MCP in-container)."),
4212
+ repo_url: z20.string().max(500).optional().describe(
4148
4213
  "Clone this git URL into /workspace on first boot (HTTPS, or SSH once a key is set)."
4149
4214
  ),
4150
- branch: z19.string().max(200).optional().describe("Branch to clone (with repo_url)."),
4215
+ branch: z20.string().max(200).optional().describe("Branch to clone (with repo_url)."),
4151
4216
  machine: machineInput
4152
4217
  },
4153
4218
  handler: async (args2, ctx) => {
@@ -4264,9 +4329,9 @@ defineTool({
4264
4329
  '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.'
4265
4330
  ].join("\n"),
4266
4331
  input: {
4267
- service_id: z19.union([z19.number().int().positive(), z19.string()]).describe("Source service to debug \u2014 numeric id or publicId."),
4268
- include_database_clone: z19.boolean().optional().describe("Clone the linked database so the app runs on copied data (default true)."),
4269
- name: z19.string().min(1).max(100).optional().describe('Dev box name (default "<source>-dev").')
4332
+ service_id: z20.union([z20.number().int().positive(), z20.string()]).describe("Source service to debug \u2014 numeric id or publicId."),
4333
+ include_database_clone: z20.boolean().optional().describe("Clone the linked database so the app runs on copied data (default true)."),
4334
+ name: z20.string().min(1).max(100).optional().describe('Dev box name (default "<source>-dev").')
4270
4335
  },
4271
4336
  handler: async (args2, ctx) => {
4272
4337
  const teamId = await ctx.resolveTeamId();
@@ -4307,7 +4372,7 @@ defineTool({
4307
4372
  'Example: delete_dev_environment({ service_id: "svc_api_dev" }) \u2192 removes the dev box, its cloned database, and the /workspace volume.'
4308
4373
  ].join("\n"),
4309
4374
  input: {
4310
- service_id: z19.union([z19.number().int().positive(), z19.string()]).describe("The dev box to tear down \u2014 numeric id or publicId.")
4375
+ service_id: z20.union([z20.number().int().positive(), z20.string()]).describe("The dev box to tear down \u2014 numeric id or publicId.")
4311
4376
  },
4312
4377
  handler: async (args2, ctx) => {
4313
4378
  const teamId = await ctx.resolveTeamId();
@@ -4345,8 +4410,8 @@ defineTool({
4345
4410
  'Example: resize_dev_environment({ service_id: "svc_skyskraber_dev", size: "large" }) \u2192 bumps the box to the large tier, applied live.'
4346
4411
  ].join("\n"),
4347
4412
  input: {
4348
- service_id: z19.union([z19.number().int().positive(), z19.string()]).describe("The box to resize \u2014 numeric id or publicId."),
4349
- size: z19.enum(SERVICE_PLANS).describe(
4413
+ service_id: z20.union([z20.number().int().positive(), z20.string()]).describe("The box to resize \u2014 numeric id or publicId."),
4414
+ size: z20.enum(SERVICE_PLANS).describe(
4350
4415
  'Target size tier (service catalog size, e.g. "standard", "large", "xlarge").'
4351
4416
  )
4352
4417
  },
@@ -4483,7 +4548,7 @@ defineTool({
4483
4548
  "Inputs:",
4484
4549
  ' - name (optional): the Dev Box name. Omit it and the box is named after its source (the repo name, or "dev-box" when blank), with a numeric suffix if that name is taken \u2014 so "give me a dev box" needs no invented name.',
4485
4550
  ' - source_kind: "github_repo" (clone a connected repo \u2014 needs github_repo_id), "url" (clone any http(s) git URL \u2014 needs clone_url), or "blank" (empty box).',
4486
- " - github_repo_id (for github_repo): numeric id of a connected GitHub repo.",
4551
+ " - github_repo_id (for github_repo): HostStack row id of a connected GitHub repo \u2014 call list_github_repos for it. It is NOT the id GitHub shows, and it cannot be guessed.",
4487
4552
  " - clone_url (for url): an http(s) git clone URL.",
4488
4553
  " - branch (optional): branch to clone.",
4489
4554
  ' - databases (optional): companion services to attach \u2014 any of "postgres", "redis", "meilisearch".',
@@ -4493,24 +4558,26 @@ defineTool({
4493
4558
  "",
4494
4559
  "Returns: { service, devUrl, deployId } \u2014 deploying. Once live: open the Terminal tab, run `claude`, start the dev server on $PORT, view at https://<devUrl>. Tear down with delete_dev_environment.",
4495
4560
  "",
4496
- 'Example: create_standalone_dev_environment({ name: "app-dev", source_kind: "github_repo", github_repo_id: 42, databases: ["postgres","redis"] })'
4561
+ 'Example: list_github_repos({ q: "app" }) \u2192 id 42, then create_standalone_dev_environment({ name: "app-dev", source_kind: "github_repo", github_repo_id: 42, databases: ["postgres","redis"] })'
4497
4562
  ].join("\n"),
4498
4563
  input: {
4499
- name: z19.string().min(1).max(100).optional().describe(
4564
+ name: z20.string().min(1).max(100).optional().describe(
4500
4565
  '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.'
4501
4566
  ),
4502
- source_kind: z19.enum(["github_repo", "url", "blank"]).describe("Where the code comes from."),
4503
- github_repo_id: z19.number().int().positive().optional().describe('Connected GitHub repo id (required when source_kind="github_repo").'),
4504
- clone_url: z19.string().url().optional().describe('http(s) git clone URL (required when source_kind="url").'),
4505
- branch: z19.string().min(1).max(255).optional().describe("Branch to clone."),
4506
- databases: z19.array(z19.enum(["postgres", "redis", "meilisearch"])).optional().describe("Companion services to attach (fresh + empty)."),
4507
- plan: z19.enum(SERVICE_PLANS).optional().describe(
4567
+ source_kind: z20.enum(["github_repo", "url", "blank"]).describe("Where the code comes from."),
4568
+ github_repo_id: z20.number().int().positive().optional().describe(
4569
+ 'Connected GitHub repo id from list_github_repos (required when source_kind="github_repo").'
4570
+ ),
4571
+ clone_url: z20.string().url().optional().describe('http(s) git clone URL (required when source_kind="url").'),
4572
+ branch: z20.string().min(1).max(255).optional().describe("Branch to clone."),
4573
+ databases: z20.array(z20.enum(["postgres", "redis", "meilisearch"])).optional().describe("Companion services to attach (fresh + empty)."),
4574
+ plan: z20.enum(SERVICE_PLANS).optional().describe(
4508
4575
  'Box size (default "standard" \u2014 2 GB; a smaller plan is floored to "standard").'
4509
4576
  ),
4510
- agent_accounts: z19.array(
4511
- z19.object({
4512
- provider: z19.enum(["claude", "codex", "opencode"]),
4513
- account_id: z19.number().int().positive()
4577
+ agent_accounts: z20.array(
4578
+ z20.object({
4579
+ provider: z20.enum(["claude", "codex", "opencode"]),
4580
+ account_id: z20.number().int().positive()
4514
4581
  })
4515
4582
  ).max(3).optional().describe(
4516
4583
  "Bind saved agent logins by account id per provider. Omit to inherit the box owner's default logins automatically."
@@ -4583,12 +4650,12 @@ defineTool({
4583
4650
  "Inputs:",
4584
4651
  ' - service_id: publicId of the service (e.g. "svc_abc123").',
4585
4652
  "",
4586
- 'Returns: { service: Service, config: ServiceConfig } \u2014 service has type/status/runtime/repoUrl/branch/autoDeploy/region/plan/timestamps; config has memoryMb, cpuShares, diskSizeGb, port, protocol, healthCheckEnabled, healthCheckInterval, healthCheckTimeout, healthCheckGracePeriodSec, allowSearchIndexing, restartPolicy, deployStrategy ("rolling" | "recreate"), preDeployCommand, min/maxInstances, scale thresholds.',
4653
+ 'Returns: { service: Service, config: ServiceConfig } \u2014 service has type/status/runtime/branch/autoDeploy/region/plan/timestamps plus repoProvider/repoFullName/repoUrl for a source-built service (absent when it deploys a prebuilt image); config has memoryMb, cpuShares, diskSizeGb, port, protocol, healthCheckEnabled, healthCheckInterval, healthCheckTimeout, healthCheckGracePeriodSec, allowSearchIndexing, restartPolicy, deployStrategy ("rolling" | "recreate"), preDeployCommand, min/maxInstances, scale thresholds.',
4587
4654
  "",
4588
4655
  'Example: get_service({ service_id: "svc_abc" }) \u2192 { service: { type: "web", status: "running", \u2026 }, config: { healthCheckGracePeriodSec: 120, \u2026 } }'
4589
4656
  ].join("\n"),
4590
4657
  input: {
4591
- service_id: z19.string().describe("Service publicId (e.g. svc_abc123).")
4658
+ service_id: z20.string().describe("Service publicId (e.g. svc_abc123).")
4592
4659
  },
4593
4660
  handler: async (args2, ctx) => {
4594
4661
  const teamId = await ctx.resolveTeamId();
@@ -4620,7 +4687,7 @@ defineTool({
4620
4687
  'Example: get_service_metrics({ service_id: "svc_abc" }) \u2192 { metrics: { cpu: 0.42, memory: 0.71, \u2026 } }'
4621
4688
  ].join("\n"),
4622
4689
  input: {
4623
- service_id: z19.string().describe("Service publicId.")
4690
+ service_id: z20.string().describe("Service publicId.")
4624
4691
  },
4625
4692
  handler: async (args2, ctx) => {
4626
4693
  const teamId = await ctx.resolveTeamId();
@@ -4653,9 +4720,9 @@ defineTool({
4653
4720
  'Example: get_service_metrics_history({ service_id: "svc_abc", from: "-1h" }) \u2192 ~120 points for the last hour.'
4654
4721
  ].join("\n"),
4655
4722
  input: {
4656
- service_id: z19.string().describe("Service publicId."),
4657
- from: z19.string().optional().describe('ISO-8601 lower bound or relative offset (e.g. "-1h", "-2d").'),
4658
- to: z19.string().optional().describe("ISO-8601 upper bound; defaults to now.")
4723
+ service_id: z20.string().describe("Service publicId."),
4724
+ from: z20.string().optional().describe('ISO-8601 lower bound or relative offset (e.g. "-1h", "-2d").'),
4725
+ to: z20.string().optional().describe("ISO-8601 upper bound; defaults to now.")
4659
4726
  },
4660
4727
  handler: async (args2, ctx) => {
4661
4728
  const teamId = await ctx.resolveTeamId();
@@ -4699,8 +4766,8 @@ defineTool({
4699
4766
  'Example: update_service({ service_id: "svc_abc", name: "api-prod" }) \u2192 { service: { name: "api-prod", \u2026 } }'
4700
4767
  ].join("\n"),
4701
4768
  input: {
4702
- service_id: z19.string().describe("Service publicId."),
4703
- name: z19.string().min(1).max(60).describe("New service name (1\u201360 chars).")
4769
+ service_id: z20.string().describe("Service publicId."),
4770
+ name: z20.string().min(1).max(60).describe("New service name (1\u201360 chars).")
4704
4771
  },
4705
4772
  handler: async (args2, ctx) => {
4706
4773
  const teamId = await ctx.resolveTeamId();
@@ -4757,54 +4824,54 @@ defineTool({
4757
4824
  'Example: update_service_config({ service_id: "svc_abc", health_check_grace_period_sec: 180 }) \u2192 { config: { healthCheckGracePeriodSec: 180, \u2026 } }'
4758
4825
  ].join("\n"),
4759
4826
  input: {
4760
- service_id: z19.string().describe("Service publicId."),
4761
- install_command: z19.string().nullable().optional().describe("Install shell command. Null clears."),
4762
- build_command: z19.string().nullable().optional().describe("Build shell command. Null clears."),
4763
- start_command: z19.string().nullable().optional().describe("Start shell command. Null clears."),
4764
- branch: z19.string().optional().describe("Git branch to track."),
4765
- root_directory: z19.string().optional().describe("Build context root."),
4766
- dockerfile_path: z19.string().nullable().optional().describe("Path to Dockerfile relative to root. Null clears."),
4767
- auto_deploy: z19.boolean().optional().describe("Auto-deploy on push."),
4768
- health_check_path: z19.string().nullable().optional().describe('HTTP health-check path (e.g. "/health"). Null = TCP-only check.'),
4769
- health_check_enabled: z19.boolean().optional().describe("Toggle health checking on/off."),
4770
- allow_search_indexing: z19.boolean().optional().describe(
4827
+ service_id: z20.string().describe("Service publicId."),
4828
+ install_command: z20.string().nullable().optional().describe("Install shell command. Null clears."),
4829
+ build_command: z20.string().nullable().optional().describe("Build shell command. Null clears."),
4830
+ start_command: z20.string().nullable().optional().describe("Start shell command. Null clears."),
4831
+ branch: z20.string().optional().describe("Git branch to track."),
4832
+ root_directory: z20.string().optional().describe("Build context root."),
4833
+ dockerfile_path: z20.string().nullable().optional().describe("Path to Dockerfile relative to root. Null clears."),
4834
+ auto_deploy: z20.boolean().optional().describe("Auto-deploy on push."),
4835
+ health_check_path: z20.string().nullable().optional().describe('HTTP health-check path (e.g. "/health"). Null = TCP-only check.'),
4836
+ health_check_enabled: z20.boolean().optional().describe("Toggle health checking on/off."),
4837
+ allow_search_indexing: z20.boolean().optional().describe(
4771
4838
  "Let search engines index the free *.hoststack.dev platform URL (off by default). Custom domains are always indexable. Applies on the next deploy."
4772
4839
  ),
4773
- health_check_interval: z19.number().int().min(5).max(300).optional().describe("How often the check runs, in seconds (5\u2013300)."),
4774
- health_check_timeout: z19.number().int().min(1).max(60).optional().describe("Single-attempt timeout in seconds (1\u201360)."),
4775
- health_check_grace_period_sec: z19.number().int().min(1).max(1800).optional().describe(
4840
+ health_check_interval: z20.number().int().min(5).max(300).optional().describe("How often the check runs, in seconds (5\u2013300)."),
4841
+ health_check_timeout: z20.number().int().min(1).max(60).optional().describe("Single-attempt timeout in seconds (1\u201360)."),
4842
+ health_check_grace_period_sec: z20.number().int().min(1).max(1800).optional().describe(
4776
4843
  "Startup grace period in seconds (1\u20131800). Raise this if the app needs more time to boot before health checks start counting failures."
4777
4844
  ),
4778
- memory_mb: z19.number().int().min(128).max(16384).optional().describe("Container memory cap in MB (128\u201316384)."),
4779
- cpu_shares: z19.number().int().min(128).max(4096).optional().describe("Relative CPU weight (128\u20134096)."),
4780
- disk_size_gb: z19.number().int().min(1).max(100).optional().describe("Ephemeral disk size in GB (1\u2013100)."),
4781
- port: z19.number().int().min(1).max(65535).optional().describe("Container port the platform forwards traffic to."),
4782
- protocol: z19.enum(["http", "tcp"]).optional().describe("Traffic protocol."),
4783
- restart_policy: z19.enum(["always", "on-failure", "no"]).optional().describe("Docker restart policy."),
4784
- deploy_strategy: z19.enum(["rolling", "recreate"]).optional().describe(
4845
+ memory_mb: z20.number().int().min(128).max(16384).optional().describe("Container memory cap in MB (128\u201316384)."),
4846
+ cpu_shares: z20.number().int().min(128).max(4096).optional().describe("Relative CPU weight (128\u20134096)."),
4847
+ disk_size_gb: z20.number().int().min(1).max(100).optional().describe("Ephemeral disk size in GB (1\u2013100)."),
4848
+ port: z20.number().int().min(1).max(65535).optional().describe("Container port the platform forwards traffic to."),
4849
+ protocol: z20.enum(["http", "tcp"]).optional().describe("Traffic protocol."),
4850
+ restart_policy: z20.enum(["always", "on-failure", "no"]).optional().describe("Docker restart policy."),
4851
+ deploy_strategy: z20.enum(["rolling", "recreate"]).optional().describe(
4785
4852
  '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.'
4786
4853
  ),
4787
- pre_deploy_command: z19.string().optional().describe("Shell command run before the new release accepts traffic."),
4788
- instance_count: z19.number().int().positive().max(50).optional().describe("Pin min and max instances to this value (1\u201350)."),
4789
- min_instances: z19.number().int().min(0).max(50).optional().describe("Autoscale lower bound. Use with max_instances for a range."),
4790
- max_instances: z19.number().int().min(1).max(50).optional().describe("Autoscale upper bound. Use with min_instances for a range."),
4791
- scale_cpu_threshold: z19.number().int().min(10).max(100).optional().describe("Autoscale CPU trigger percentage (10\u2013100)."),
4792
- scale_memory_threshold: z19.number().int().min(10).max(100).optional().describe("Autoscale memory trigger percentage (10\u2013100)."),
4793
- scale_requests_per_sec_threshold: z19.number().int().min(1).max(1e4).nullable().optional().describe("Autoscale request-rate trigger (1\u201310000 req/s). null stops using it."),
4794
- build_timeout_seconds: z19.number().int().min(60).max(1800).nullable().optional().describe("Kill a build that runs longer than this (60\u20131800s). null = default."),
4795
- plan: z19.enum(SERVICE_PLANS).optional().describe(
4854
+ pre_deploy_command: z20.string().optional().describe("Shell command run before the new release accepts traffic."),
4855
+ instance_count: z20.number().int().positive().max(50).optional().describe("Pin min and max instances to this value (1\u201350)."),
4856
+ min_instances: z20.number().int().min(0).max(50).optional().describe("Autoscale lower bound. Use with max_instances for a range."),
4857
+ max_instances: z20.number().int().min(1).max(50).optional().describe("Autoscale upper bound. Use with min_instances for a range."),
4858
+ scale_cpu_threshold: z20.number().int().min(10).max(100).optional().describe("Autoscale CPU trigger percentage (10\u2013100)."),
4859
+ scale_memory_threshold: z20.number().int().min(10).max(100).optional().describe("Autoscale memory trigger percentage (10\u2013100)."),
4860
+ scale_requests_per_sec_threshold: z20.number().int().min(1).max(1e4).nullable().optional().describe("Autoscale request-rate trigger (1\u201310000 req/s). null stops using it."),
4861
+ build_timeout_seconds: z20.number().int().min(60).max(1800).nullable().optional().describe("Kill a build that runs longer than this (60\u20131800s). null = default."),
4862
+ plan: z20.enum(SERVICE_PLANS).optional().describe(
4796
4863
  "Move to a different size tier. memory_mb and cpu_shares are clamped to the tier, so this is what raises the ceiling. Changes cost."
4797
4864
  ),
4798
- docker_image: z19.string().max(500).nullable().optional().describe(
4865
+ docker_image: z20.string().max(500).nullable().optional().describe(
4799
4866
  "Repoint an image-backed service. Applies on the next deploy. null clears it."
4800
4867
  ),
4801
- cron_schedule: z19.string().max(100).nullable().optional().describe("Cron expression for a cron_job service. null clears it."),
4802
- publish_path: z19.string().max(500).nullable().optional().describe('Static-site output directory, e.g. "dist". null clears it.'),
4803
- runtime: z19.string().max(50).optional().describe("Runtime hint (node/bun/python/\u2026). Normally auto-detected."),
4804
- log_filter_rules: z19.array(
4805
- z19.object({
4806
- pattern: z19.string().min(1).max(200),
4807
- action: z19.enum(["drop", "downgrade"])
4868
+ cron_schedule: z20.string().max(100).nullable().optional().describe("Cron expression for a cron_job service. null clears it."),
4869
+ publish_path: z20.string().max(500).nullable().optional().describe('Static-site output directory, e.g. "dist". null clears it.'),
4870
+ runtime: z20.string().max(50).optional().describe("Runtime hint (node/bun/python/\u2026). Normally auto-detected."),
4871
+ log_filter_rules: z20.array(
4872
+ z20.object({
4873
+ pattern: z20.string().min(1).max(200),
4874
+ action: z20.enum(["drop", "downgrade"])
4808
4875
  })
4809
4876
  ).max(50).optional().describe(
4810
4877
  "Runtime-log filter rules. Empty array [] clears all rules. Each pattern is case-insensitive substring match against the message."
@@ -4912,7 +4979,7 @@ defineTool({
4912
4979
  'Example: suspend_service({ service_id: "svc_dev" }) \u2192 { ok: true }'
4913
4980
  ].join("\n"),
4914
4981
  input: {
4915
- service_id: z19.string().describe("Service publicId.")
4982
+ service_id: z20.string().describe("Service publicId.")
4916
4983
  },
4917
4984
  handler: async (args2, ctx) => {
4918
4985
  const teamId = await ctx.resolveTeamId();
@@ -4936,7 +5003,7 @@ defineTool({
4936
5003
  'Example: resume_service({ service_id: "svc_dev" }) \u2192 { ok: true }'
4937
5004
  ].join("\n"),
4938
5005
  input: {
4939
- service_id: z19.string().describe("Service publicId.")
5006
+ service_id: z20.string().describe("Service publicId.")
4940
5007
  },
4941
5008
  handler: async (args2, ctx) => {
4942
5009
  const teamId = await ctx.resolveTeamId();
@@ -4962,7 +5029,7 @@ defineTool({
4962
5029
  'Example: delete_service({ service_id: "svc_abandoned" }) \u2192 { ok: true }'
4963
5030
  ].join("\n"),
4964
5031
  input: {
4965
- service_id: z19.string().describe("Service publicId.")
5032
+ service_id: z20.string().describe("Service publicId.")
4966
5033
  },
4967
5034
  handler: async (args2, ctx) => {
4968
5035
  const teamId = await ctx.resolveTeamId();
@@ -5001,16 +5068,16 @@ defineTool({
5001
5068
  ' - Just count error lines without fetching them: get_service_logs({ service_id: "svc_abc", level: "error", since: "-5m", count_only: true }) \u2192 { count: 47 }'
5002
5069
  ].join("\n"),
5003
5070
  input: {
5004
- service_id: z19.string().describe("Service publicId."),
5005
- lines: z19.number().int().positive().max(1e3).optional().describe("Tail size; default 200, hard cap 1000."),
5006
- since: z19.string().optional().describe('ISO-8601 timestamp or relative offset (e.g. "-5m", "-1h").'),
5007
- until: z19.string().optional().describe("ISO-8601 timestamp or relative offset upper bound."),
5008
- stream: z19.enum(["stdout", "stderr"]).optional().describe("Restrict to one stream."),
5009
- level: z19.enum(["stdout", "stderr", "trace", "debug", "info", "warn", "error", "fatal"]).optional().describe(
5071
+ service_id: z20.string().describe("Service publicId."),
5072
+ lines: z20.number().int().positive().max(1e3).optional().describe("Tail size; default 200, hard cap 1000."),
5073
+ since: z20.string().optional().describe('ISO-8601 timestamp or relative offset (e.g. "-5m", "-1h").'),
5074
+ until: z20.string().optional().describe("ISO-8601 timestamp or relative offset upper bound."),
5075
+ stream: z20.enum(["stdout", "stderr"]).optional().describe("Restrict to one stream."),
5076
+ level: z20.enum(["stdout", "stderr", "trace", "debug", "info", "warn", "error", "fatal"]).optional().describe(
5010
5077
  "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)."
5011
5078
  ),
5012
- search: z19.string().max(100).optional().describe("Case-insensitive substring filter."),
5013
- count_only: z19.boolean().optional().describe("When true, return only { count } \u2014 skips the log payload.")
5079
+ search: z20.string().max(100).optional().describe("Case-insensitive substring filter."),
5080
+ count_only: z20.boolean().optional().describe("When true, return only { count } \u2014 skips the log payload.")
5014
5081
  },
5015
5082
  handler: async (args2, ctx) => {
5016
5083
  const teamId = await ctx.resolveTeamId();
@@ -5059,14 +5126,14 @@ defineTool({
5059
5126
  '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 } } }.'
5060
5127
  ].join("\n"),
5061
5128
  input: {
5062
- service_ids: z19.array(z19.string()).min(1).max(10).describe("Service publicIds (1\u201310). Hard cap 10 to bound parallel work."),
5063
- lines_per_service: z19.number().int().positive().max(500).optional().describe("Tail size per service; default 100, hard cap 500."),
5064
- since: z19.string().optional().describe('ISO-8601 timestamp or relative offset (e.g. "-5m", "-1h").'),
5065
- until: z19.string().optional().describe("ISO-8601 timestamp or relative offset upper bound."),
5066
- stream: z19.enum(["stdout", "stderr"]).optional().describe("Restrict to one stream."),
5067
- level: z19.enum(["stdout", "stderr", "trace", "debug", "info", "warn", "error", "fatal"]).optional().describe("Structured log level filter (same as get_service_logs)."),
5068
- search: z19.string().max(100).optional().describe("Case-insensitive substring filter."),
5069
- count_only: z19.boolean().optional().describe("When true, return only counts per service \u2014 skips the log payload.")
5129
+ service_ids: z20.array(z20.string()).min(1).max(10).describe("Service publicIds (1\u201310). Hard cap 10 to bound parallel work."),
5130
+ lines_per_service: z20.number().int().positive().max(500).optional().describe("Tail size per service; default 100, hard cap 500."),
5131
+ since: z20.string().optional().describe('ISO-8601 timestamp or relative offset (e.g. "-5m", "-1h").'),
5132
+ until: z20.string().optional().describe("ISO-8601 timestamp or relative offset upper bound."),
5133
+ stream: z20.enum(["stdout", "stderr"]).optional().describe("Restrict to one stream."),
5134
+ level: z20.enum(["stdout", "stderr", "trace", "debug", "info", "warn", "error", "fatal"]).optional().describe("Structured log level filter (same as get_service_logs)."),
5135
+ search: z20.string().max(100).optional().describe("Case-insensitive substring filter."),
5136
+ count_only: z20.boolean().optional().describe("When true, return only counts per service \u2014 skips the log payload.")
5070
5137
  },
5071
5138
  handler: async (args2, ctx) => {
5072
5139
  const teamId = await ctx.resolveTeamId();
@@ -5112,7 +5179,7 @@ defineTool({
5112
5179
  });
5113
5180
 
5114
5181
  // src/tools/uptime.ts
5115
- import { z as z20 } from "zod";
5182
+ import { z as z21 } from "zod";
5116
5183
  var STATUS_MEANING = [
5117
5184
  "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."
5118
5185
  ].join("\n");
@@ -5134,7 +5201,7 @@ defineTool({
5134
5201
  "",
5135
5202
  "Example: get_uptime_check({ serviceId: 48 }) \u2192 { check: { path: '/healthz', status: 'down', consecutiveFailures: 5, lastError: 'No response within 10000ms', lastChangedAt: '2026-08-21T04:12:00Z', lastTargetUrl: 'https://grundfast.dk/healthz' } }."
5136
5203
  ].join("\n"),
5137
- input: { serviceId: z20.number().int().positive() },
5204
+ input: { serviceId: z21.number().int().positive() },
5138
5205
  handler: async (args2, ctx) => {
5139
5206
  const teamId = await ctx.resolveTeamId();
5140
5207
  const response = await ctx.api.get(
@@ -5180,14 +5247,14 @@ defineTool({
5180
5247
  "Example: set_uptime_check({ serviceId: 48, path: '/healthz', intervalSeconds: 60, failureThreshold: 3 }) \u2192 { check: { status: 'unknown', \u2026 } }."
5181
5248
  ].join("\n"),
5182
5249
  input: {
5183
- serviceId: z20.number().int().positive(),
5184
- enabled: z20.boolean().optional(),
5185
- path: z20.string().max(500).optional().describe('Must start with /. Default "/".'),
5186
- method: z20.enum(["GET", "HEAD"]).optional(),
5187
- expectedStatus: z20.number().int().min(100).max(599).optional(),
5188
- timeoutMs: z20.number().int().min(1e3).max(6e4).optional(),
5189
- intervalSeconds: z20.number().int().min(30).max(3600).optional(),
5190
- failureThreshold: z20.number().int().min(1).max(10).optional()
5250
+ serviceId: z21.number().int().positive(),
5251
+ enabled: z21.boolean().optional(),
5252
+ path: z21.string().max(500).optional().describe('Must start with /. Default "/".'),
5253
+ method: z21.enum(["GET", "HEAD"]).optional(),
5254
+ expectedStatus: z21.number().int().min(100).max(599).optional(),
5255
+ timeoutMs: z21.number().int().min(1e3).max(6e4).optional(),
5256
+ intervalSeconds: z21.number().int().min(30).max(3600).optional(),
5257
+ failureThreshold: z21.number().int().min(1).max(10).optional()
5191
5258
  },
5192
5259
  handler: async (args2, ctx) => {
5193
5260
  const teamId = await ctx.resolveTeamId();
@@ -5218,7 +5285,7 @@ defineTool({
5218
5285
  "",
5219
5286
  "Example: delete_uptime_check({ serviceId: 48 }) \u2192 { success: true }."
5220
5287
  ].join("\n"),
5221
- input: { serviceId: z20.number().int().positive() },
5288
+ input: { serviceId: z21.number().int().positive() },
5222
5289
  handler: async (args2, ctx) => {
5223
5290
  const teamId = await ctx.resolveTeamId();
5224
5291
  await ctx.api.delete(`/api/services/${teamId}/${args2.serviceId}/uptime-check`);
@@ -5227,7 +5294,7 @@ defineTool({
5227
5294
  });
5228
5295
 
5229
5296
  // src/tools/volumes.ts
5230
- import { z as z21 } from "zod";
5297
+ import { z as z22 } from "zod";
5231
5298
  var MIN_VOLUME_SIZE_GB = 10;
5232
5299
  defineTool({
5233
5300
  name: "list_volumes",
@@ -5245,7 +5312,7 @@ defineTool({
5245
5312
  'Example: list_volumes({ service_id: "svc_abc" }) \u2192 { items: [{ name: "data", mountPath: "/var/data", sizeGb: 10, status: "active" }] }'
5246
5313
  ].join("\n"),
5247
5314
  input: {
5248
- service_id: z21.string().describe("Service publicId (e.g. svc_abc123).")
5315
+ service_id: z22.string().describe("Service publicId (e.g. svc_abc123).")
5249
5316
  },
5250
5317
  handler: async (args2, ctx) => {
5251
5318
  const teamId = await ctx.resolveTeamId();
@@ -5274,15 +5341,15 @@ defineTool({
5274
5341
  '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" } }'
5275
5342
  ].join("\n"),
5276
5343
  input: {
5277
- service_id: z21.string().describe("Service publicId."),
5278
- name: z21.string().min(1).max(64).regex(/^[a-z0-9-]+$/).describe("Volume name (lowercase alphanumeric + hyphens)."),
5279
- mount_path: z21.string().startsWith("/").max(500).describe("In-container mount path (absolute)."),
5344
+ service_id: z22.string().describe("Service publicId."),
5345
+ name: z22.string().min(1).max(64).regex(/^[a-z0-9-]+$/).describe("Volume name (lowercase alphanumeric + hyphens)."),
5346
+ mount_path: z22.string().startsWith("/").max(500).describe("In-container mount path (absolute)."),
5280
5347
  // 10 GB is the real floor: the block-storage backend rejects anything
5281
5348
  // smaller. Advertising 1 GB here (and defaulting to it) meant taking the
5282
5349
  // defaults produced a volume that provisioned with `Hetzner API error:
5283
5350
  // 422` on the NEXT deploy, with nothing tying the failure back to the
5284
5351
  // size. Reject it at the call instead.
5285
- size_gb: z21.number().int().min(MIN_VOLUME_SIZE_GB).max(100).optional().describe(
5352
+ size_gb: z22.number().int().min(MIN_VOLUME_SIZE_GB).max(100).optional().describe(
5286
5353
  `Disk size in GB (minimum ${MIN_VOLUME_SIZE_GB}, default ${MIN_VOLUME_SIZE_GB}, max 100 via MCP).`
5287
5354
  )
5288
5355
  },
@@ -5321,11 +5388,11 @@ defineTool({
5321
5388
  'Example: update_volume({ service_id: "svc_abc", volume_id: "vol_xyz", size_gb: 20 }) \u2192 { volume: { sizeGb: 20, \u2026 } }'
5322
5389
  ].join("\n"),
5323
5390
  input: {
5324
- service_id: z21.string().describe("Service publicId."),
5325
- volume_id: z21.string().describe("Volume publicId (e.g. vol_\u2026)."),
5326
- mount_path: z21.string().startsWith("/").max(500).optional().describe("New mount path."),
5327
- 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).`),
5328
- backup_enabled: z21.boolean().optional().describe("Whether to keep backing this volume up. Past backups are kept either way.")
5391
+ service_id: z22.string().describe("Service publicId."),
5392
+ volume_id: z22.string().describe("Volume publicId (e.g. vol_\u2026)."),
5393
+ mount_path: z22.string().startsWith("/").max(500).optional().describe("New mount path."),
5394
+ size_gb: z22.number().int().min(MIN_VOLUME_SIZE_GB).max(100).optional().describe(`New size in GB (minimum ${MIN_VOLUME_SIZE_GB}, grow-only).`),
5395
+ backup_enabled: z22.boolean().optional().describe("Whether to keep backing this volume up. Past backups are kept either way.")
5329
5396
  },
5330
5397
  handler: async (args2, ctx) => {
5331
5398
  const teamId = await ctx.resolveTeamId();
@@ -5364,8 +5431,8 @@ defineTool({
5364
5431
  'Example: delete_volume({ service_id: "svc_abc", volume_id: "vol_xyz" }) \u2192 { ok: true }'
5365
5432
  ].join("\n"),
5366
5433
  input: {
5367
- service_id: z21.string().describe("Service publicId."),
5368
- volume_id: z21.string().describe("Volume publicId.")
5434
+ service_id: z22.string().describe("Service publicId."),
5435
+ volume_id: z22.string().describe("Volume publicId.")
5369
5436
  },
5370
5437
  handler: async (args2, ctx) => {
5371
5438
  const teamId = await ctx.resolveTeamId();