@hoststack.dev/mcp 0.23.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.
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { HostStack } from "@hoststack.dev/sdk";
4
4
 
5
5
  // src/version.ts
6
- var MCP_VERSION = true ? "0.23.0" : "0.0.0-dev";
6
+ var MCP_VERSION = true ? "0.25.0" : "0.0.0-dev";
7
7
  var USER_AGENT = `hoststack-mcp/${MCP_VERSION}`;
8
8
 
9
9
  // src/api-client.ts
@@ -1394,7 +1394,7 @@ var DNS_RECORD_TYPES = [
1394
1394
  async function resolveZonePublicId(hoststack, teamId, input) {
1395
1395
  if (input.zone_id) {
1396
1396
  const { zones: zones2 } = await hoststack.dns.listZones(teamId);
1397
- const match = zones2.find((z22) => z22.publicId === input.zone_id);
1397
+ const match = zones2.find((z23) => z23.publicId === input.zone_id);
1398
1398
  if (!match) {
1399
1399
  throw new Error(`Zone ${input.zone_id} not found on this team.`);
1400
1400
  }
@@ -1408,7 +1408,7 @@ async function resolveZonePublicId(hoststack, teamId, input) {
1408
1408
  const labels = fqdn.split(".");
1409
1409
  for (let i = 0; i < labels.length - 1; i++) {
1410
1410
  const candidate = labels.slice(i).join(".");
1411
- const match = zones.find((z22) => z22.domainName.toLowerCase() === candidate);
1411
+ const match = zones.find((z23) => z23.domainName.toLowerCase() === candidate);
1412
1412
  if (match && match.status !== "deleting") {
1413
1413
  return { publicId: match.publicId, domainName: match.domainName };
1414
1414
  }
@@ -2894,19 +2894,69 @@ defineTool({
2894
2894
  });
2895
2895
 
2896
2896
  // src/tools/github.ts
2897
+ import { z as z14 } from "zod";
2898
+ function shapeRepo(repo) {
2899
+ return {
2900
+ id: repo.id,
2901
+ fullName: repo.fullName,
2902
+ defaultBranch: repo.defaultBranch,
2903
+ private: repo.isPrivate,
2904
+ url: repo.htmlUrl,
2905
+ ...repo.language ? { language: repo.language } : {}
2906
+ };
2907
+ }
2908
+ defineTool({
2909
+ name: "list_github_repos",
2910
+ category: "github",
2911
+ description: [
2912
+ "Every GitHub repository this team can deploy, with the `id` create_service takes \u2014 and the `owner/name` you can pass instead of it.",
2913
+ "",
2914
+ "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.)",
2915
+ "",
2916
+ "Inputs:",
2917
+ ' - q (optional): case-insensitive substring of the full name ("owner/name"). Omit for everything.',
2918
+ "",
2919
+ "Returns: { items: [{ id, fullName, defaultBranch, private, url, language? }], total } \u2014 `total` is the count before `q` filtered it.",
2920
+ "",
2921
+ "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.",
2922
+ "",
2923
+ 'Example: list_github_repos({ q: "stenshoppen" }) \u2192 { items: [{ id: 143, fullName: "acme/stenshoppen", defaultBranch: "master", private: true }], total: 45 }',
2924
+ 'Then either: create_service({ \u2026, github_repo: "acme/stenshoppen" }) \u2014 or the id, if you prefer.'
2925
+ ].join("\n"),
2926
+ input: {
2927
+ q: z14.string().min(1).max(200).optional().describe('Case-insensitive substring of "owner/name". Omit to list everything.')
2928
+ },
2929
+ handler: async (args, ctx) => {
2930
+ const teamId = await ctx.resolveTeamId();
2931
+ const { repos } = await ctx.api.get(
2932
+ `/api/github/${teamId}/repos`
2933
+ );
2934
+ const needle = args.q?.trim().toLowerCase();
2935
+ const matched = needle ? repos.filter((r) => r.fullName.toLowerCase().includes(needle)) : repos;
2936
+ const items = matched.map(shapeRepo).sort((a, b) => a.fullName.localeCompare(b.fullName));
2937
+ if (repos.length === 0) {
2938
+ return respond({
2939
+ 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.",
2940
+ data: { items, total: 0 }
2941
+ });
2942
+ }
2943
+ const summary = needle ? items.length === 0 ? `No connected repository matches "${args.q}" (${repos.length} connected). Push it, then call sync_github_repos.` : `${items.length} of ${repos.length} connected repositories match "${args.q}".` : `${items.length} connected repositor${items.length === 1 ? "y" : "ies"}.`;
2944
+ return respond({ summary, data: { items, total: repos.length } });
2945
+ }
2946
+ });
2897
2947
  defineTool({
2898
2948
  name: "sync_github_repos",
2899
2949
  category: "github",
2900
2950
  description: [
2901
- "Re-sync the repository list from GitHub for every connected GitHub App installation.",
2951
+ "Re-sync the repository list from GitHub for every connected GitHub App installation, and return what that now exposes.",
2902
2952
  "",
2903
- "When to use: right after pushing a brand-new repository, HostStack cannot see it until an installation re-sync runs \u2014 so create_service with a github_repo_id would fail. Call this first. (Same operation as the dashboard's refresh icon on the repo picker.)",
2953
+ "When to use: right after pushing a brand-new repository, HostStack cannot see it until an installation re-sync runs \u2014 so create_service with a github_repo_id or github_repo would fail. Call this first. (Same operation as the dashboard's refresh icon on the repo picker.) To read the list WITHOUT re-syncing, call list_github_repos.",
2904
2954
  "",
2905
2955
  "No inputs.",
2906
2956
  "",
2907
- "Returns: { installations: number, repos: [{ account, count }], totalRepos } \u2014 the accounts synced and how many repos each now exposes.",
2957
+ "Returns: { installations: number, accounts: [{ account, count }], totalRepos, repos: [{ id, fullName }] } \u2014 `repos` carries the ids create_service takes, so a sync is enough to act on; it does not have to be followed by a list.",
2908
2958
  "",
2909
- 'Example: sync_github_repos({}) \u2192 { installations: 1, repos: [{ account: "acme", count: 12 }], totalRepos: 12 }'
2959
+ 'Example: sync_github_repos({}) \u2192 { installations: 1, accounts: [{ account: "acme", count: 12 }], totalRepos: 12, repos: [{ id: 143, fullName: "acme/stenshoppen" }, \u2026] }'
2910
2960
  ].join("\n"),
2911
2961
  input: {},
2912
2962
  handler: async (_args, ctx) => {
@@ -2920,23 +2970,29 @@ defineTool({
2920
2970
  );
2921
2971
  }
2922
2972
  const perAccount = [];
2923
- let totalRepos = 0;
2973
+ const synced = [];
2924
2974
  for (const inst of installations) {
2925
2975
  const { repos } = await ctx.api.post(
2926
2976
  `/api/github/${teamId}/installations/${inst.id}/sync`
2927
2977
  );
2928
2978
  perAccount.push({ account: inst.accountLogin, count: repos.length });
2929
- totalRepos += repos.length;
2979
+ for (const repo of repos) synced.push({ id: repo.id, fullName: repo.fullName });
2930
2980
  }
2981
+ synced.sort((a, b) => a.fullName.localeCompare(b.fullName));
2931
2982
  return respond({
2932
- summary: `Synced ${totalRepos} repositories across ${installations.length} installation(s).`,
2933
- data: { installations: installations.length, repos: perAccount, totalRepos }
2983
+ summary: `Synced ${synced.length} repositories across ${installations.length} installation(s).`,
2984
+ data: {
2985
+ installations: installations.length,
2986
+ accounts: perAccount,
2987
+ totalRepos: synced.length,
2988
+ repos: synced
2989
+ }
2934
2990
  });
2935
2991
  }
2936
2992
  });
2937
2993
 
2938
2994
  // src/tools/issue-reports.ts
2939
- import { z as z14 } from "zod";
2995
+ import { z as z15 } from "zod";
2940
2996
  defineTool({
2941
2997
  name: "report_issue",
2942
2998
  category: "support",
@@ -2958,16 +3014,16 @@ defineTool({
2958
3014
  "Example: report_issue({ title: 'Dev box deploys fail with 404 No such image', description: 'Every deploy of svc 172 since 2026-08-22 09:03 fails at container create with `404 No such image: \u2026/dev-env:latest`. The deploy log reports \"Image ready in 1s\" for a 5.5 GB image, so the pull is not happening. The image pulls fine by hand from the same registry.', severity: 'high', serviceId: 172 }) \u2192 { ticket: { publicId: 'tkt_\u2026' } }"
2959
3015
  ].join("\n"),
2960
3016
  input: {
2961
- title: z14.string().min(1).max(300).describe("One-line summary of the fault."),
2962
- description: z14.string().min(1).max(1e4).describe("What happened, what you expected instead, and what you already ruled out."),
2963
- severity: z14.enum(["low", "normal", "high", "urgent"]).optional().describe(
3017
+ title: z15.string().min(1).max(300).describe("One-line summary of the fault."),
3018
+ description: z15.string().min(1).max(1e4).describe("What happened, what you expected instead, and what you already ruled out."),
3019
+ severity: z15.enum(["low", "normal", "high", "urgent"]).optional().describe(
2964
3020
  'Default normal. Use high/urgent only when something is DOWN or losing data \u2014 not for "this is annoying".'
2965
3021
  ),
2966
- serviceId: z14.number().int().positive().optional().describe(
3022
+ serviceId: z15.number().int().positive().optional().describe(
2967
3023
  "The affected service. Attaches the service, its latest deploy, and that deploy log tail automatically."
2968
3024
  ),
2969
- deployId: z14.number().int().positive().optional().describe("Pin a specific deploy instead of the service\u2019s most recent one."),
2970
- databaseId: z14.number().int().positive().optional().describe("The affected database.")
3025
+ deployId: z15.number().int().positive().optional().describe("Pin a specific deploy instead of the service\u2019s most recent one."),
3026
+ databaseId: z15.number().int().positive().optional().describe("The affected database.")
2971
3027
  },
2972
3028
  handler: async (args, ctx) => {
2973
3029
  const teamId = await ctx.resolveTeamId();
@@ -3055,7 +3111,7 @@ defineTool({
3055
3111
  });
3056
3112
 
3057
3113
  // src/tools/notifications.ts
3058
- import { z as z15 } from "zod";
3114
+ import { z as z16 } from "zod";
3059
3115
  var NOTIFICATION_EVENTS = [
3060
3116
  "deploy.started",
3061
3117
  "deploy.succeeded",
@@ -3081,6 +3137,7 @@ var NOTIFICATION_EVENTS = [
3081
3137
  "error.issue_regressed",
3082
3138
  "git.auth_failed",
3083
3139
  "cron.execution_failed",
3140
+ "cron.schedule_missed",
3084
3141
  "workflow.failed",
3085
3142
  "devenv.agent.needs_input",
3086
3143
  "devenv.agent.finished",
@@ -3149,10 +3206,10 @@ defineTool({
3149
3206
  "Example: create_notification_channel({ type: 'slack', name: 'eng-alerts', webhook_url: 'https://hooks.slack.com/\u2026', events: ['deploy.failed', 'git.auth_failed', 'service.restart_failed'] })"
3150
3207
  ].join("\n"),
3151
3208
  input: {
3152
- type: z15.enum(["slack", "discord", "email"]).describe("Channel type."),
3153
- name: z15.string().min(1).max(128).describe("Human-readable label."),
3154
- webhook_url: z15.string().max(500).describe("Slack/Discord webhook URL or email address (when type=email)."),
3155
- events: z15.array(z15.enum(NOTIFICATION_EVENTS)).describe(
3209
+ type: z16.enum(["slack", "discord", "email"]).describe("Channel type."),
3210
+ name: z16.string().min(1).max(128).describe("Human-readable label."),
3211
+ webhook_url: z16.string().max(500).describe("Slack/Discord webhook URL or email address (when type=email)."),
3212
+ events: z16.array(z16.enum(NOTIFICATION_EVENTS)).describe(
3156
3213
  "List of events the channel subscribes to. Empty list = subscribe to nothing."
3157
3214
  )
3158
3215
  },
@@ -3190,10 +3247,10 @@ defineTool({
3190
3247
  "Example: update_notification_channel({ channel_id: 3, events: ['deploy.failed', 'service.restart_failed', 'git.auth_failed'] })"
3191
3248
  ].join("\n"),
3192
3249
  input: {
3193
- channel_id: z15.number().int().positive().describe("Numeric channel id from list_notification_channels."),
3194
- name: z15.string().min(1).max(128).optional().describe("New label."),
3195
- active: z15.boolean().optional().describe("false silences without deleting."),
3196
- events: z15.array(z15.enum(NOTIFICATION_EVENTS)).optional().describe("Replaces the full subscription list.")
3250
+ channel_id: z16.number().int().positive().describe("Numeric channel id from list_notification_channels."),
3251
+ name: z16.string().min(1).max(128).optional().describe("New label."),
3252
+ active: z16.boolean().optional().describe("false silences without deleting."),
3253
+ events: z16.array(z16.enum(NOTIFICATION_EVENTS)).optional().describe("Replaces the full subscription list.")
3197
3254
  },
3198
3255
  handler: async (args, ctx) => {
3199
3256
  const teamId = await ctx.resolveTeamId();
@@ -3232,7 +3289,7 @@ defineTool({
3232
3289
  "Example: delete_notification_channel({ channel_id: 3 }) \u2192 { ok: true }"
3233
3290
  ].join("\n"),
3234
3291
  input: {
3235
- channel_id: z15.number().int().positive().describe("Numeric channel id.")
3292
+ channel_id: z16.number().int().positive().describe("Numeric channel id.")
3236
3293
  },
3237
3294
  handler: async (args, ctx) => {
3238
3295
  const teamId = await ctx.resolveTeamId();
@@ -3259,7 +3316,7 @@ defineTool({
3259
3316
  "Example: test_notification_channel({ channel_id: 3 }) \u2192 { success: true }"
3260
3317
  ].join("\n"),
3261
3318
  input: {
3262
- channel_id: z15.number().int().positive().describe("Numeric channel id.")
3319
+ channel_id: z16.number().int().positive().describe("Numeric channel id.")
3263
3320
  },
3264
3321
  handler: async (args, ctx) => {
3265
3322
  const teamId = await ctx.resolveTeamId();
@@ -3274,7 +3331,7 @@ defineTool({
3274
3331
  });
3275
3332
 
3276
3333
  // src/tools/projects.ts
3277
- import { z as z16 } from "zod";
3334
+ import { z as z17 } from "zod";
3278
3335
  var AVAILABLE_REGION_IDS = ["eu-central-1"];
3279
3336
  defineTool({
3280
3337
  name: "list_projects",
@@ -3315,9 +3372,9 @@ defineTool({
3315
3372
  'Example: create_project({ name: "billing-api", description: "Stripe webhooks", region: "eu-central-1" }) \u2192 { project: { id: 12, publicId: "prj_\u2026", \u2026 } }'
3316
3373
  ].join("\n"),
3317
3374
  input: {
3318
- name: z16.string().min(1).max(60).describe("Project name (1\u201360 chars)."),
3319
- description: z16.string().max(500).optional().describe("Short description (\u2264500 chars)."),
3320
- region: z16.enum(AVAILABLE_REGION_IDS).optional().describe("Region: eu-central-1 (Falkenstein) \u2014 currently the only available region.")
3375
+ name: z17.string().min(1).max(60).describe("Project name (1\u201360 chars)."),
3376
+ description: z17.string().max(500).optional().describe("Short description (\u2264500 chars)."),
3377
+ region: z17.enum(AVAILABLE_REGION_IDS).optional().describe("Region: eu-central-1 (Falkenstein) \u2014 currently the only available region.")
3321
3378
  },
3322
3379
  handler: async (args, ctx) => {
3323
3380
  const teamId = await ctx.resolveTeamId();
@@ -3350,9 +3407,9 @@ defineTool({
3350
3407
  'Example: update_project({ project_id: "prj_abc", name: "billing-prod" }) \u2192 { project: { name: "billing-prod", \u2026 } }'
3351
3408
  ].join("\n"),
3352
3409
  input: {
3353
- project_id: z16.string().describe("Project publicId."),
3354
- name: z16.string().min(1).max(60).optional().describe("New name (1\u201360 chars)."),
3355
- description: z16.string().max(500).optional().describe("New description (\u2264500 chars).")
3410
+ project_id: z17.string().describe("Project publicId."),
3411
+ name: z17.string().min(1).max(60).optional().describe("New name (1\u201360 chars)."),
3412
+ description: z17.string().max(500).optional().describe("New description (\u2264500 chars).")
3356
3413
  },
3357
3414
  handler: async (args, ctx) => {
3358
3415
  if (args.name === void 0 && args.description === void 0) {
@@ -3386,7 +3443,7 @@ defineTool({
3386
3443
  'Example: get_project({ project_id: "prj_abc" }) \u2192 { project: { id: 12, name: "billing", \u2026 } }'
3387
3444
  ].join("\n"),
3388
3445
  input: {
3389
- project_id: z16.string().describe("Project publicId (e.g. prj_abc123).")
3446
+ project_id: z17.string().describe("Project publicId (e.g. prj_abc123).")
3390
3447
  },
3391
3448
  handler: async (args, ctx) => {
3392
3449
  const teamId = await ctx.resolveTeamId();
@@ -3398,7 +3455,7 @@ defineTool({
3398
3455
  });
3399
3456
 
3400
3457
  // src/tools/dev-tasks.ts
3401
- import { z as z17 } from "zod";
3458
+ import { z as z18 } from "zod";
3402
3459
  var NOT_A_RUN = "Filing a task does NOT start an agent. It writes a prompt into the box's backlog for someone to run; a suspended box keeps it until it wakes.";
3403
3460
  defineTool({
3404
3461
  name: "list_dev_tasks",
@@ -3418,7 +3475,7 @@ defineTool({
3418
3475
  'Example: list_dev_tasks({ project_id: 26 }) \u2192 { items: [{ publicId: "task_\u2026", status: "idea", title: "Fix the footprint join" }] }'
3419
3476
  ].join("\n"),
3420
3477
  input: {
3421
- project_id: z17.union([z17.number().int().positive(), z17.string()]).describe('Project \u2014 publicId ("prj_\u2026") or numeric id.')
3478
+ project_id: z18.union([z18.number().int().positive(), z18.string()]).describe('Project \u2014 publicId ("prj_\u2026") or numeric id.')
3422
3479
  },
3423
3480
  handler: async (args, ctx) => {
3424
3481
  const teamId = await ctx.resolveTeamId();
@@ -3447,7 +3504,7 @@ defineTool({
3447
3504
  'Example: get_dev_task({ task_id: "task_hy1i2jdp\u2026" }) \u2192 { data: { title: "Footprint coverage", body: "The BBRUUID join returns 17 of 49 \u2026", status: "idea" } }'
3448
3505
  ].join("\n"),
3449
3506
  input: {
3450
- task_id: z17.union([z17.number().int().positive(), z17.string()]).describe('Task \u2014 publicId ("task_\u2026") or numeric id.')
3507
+ task_id: z18.union([z18.number().int().positive(), z18.string()]).describe('Task \u2014 publicId ("task_\u2026") or numeric id.')
3451
3508
  },
3452
3509
  handler: async (args, ctx) => {
3453
3510
  const teamId = await ctx.resolveTeamId();
@@ -3475,10 +3532,10 @@ defineTool({
3475
3532
  'Example: create_dev_task({ project_id: 26, service_id: 51, title: "Footprint coverage", body: "The BBRUUID join returns 17 of 49 \u2026" })'
3476
3533
  ].join("\n"),
3477
3534
  input: {
3478
- project_id: z17.union([z17.number().int().positive(), z17.string()]).describe('Project \u2014 publicId ("prj_\u2026") or numeric id.'),
3479
- title: z17.string().min(1).max(200).describe("One-line title, \u2264200 chars."),
3480
- body: z17.string().max(2e4).optional().describe("The prompt handed to the agent. Markdown, \u226420 000 chars."),
3481
- service_id: z17.union([z17.number().int().positive(), z17.string()]).optional().describe(
3535
+ project_id: z18.union([z18.number().int().positive(), z18.string()]).describe('Project \u2014 publicId ("prj_\u2026") or numeric id.'),
3536
+ title: z18.string().min(1).max(200).describe("One-line title, \u2264200 chars."),
3537
+ body: z18.string().max(2e4).optional().describe("The prompt handed to the agent. Markdown, \u226420 000 chars."),
3538
+ service_id: z18.union([z18.number().int().positive(), z18.string()]).optional().describe(
3482
3539
  'Dev box to pin it to \u2014 publicId ("svc_\u2026") or numeric id. Omit for a loose idea.'
3483
3540
  )
3484
3541
  },
@@ -3524,13 +3581,13 @@ defineTool({
3524
3581
  'Example: update_dev_task({ task_id: "task_hy1i2jdp\u2026", status: "done" }) \u2192 { data: { status: "done" } }'
3525
3582
  ].join("\n"),
3526
3583
  input: {
3527
- task_id: z17.union([z17.number().int().positive(), z17.string()]).describe('Task \u2014 publicId ("task_\u2026") or numeric id.'),
3528
- status: z17.enum(["idea", "done"]).optional().describe("The only two a person may set. The runner owns the rest of the lifecycle."),
3529
- title: z17.string().min(1).max(200).optional().describe("New title."),
3530
- body: z17.string().max(2e4).optional().describe("New prompt body."),
3531
- 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."),
3532
- provider: z17.enum(["claude", "codex", "opencode"]).nullable().optional().describe("Which agent runs this task, or null to leave it to the box."),
3533
- permission_mode: z17.enum(["acceptEdits", "bypassPermissions", "default"]).optional().describe(
3584
+ task_id: z18.union([z18.number().int().positive(), z18.string()]).describe('Task \u2014 publicId ("task_\u2026") or numeric id.'),
3585
+ status: z18.enum(["idea", "done"]).optional().describe("The only two a person may set. The runner owns the rest of the lifecycle."),
3586
+ title: z18.string().min(1).max(200).optional().describe("New title."),
3587
+ body: z18.string().max(2e4).optional().describe("New prompt body."),
3588
+ 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."),
3589
+ provider: z18.enum(["claude", "codex", "opencode"]).nullable().optional().describe("Which agent runs this task, or null to leave it to the box."),
3590
+ permission_mode: z18.enum(["acceptEdits", "bypassPermissions", "default"]).optional().describe(
3534
3591
  "How the agent may act. bypassPermissions asks for nothing \u2014 set it only on request."
3535
3592
  )
3536
3593
  },
@@ -3555,7 +3612,7 @@ defineTool({
3555
3612
  });
3556
3613
 
3557
3614
  // src/tools/resource-links.ts
3558
- import { z as z18 } from "zod";
3615
+ import { z as z19 } from "zod";
3559
3616
  var RESOURCE_LINK_TYPES = [
3560
3617
  "database",
3561
3618
  "object_storage",
@@ -3616,7 +3673,7 @@ defineTool({
3616
3673
  'Example: list_service_resources({ service_id: "svc_abc" }) \u2192 { items: [{ id: 7, resourceType: "database", resourceId: 42, alias: "APP_DB" }] }'
3617
3674
  ].join("\n"),
3618
3675
  input: {
3619
- service_id: z18.union([z18.number().int().positive(), z18.string()]).describe('Service \u2014 publicId ("svc_\u2026") or numeric id.')
3676
+ service_id: z19.union([z19.number().int().positive(), z19.string()]).describe('Service \u2014 publicId ("svc_\u2026") or numeric id.')
3620
3677
  },
3621
3678
  handler: async (args, ctx) => {
3622
3679
  const teamId = await ctx.resolveTeamId();
@@ -3653,10 +3710,10 @@ defineTool({
3653
3710
  'Example: link_resource_to_service({ service_id: "svc_abc", resource_type: "database", resource_id: 42, alias: "APP_DB" }) \u2192 { link: { id: 7, alias: "APP_DB" } }'
3654
3711
  ].join("\n"),
3655
3712
  input: {
3656
- service_id: z18.union([z18.number().int().positive(), z18.string()]).describe('Consuming service \u2014 publicId ("svc_\u2026") or numeric id.'),
3657
- resource_type: z18.enum(RESOURCE_LINK_TYPES).describe("Kind of resource being linked."),
3658
- resource_id: z18.number().int().positive().describe("NUMERIC id of the resource (e.g. database.id) \u2014 not the publicId."),
3659
- alias: z18.string().min(1).max(48).regex(
3713
+ service_id: z19.union([z19.number().int().positive(), z19.string()]).describe('Consuming service \u2014 publicId ("svc_\u2026") or numeric id.'),
3714
+ resource_type: z19.enum(RESOURCE_LINK_TYPES).describe("Kind of resource being linked."),
3715
+ resource_id: z19.number().int().positive().describe("NUMERIC id of the resource (e.g. database.id) \u2014 not the publicId."),
3716
+ alias: z19.string().min(1).max(48).regex(
3660
3717
  /^[A-Z][A-Z0-9_]*$/,
3661
3718
  "Alias must be uppercase letters, digits and underscores, starting with a letter."
3662
3719
  ).describe('Uppercase env-var prefix, e.g. "APP_DB". Unique within the service.')
@@ -3692,8 +3749,8 @@ defineTool({
3692
3749
  'Example: unlink_resource_from_service({ service_id: "svc_abc", link_id: 7 }) \u2192 { ok: true }'
3693
3750
  ].join("\n"),
3694
3751
  input: {
3695
- service_id: z18.union([z18.number().int().positive(), z18.string()]).describe('Service \u2014 publicId ("svc_\u2026") or numeric id.'),
3696
- link_id: z18.number().int().positive().describe("Numeric linkId from list_service_resources (the link's own `id`).")
3752
+ service_id: z19.union([z19.number().int().positive(), z19.string()]).describe('Service \u2014 publicId ("svc_\u2026") or numeric id.'),
3753
+ link_id: z19.number().int().positive().describe("Numeric linkId from list_service_resources (the link's own `id`).")
3697
3754
  },
3698
3755
  handler: async (args, ctx) => {
3699
3756
  const teamId = await ctx.resolveTeamId();
@@ -3705,7 +3762,7 @@ defineTool({
3705
3762
  });
3706
3763
 
3707
3764
  // src/tools/services.ts
3708
- import { z as z19 } from "zod";
3765
+ import { z as z20 } from "zod";
3709
3766
 
3710
3767
  // src/lib/app-templates.ts
3711
3768
  var MCP_APP_TEMPLATES = [
@@ -3957,16 +4014,16 @@ defineTool({
3957
4014
  ' - type: "web_service" | "private_service" | "worker" | "cron_job" | "static_site".',
3958
4015
  " - dev_environment: include Dev Boxes in the results (excluded by default).",
3959
4016
  "",
3960
- "Returns: { items: Service[] } \u2014 each service includes id, publicId, name, type, status, projectId, repoUrl, branch, runtime, createdAt.",
4017
+ "Returns: { items: Service[] } \u2014 each service includes id, publicId, name, type, status, projectId, branch, runtime, createdAt, and, for a source-built service, repoProvider/repoFullName/repoUrl (what it actually builds \u2014 the stored id alone names no repository).",
3961
4018
  "",
3962
4019
  'Example: list_services({ status: "failed" }) \u2192 only services that need attention.'
3963
4020
  ].join("\n"),
3964
4021
  input: {
3965
- project_id: z19.union([z19.number().int().positive(), z19.string()]).optional().describe("Project filter \u2014 numeric id or publicId."),
3966
- environment_id: z19.union([z19.number().int().positive(), z19.string()]).optional().describe("Environment filter \u2014 numeric id or publicId."),
3967
- status: z19.enum(["active", "deploying", "suspended", "failed", "not_deployed"]).optional().describe("Filter by current runtime status."),
3968
- type: z19.enum(["web_service", "private_service", "worker", "cron_job", "static_site"]).optional().describe("Filter by service type."),
3969
- dev_environment: z19.boolean().optional().describe(
4022
+ project_id: z20.union([z20.number().int().positive(), z20.string()]).optional().describe("Project filter \u2014 numeric id or publicId."),
4023
+ environment_id: z20.union([z20.number().int().positive(), z20.string()]).optional().describe("Environment filter \u2014 numeric id or publicId."),
4024
+ status: z20.enum(["active", "deploying", "suspended", "failed", "not_deployed"]).optional().describe("Filter by current runtime status."),
4025
+ type: z20.enum(["web_service", "private_service", "worker", "cron_job", "static_site"]).optional().describe("Filter by service type."),
4026
+ dev_environment: z20.boolean().optional().describe(
3970
4027
  "Include agentic Dev Boxes in the results (excluded by default; see list_dev_environments)."
3971
4028
  )
3972
4029
  },
@@ -4007,7 +4064,7 @@ defineTool({
4007
4064
  name: "create_service",
4008
4065
  category: "services",
4009
4066
  description: [
4010
- "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.",
4067
+ 'Create a new service in a project. Source is either a connected git repo (github_repo "owner/name", or github_repo_id) OR a pre-built docker_image \u2014 never both.',
4011
4068
  "",
4012
4069
  "When to use: the user wants to deploy something new. For a one-command AI dev environment specifically, prefer create_dev_environment (it also attaches the /workspace volume and sets the MCP keys).",
4013
4070
  "",
@@ -4020,7 +4077,9 @@ defineTool({
4020
4077
  " - name: service name (1\u2013100 chars).",
4021
4078
  ' - type: "web_service" | "private_service" | "worker" | "cron_job" | "static_site".',
4022
4079
  " - docker_image (optional): pre-built image ref to deploy instead of building from source.",
4023
- " - github_repo_id (optional): connect a previously-linked GitHub repo by numeric id.",
4080
+ ' - 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.',
4081
+ " - 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.",
4082
+ " - 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
4083
  ' - branch (optional): git branch (default "main").',
4025
4084
  " - install_command / build_command / start_command (optional): build/run shell commands. start_command is required for web/private services without a docker_image.",
4026
4085
  " - cron_schedule (optional): cron expression \u2014 required for cron_job.",
@@ -4035,33 +4094,42 @@ defineTool({
4035
4094
  "",
4036
4095
  "Returns: { service: Service, deployId: number | null }.",
4037
4096
  "",
4038
- 'Example: create_service({ project_id: "prj_abc", name: "api", type: "web_service", github_repo_id: 42 }) \u2192 { service: { publicId: "svc_\u2026" }, deployId: 1234 }',
4097
+ '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 }',
4098
+ "Repo not listed by list_github_repos? Call sync_github_repos \u2014 a repository pushed since the last sync is invisible until then.",
4039
4099
  '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
4100
  ].join("\n"),
4041
4101
  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(
4102
+ project_id: z20.union([z20.number().int().positive(), z20.string()]).describe("Target project \u2014 numeric id or publicId."),
4103
+ name: z20.string().min(1).max(100).describe("Service name (1\u2013100 chars)."),
4104
+ type: z20.enum(SERVICE_TYPES).describe("Service type."),
4105
+ docker_image: z20.string().max(500).optional().describe(
4046
4106
  "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
4107
  ),
4048
- github_repo_id: z19.number().int().positive().optional().describe("Linked GitHub repo numeric id. Mutually exclusive with docker_image."),
4049
- branch: z19.string().max(200).optional().describe('Git branch (default "main").'),
4050
- install_command: z19.string().max(1e3).optional().describe("Install shell command."),
4051
- build_command: z19.string().max(1e3).optional().describe("Build shell command."),
4052
- start_command: z19.string().max(1e3).optional().describe("Start shell command (required for web/private services without an image)."),
4053
- cron_schedule: z19.string().max(100).optional().describe("Cron expression \u2014 required for cron_job."),
4054
- publish_path: z19.string().max(500).optional().describe("Static-site output dir."),
4055
- runtime: z19.string().max(50).optional().describe("Runtime hint (node/bun/python/\u2026)."),
4056
- port: z19.number().int().min(1).max(65535).optional().describe(
4108
+ github_repo: z20.string().min(1).max(255).optional().describe(
4109
+ 'Connected GitHub repo as "owner/name" (see list_github_repos). Resolved server-side. Mutually exclusive with github_repo_id and docker_image.'
4110
+ ),
4111
+ github_repo_id: z20.number().int().positive().optional().describe(
4112
+ "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."
4113
+ ),
4114
+ git_repo_url: z20.string().max(2048).optional().describe(
4115
+ "Public https git URL. No embedded credentials \u2014 private repos need a provider connection. Mutually exclusive with github_repo_id and docker_image."
4116
+ ),
4117
+ branch: z20.string().max(200).optional().describe('Git branch (default "main").'),
4118
+ install_command: z20.string().max(1e3).optional().describe("Install shell command."),
4119
+ build_command: z20.string().max(1e3).optional().describe("Build shell command."),
4120
+ start_command: z20.string().max(1e3).optional().describe("Start shell command (required for web/private services without an image)."),
4121
+ cron_schedule: z20.string().max(100).optional().describe("Cron expression \u2014 required for cron_job."),
4122
+ publish_path: z20.string().max(500).optional().describe("Static-site output dir."),
4123
+ runtime: z20.string().max(50).optional().describe("Runtime hint (node/bun/python/\u2026)."),
4124
+ port: z20.number().int().min(1).max(65535).optional().describe(
4057
4125
  "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."
4058
4126
  ),
4059
- template_id: z19.string().max(64).optional().describe(
4127
+ template_id: z20.string().max(64).optional().describe(
4060
4128
  "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."
4061
4129
  ),
4062
- plan: z19.enum(SERVICE_PLANS).optional().describe('Service size (default "micro").'),
4063
- environment_id: z19.union([z19.number().int().positive(), z19.string()]).optional().describe("Bind to a specific environment; defaults to Production."),
4064
- auto_deploy: z19.boolean().optional().describe("Trigger the first deploy immediately (default true)."),
4130
+ plan: z20.enum(SERVICE_PLANS).optional().describe('Service size (default "micro").'),
4131
+ environment_id: z20.union([z20.number().int().positive(), z20.string()]).optional().describe("Bind to a specific environment; defaults to Production."),
4132
+ auto_deploy: z20.boolean().optional().describe("Trigger the first deploy immediately (default true)."),
4065
4133
  machine: machineInput
4066
4134
  },
4067
4135
  handler: async (args, ctx) => {
@@ -4076,7 +4144,9 @@ defineTool({
4076
4144
  projectId
4077
4145
  };
4078
4146
  if (args.docker_image !== void 0) input.dockerImage = args.docker_image;
4147
+ if (args.github_repo !== void 0) input.githubRepo = args.github_repo;
4079
4148
  if (args.github_repo_id !== void 0) input.githubRepoId = args.github_repo_id;
4149
+ if (args.git_repo_url !== void 0) input.gitRepoUrl = args.git_repo_url;
4080
4150
  if (args.branch !== void 0) input.branch = args.branch;
4081
4151
  if (args.install_command !== void 0) input.installCommand = args.install_command;
4082
4152
  if (args.build_command !== void 0) input.buildCommand = args.build_command;
@@ -4132,18 +4202,18 @@ defineTool({
4132
4202
  'Example: create_dev_environment({ project_id: "prj_abc", name: "scratch", hoststack_api_key: "hs_live_\u2026" })'
4133
4203
  ].join("\n"),
4134
4204
  input: {
4135
- project_id: z19.union([z19.number().int().positive(), z19.string()]).describe("Target project \u2014 numeric id or publicId."),
4136
- name: z19.string().min(1).max(100).optional().describe('Service name (default "dev-environment").'),
4137
- plan: z19.enum(SERVICE_PLANS).optional().describe(
4205
+ project_id: z20.union([z20.number().int().positive(), z20.string()]).describe("Target project \u2014 numeric id or publicId."),
4206
+ name: z20.string().min(1).max(100).optional().describe('Service name (default "dev-environment").'),
4207
+ plan: z20.enum(SERVICE_PLANS).optional().describe(
4138
4208
  'Box size (default "standard" \u2014 2 GB, the OOM-safe floor; a smaller plan is clamped up to "standard").'
4139
4209
  ),
4140
- disk_gb: z19.number().int().min(10).max(10240).optional().describe("/workspace volume size in GB (default 10, min 10, max 10240)."),
4141
- hoststack_api_key: z19.string().optional().describe("Value for HOSTSTACK_API_KEY (enables the hoststack MCP in-container)."),
4142
- poststack_api_key: z19.string().optional().describe("Value for POSTSTACK_API_KEY (enables the poststack MCP in-container)."),
4143
- repo_url: z19.string().max(500).optional().describe(
4210
+ disk_gb: z20.number().int().min(10).max(10240).optional().describe("/workspace volume size in GB (default 10, min 10, max 10240)."),
4211
+ hoststack_api_key: z20.string().optional().describe("Value for HOSTSTACK_API_KEY (enables the hoststack MCP in-container)."),
4212
+ poststack_api_key: z20.string().optional().describe("Value for POSTSTACK_API_KEY (enables the poststack MCP in-container)."),
4213
+ repo_url: z20.string().max(500).optional().describe(
4144
4214
  "Clone this git URL into /workspace on first boot (HTTPS, or SSH once a key is set)."
4145
4215
  ),
4146
- branch: z19.string().max(200).optional().describe("Branch to clone (with repo_url)."),
4216
+ branch: z20.string().max(200).optional().describe("Branch to clone (with repo_url)."),
4147
4217
  machine: machineInput
4148
4218
  },
4149
4219
  handler: async (args, ctx) => {
@@ -4260,9 +4330,9 @@ defineTool({
4260
4330
  '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.'
4261
4331
  ].join("\n"),
4262
4332
  input: {
4263
- service_id: z19.union([z19.number().int().positive(), z19.string()]).describe("Source service to debug \u2014 numeric id or publicId."),
4264
- include_database_clone: z19.boolean().optional().describe("Clone the linked database so the app runs on copied data (default true)."),
4265
- name: z19.string().min(1).max(100).optional().describe('Dev box name (default "<source>-dev").')
4333
+ service_id: z20.union([z20.number().int().positive(), z20.string()]).describe("Source service to debug \u2014 numeric id or publicId."),
4334
+ include_database_clone: z20.boolean().optional().describe("Clone the linked database so the app runs on copied data (default true)."),
4335
+ name: z20.string().min(1).max(100).optional().describe('Dev box name (default "<source>-dev").')
4266
4336
  },
4267
4337
  handler: async (args, ctx) => {
4268
4338
  const teamId = await ctx.resolveTeamId();
@@ -4303,7 +4373,7 @@ defineTool({
4303
4373
  'Example: delete_dev_environment({ service_id: "svc_api_dev" }) \u2192 removes the dev box, its cloned database, and the /workspace volume.'
4304
4374
  ].join("\n"),
4305
4375
  input: {
4306
- service_id: z19.union([z19.number().int().positive(), z19.string()]).describe("The dev box to tear down \u2014 numeric id or publicId.")
4376
+ service_id: z20.union([z20.number().int().positive(), z20.string()]).describe("The dev box to tear down \u2014 numeric id or publicId.")
4307
4377
  },
4308
4378
  handler: async (args, ctx) => {
4309
4379
  const teamId = await ctx.resolveTeamId();
@@ -4341,8 +4411,8 @@ defineTool({
4341
4411
  'Example: resize_dev_environment({ service_id: "svc_skyskraber_dev", size: "large" }) \u2192 bumps the box to the large tier, applied live.'
4342
4412
  ].join("\n"),
4343
4413
  input: {
4344
- service_id: z19.union([z19.number().int().positive(), z19.string()]).describe("The box to resize \u2014 numeric id or publicId."),
4345
- size: z19.enum(SERVICE_PLANS).describe(
4414
+ service_id: z20.union([z20.number().int().positive(), z20.string()]).describe("The box to resize \u2014 numeric id or publicId."),
4415
+ size: z20.enum(SERVICE_PLANS).describe(
4346
4416
  'Target size tier (service catalog size, e.g. "standard", "large", "xlarge").'
4347
4417
  )
4348
4418
  },
@@ -4479,7 +4549,7 @@ defineTool({
4479
4549
  "Inputs:",
4480
4550
  ' - 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.',
4481
4551
  ' - 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).',
4482
- " - github_repo_id (for github_repo): numeric id of a connected GitHub repo.",
4552
+ " - 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.",
4483
4553
  " - clone_url (for url): an http(s) git clone URL.",
4484
4554
  " - branch (optional): branch to clone.",
4485
4555
  ' - databases (optional): companion services to attach \u2014 any of "postgres", "redis", "meilisearch".',
@@ -4489,24 +4559,26 @@ defineTool({
4489
4559
  "",
4490
4560
  "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.",
4491
4561
  "",
4492
- 'Example: create_standalone_dev_environment({ name: "app-dev", source_kind: "github_repo", github_repo_id: 42, databases: ["postgres","redis"] })'
4562
+ '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"] })'
4493
4563
  ].join("\n"),
4494
4564
  input: {
4495
- name: z19.string().min(1).max(100).optional().describe(
4565
+ name: z20.string().min(1).max(100).optional().describe(
4496
4566
  '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.'
4497
4567
  ),
4498
- source_kind: z19.enum(["github_repo", "url", "blank"]).describe("Where the code comes from."),
4499
- github_repo_id: z19.number().int().positive().optional().describe('Connected GitHub repo id (required when source_kind="github_repo").'),
4500
- clone_url: z19.string().url().optional().describe('http(s) git clone URL (required when source_kind="url").'),
4501
- branch: z19.string().min(1).max(255).optional().describe("Branch to clone."),
4502
- databases: z19.array(z19.enum(["postgres", "redis", "meilisearch"])).optional().describe("Companion services to attach (fresh + empty)."),
4503
- plan: z19.enum(SERVICE_PLANS).optional().describe(
4568
+ source_kind: z20.enum(["github_repo", "url", "blank"]).describe("Where the code comes from."),
4569
+ github_repo_id: z20.number().int().positive().optional().describe(
4570
+ 'Connected GitHub repo id from list_github_repos (required when source_kind="github_repo").'
4571
+ ),
4572
+ clone_url: z20.string().url().optional().describe('http(s) git clone URL (required when source_kind="url").'),
4573
+ branch: z20.string().min(1).max(255).optional().describe("Branch to clone."),
4574
+ databases: z20.array(z20.enum(["postgres", "redis", "meilisearch"])).optional().describe("Companion services to attach (fresh + empty)."),
4575
+ plan: z20.enum(SERVICE_PLANS).optional().describe(
4504
4576
  'Box size (default "standard" \u2014 2 GB; a smaller plan is floored to "standard").'
4505
4577
  ),
4506
- agent_accounts: z19.array(
4507
- z19.object({
4508
- provider: z19.enum(["claude", "codex", "opencode"]),
4509
- account_id: z19.number().int().positive()
4578
+ agent_accounts: z20.array(
4579
+ z20.object({
4580
+ provider: z20.enum(["claude", "codex", "opencode"]),
4581
+ account_id: z20.number().int().positive()
4510
4582
  })
4511
4583
  ).max(3).optional().describe(
4512
4584
  "Bind saved agent logins by account id per provider. Omit to inherit the box owner's default logins automatically."
@@ -4579,12 +4651,12 @@ defineTool({
4579
4651
  "Inputs:",
4580
4652
  ' - service_id: publicId of the service (e.g. "svc_abc123").',
4581
4653
  "",
4582
- '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.',
4654
+ '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.',
4583
4655
  "",
4584
4656
  'Example: get_service({ service_id: "svc_abc" }) \u2192 { service: { type: "web", status: "running", \u2026 }, config: { healthCheckGracePeriodSec: 120, \u2026 } }'
4585
4657
  ].join("\n"),
4586
4658
  input: {
4587
- service_id: z19.string().describe("Service publicId (e.g. svc_abc123).")
4659
+ service_id: z20.string().describe("Service publicId (e.g. svc_abc123).")
4588
4660
  },
4589
4661
  handler: async (args, ctx) => {
4590
4662
  const teamId = await ctx.resolveTeamId();
@@ -4616,7 +4688,7 @@ defineTool({
4616
4688
  'Example: get_service_metrics({ service_id: "svc_abc" }) \u2192 { metrics: { cpu: 0.42, memory: 0.71, \u2026 } }'
4617
4689
  ].join("\n"),
4618
4690
  input: {
4619
- service_id: z19.string().describe("Service publicId.")
4691
+ service_id: z20.string().describe("Service publicId.")
4620
4692
  },
4621
4693
  handler: async (args, ctx) => {
4622
4694
  const teamId = await ctx.resolveTeamId();
@@ -4649,9 +4721,9 @@ defineTool({
4649
4721
  'Example: get_service_metrics_history({ service_id: "svc_abc", from: "-1h" }) \u2192 ~120 points for the last hour.'
4650
4722
  ].join("\n"),
4651
4723
  input: {
4652
- service_id: z19.string().describe("Service publicId."),
4653
- from: z19.string().optional().describe('ISO-8601 lower bound or relative offset (e.g. "-1h", "-2d").'),
4654
- to: z19.string().optional().describe("ISO-8601 upper bound; defaults to now.")
4724
+ service_id: z20.string().describe("Service publicId."),
4725
+ from: z20.string().optional().describe('ISO-8601 lower bound or relative offset (e.g. "-1h", "-2d").'),
4726
+ to: z20.string().optional().describe("ISO-8601 upper bound; defaults to now.")
4655
4727
  },
4656
4728
  handler: async (args, ctx) => {
4657
4729
  const teamId = await ctx.resolveTeamId();
@@ -4695,8 +4767,8 @@ defineTool({
4695
4767
  'Example: update_service({ service_id: "svc_abc", name: "api-prod" }) \u2192 { service: { name: "api-prod", \u2026 } }'
4696
4768
  ].join("\n"),
4697
4769
  input: {
4698
- service_id: z19.string().describe("Service publicId."),
4699
- name: z19.string().min(1).max(60).describe("New service name (1\u201360 chars).")
4770
+ service_id: z20.string().describe("Service publicId."),
4771
+ name: z20.string().min(1).max(60).describe("New service name (1\u201360 chars).")
4700
4772
  },
4701
4773
  handler: async (args, ctx) => {
4702
4774
  const teamId = await ctx.resolveTeamId();
@@ -4741,6 +4813,11 @@ defineTool({
4741
4813
  " - scale_cpu_threshold, scale_memory_threshold (optional): integer 10\u2013100 \u2014 autoscale trigger percentage.",
4742
4814
  " - scale_requests_per_sec_threshold (optional): integer 1\u201310000 \u2014 autoscale on request rate instead of (or alongside) CPU and memory. Pass null to stop scaling on request rate.",
4743
4815
  " - build_timeout_seconds (optional): integer 60\u20131800 \u2014 how long a build may run before it is killed. Raise it for a service whose build legitimately takes longer than the default; pass null to go back to the default.",
4816
+ " - plan (optional): move the service to a different size tier. This is the supported way to grow past the current tier's ceiling \u2014 memory_mb and cpu_shares are CLAMPED to the tier, so raising them alone does nothing once you are at the top of it. Memory and CPU apply to the running container where possible; disk grows on the next recreate. Changing tier changes what the service costs.",
4817
+ " - docker_image (optional): repoint an image-backed service at a different image. Takes effect on the next deploy \u2014 this writes the reference, it does not pull. Pass null to clear it.",
4818
+ " - cron_schedule (optional): the schedule for a cron_job service. Pass null to clear it.",
4819
+ ' - publish_path (optional): the directory a static_site publishes (e.g. "dist"). Pass null to clear it.',
4820
+ ' - runtime (optional): runtime hint ("node" | "bun" | "python" | \u2026), normally auto-detected from the repo. Set it when the detection is wrong.',
4744
4821
  ` - log_filter_rules (optional): list of { pattern, action } rules applied at read time \u2014 to get_service_logs (tail AND count) and to the dashboard's live stream alike. Pattern matches the message by case-insensitive substring, literally (a % or _ is not a wildcard); action is "drop" (filter out) or "downgrade" (flip stderr \u2192 stdout so it stops looking like an error). Stored lines are never altered, so removing a rule re-exposes everything it was hiding. Pass [] to clear all rules. Capped at 50 rules.`,
4745
4822
  "",
4746
4823
  "Returns: { service?: Service, config?: ServiceConfig } \u2014 whichever rows were touched.",
@@ -4748,45 +4825,54 @@ defineTool({
4748
4825
  'Example: update_service_config({ service_id: "svc_abc", health_check_grace_period_sec: 180 }) \u2192 { config: { healthCheckGracePeriodSec: 180, \u2026 } }'
4749
4826
  ].join("\n"),
4750
4827
  input: {
4751
- service_id: z19.string().describe("Service publicId."),
4752
- install_command: z19.string().nullable().optional().describe("Install shell command. Null clears."),
4753
- build_command: z19.string().nullable().optional().describe("Build shell command. Null clears."),
4754
- start_command: z19.string().nullable().optional().describe("Start shell command. Null clears."),
4755
- branch: z19.string().optional().describe("Git branch to track."),
4756
- root_directory: z19.string().optional().describe("Build context root."),
4757
- dockerfile_path: z19.string().nullable().optional().describe("Path to Dockerfile relative to root. Null clears."),
4758
- auto_deploy: z19.boolean().optional().describe("Auto-deploy on push."),
4759
- health_check_path: z19.string().nullable().optional().describe('HTTP health-check path (e.g. "/health"). Null = TCP-only check.'),
4760
- health_check_enabled: z19.boolean().optional().describe("Toggle health checking on/off."),
4761
- allow_search_indexing: z19.boolean().optional().describe(
4828
+ service_id: z20.string().describe("Service publicId."),
4829
+ install_command: z20.string().nullable().optional().describe("Install shell command. Null clears."),
4830
+ build_command: z20.string().nullable().optional().describe("Build shell command. Null clears."),
4831
+ start_command: z20.string().nullable().optional().describe("Start shell command. Null clears."),
4832
+ branch: z20.string().optional().describe("Git branch to track."),
4833
+ root_directory: z20.string().optional().describe("Build context root."),
4834
+ dockerfile_path: z20.string().nullable().optional().describe("Path to Dockerfile relative to root. Null clears."),
4835
+ auto_deploy: z20.boolean().optional().describe("Auto-deploy on push."),
4836
+ health_check_path: z20.string().nullable().optional().describe('HTTP health-check path (e.g. "/health"). Null = TCP-only check.'),
4837
+ health_check_enabled: z20.boolean().optional().describe("Toggle health checking on/off."),
4838
+ allow_search_indexing: z20.boolean().optional().describe(
4762
4839
  "Let search engines index the free *.hoststack.dev platform URL (off by default). Custom domains are always indexable. Applies on the next deploy."
4763
4840
  ),
4764
- health_check_interval: z19.number().int().min(5).max(300).optional().describe("How often the check runs, in seconds (5\u2013300)."),
4765
- health_check_timeout: z19.number().int().min(1).max(60).optional().describe("Single-attempt timeout in seconds (1\u201360)."),
4766
- health_check_grace_period_sec: z19.number().int().min(1).max(1800).optional().describe(
4841
+ health_check_interval: z20.number().int().min(5).max(300).optional().describe("How often the check runs, in seconds (5\u2013300)."),
4842
+ health_check_timeout: z20.number().int().min(1).max(60).optional().describe("Single-attempt timeout in seconds (1\u201360)."),
4843
+ health_check_grace_period_sec: z20.number().int().min(1).max(1800).optional().describe(
4767
4844
  "Startup grace period in seconds (1\u20131800). Raise this if the app needs more time to boot before health checks start counting failures."
4768
4845
  ),
4769
- memory_mb: z19.number().int().min(128).max(16384).optional().describe("Container memory cap in MB (128\u201316384)."),
4770
- cpu_shares: z19.number().int().min(128).max(4096).optional().describe("Relative CPU weight (128\u20134096)."),
4771
- disk_size_gb: z19.number().int().min(1).max(100).optional().describe("Ephemeral disk size in GB (1\u2013100)."),
4772
- port: z19.number().int().min(1).max(65535).optional().describe("Container port the platform forwards traffic to."),
4773
- protocol: z19.enum(["http", "tcp"]).optional().describe("Traffic protocol."),
4774
- restart_policy: z19.enum(["always", "on-failure", "no"]).optional().describe("Docker restart policy."),
4775
- deploy_strategy: z19.enum(["rolling", "recreate"]).optional().describe(
4846
+ memory_mb: z20.number().int().min(128).max(16384).optional().describe("Container memory cap in MB (128\u201316384)."),
4847
+ cpu_shares: z20.number().int().min(128).max(4096).optional().describe("Relative CPU weight (128\u20134096)."),
4848
+ disk_size_gb: z20.number().int().min(1).max(100).optional().describe("Ephemeral disk size in GB (1\u2013100)."),
4849
+ port: z20.number().int().min(1).max(65535).optional().describe("Container port the platform forwards traffic to."),
4850
+ protocol: z20.enum(["http", "tcp"]).optional().describe("Traffic protocol."),
4851
+ restart_policy: z20.enum(["always", "on-failure", "no"]).optional().describe("Docker restart policy."),
4852
+ deploy_strategy: z20.enum(["rolling", "recreate"]).optional().describe(
4776
4853
  '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.'
4777
4854
  ),
4778
- pre_deploy_command: z19.string().optional().describe("Shell command run before the new release accepts traffic."),
4779
- instance_count: z19.number().int().positive().max(50).optional().describe("Pin min and max instances to this value (1\u201350)."),
4780
- min_instances: z19.number().int().min(0).max(50).optional().describe("Autoscale lower bound. Use with max_instances for a range."),
4781
- max_instances: z19.number().int().min(1).max(50).optional().describe("Autoscale upper bound. Use with min_instances for a range."),
4782
- scale_cpu_threshold: z19.number().int().min(10).max(100).optional().describe("Autoscale CPU trigger percentage (10\u2013100)."),
4783
- scale_memory_threshold: z19.number().int().min(10).max(100).optional().describe("Autoscale memory trigger percentage (10\u2013100)."),
4784
- 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."),
4785
- 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."),
4786
- log_filter_rules: z19.array(
4787
- z19.object({
4788
- pattern: z19.string().min(1).max(200),
4789
- action: z19.enum(["drop", "downgrade"])
4855
+ pre_deploy_command: z20.string().optional().describe("Shell command run before the new release accepts traffic."),
4856
+ instance_count: z20.number().int().positive().max(50).optional().describe("Pin min and max instances to this value (1\u201350)."),
4857
+ min_instances: z20.number().int().min(0).max(50).optional().describe("Autoscale lower bound. Use with max_instances for a range."),
4858
+ max_instances: z20.number().int().min(1).max(50).optional().describe("Autoscale upper bound. Use with min_instances for a range."),
4859
+ scale_cpu_threshold: z20.number().int().min(10).max(100).optional().describe("Autoscale CPU trigger percentage (10\u2013100)."),
4860
+ scale_memory_threshold: z20.number().int().min(10).max(100).optional().describe("Autoscale memory trigger percentage (10\u2013100)."),
4861
+ 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."),
4862
+ 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."),
4863
+ plan: z20.enum(SERVICE_PLANS).optional().describe(
4864
+ "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."
4865
+ ),
4866
+ docker_image: z20.string().max(500).nullable().optional().describe(
4867
+ "Repoint an image-backed service. Applies on the next deploy. null clears it."
4868
+ ),
4869
+ cron_schedule: z20.string().max(100).nullable().optional().describe("Cron expression for a cron_job service. null clears it."),
4870
+ publish_path: z20.string().max(500).nullable().optional().describe('Static-site output directory, e.g. "dist". null clears it.'),
4871
+ runtime: z20.string().max(50).optional().describe("Runtime hint (node/bun/python/\u2026). Normally auto-detected."),
4872
+ log_filter_rules: z20.array(
4873
+ z20.object({
4874
+ pattern: z20.string().min(1).max(200),
4875
+ action: z20.enum(["drop", "downgrade"])
4790
4876
  })
4791
4877
  ).max(50).optional().describe(
4792
4878
  "Runtime-log filter rules. Empty array [] clears all rules. Each pattern is case-insensitive substring match against the message."
@@ -4806,6 +4892,10 @@ defineTool({
4806
4892
  if (args.auto_deploy !== void 0) serviceUpdate["autoDeploy"] = args.auto_deploy;
4807
4893
  if (args.health_check_path !== void 0)
4808
4894
  serviceUpdate["healthCheckPath"] = args.health_check_path;
4895
+ if (args.plan !== void 0) serviceUpdate["plan"] = args.plan;
4896
+ if (args.cron_schedule !== void 0) serviceUpdate["cronSchedule"] = args.cron_schedule;
4897
+ if (args.publish_path !== void 0) serviceUpdate["publishPath"] = args.publish_path;
4898
+ if (args.runtime !== void 0) serviceUpdate["runtime"] = args.runtime;
4809
4899
  const configUpdate = {};
4810
4900
  if (args.health_check_enabled !== void 0)
4811
4901
  configUpdate["healthCheckEnabled"] = args.health_check_enabled;
@@ -4841,6 +4931,7 @@ defineTool({
4841
4931
  configUpdate["scaleRequestsPerSecThreshold"] = args.scale_requests_per_sec_threshold;
4842
4932
  if (args.build_timeout_seconds !== void 0)
4843
4933
  configUpdate["buildTimeoutSeconds"] = args.build_timeout_seconds;
4934
+ if (args.docker_image !== void 0) configUpdate["dockerImage"] = args.docker_image;
4844
4935
  if (args.log_filter_rules !== void 0) {
4845
4936
  configUpdate["logFilterRules"] = args.log_filter_rules;
4846
4937
  }
@@ -4889,7 +4980,7 @@ defineTool({
4889
4980
  'Example: suspend_service({ service_id: "svc_dev" }) \u2192 { ok: true }'
4890
4981
  ].join("\n"),
4891
4982
  input: {
4892
- service_id: z19.string().describe("Service publicId.")
4983
+ service_id: z20.string().describe("Service publicId.")
4893
4984
  },
4894
4985
  handler: async (args, ctx) => {
4895
4986
  const teamId = await ctx.resolveTeamId();
@@ -4913,7 +5004,7 @@ defineTool({
4913
5004
  'Example: resume_service({ service_id: "svc_dev" }) \u2192 { ok: true }'
4914
5005
  ].join("\n"),
4915
5006
  input: {
4916
- service_id: z19.string().describe("Service publicId.")
5007
+ service_id: z20.string().describe("Service publicId.")
4917
5008
  },
4918
5009
  handler: async (args, ctx) => {
4919
5010
  const teamId = await ctx.resolveTeamId();
@@ -4939,7 +5030,7 @@ defineTool({
4939
5030
  'Example: delete_service({ service_id: "svc_abandoned" }) \u2192 { ok: true }'
4940
5031
  ].join("\n"),
4941
5032
  input: {
4942
- service_id: z19.string().describe("Service publicId.")
5033
+ service_id: z20.string().describe("Service publicId.")
4943
5034
  },
4944
5035
  handler: async (args, ctx) => {
4945
5036
  const teamId = await ctx.resolveTeamId();
@@ -4978,16 +5069,16 @@ defineTool({
4978
5069
  ' - Just count error lines without fetching them: get_service_logs({ service_id: "svc_abc", level: "error", since: "-5m", count_only: true }) \u2192 { count: 47 }'
4979
5070
  ].join("\n"),
4980
5071
  input: {
4981
- service_id: z19.string().describe("Service publicId."),
4982
- lines: z19.number().int().positive().max(1e3).optional().describe("Tail size; default 200, hard cap 1000."),
4983
- since: z19.string().optional().describe('ISO-8601 timestamp or relative offset (e.g. "-5m", "-1h").'),
4984
- until: z19.string().optional().describe("ISO-8601 timestamp or relative offset upper bound."),
4985
- stream: z19.enum(["stdout", "stderr"]).optional().describe("Restrict to one stream."),
4986
- level: z19.enum(["stdout", "stderr", "trace", "debug", "info", "warn", "error", "fatal"]).optional().describe(
5072
+ service_id: z20.string().describe("Service publicId."),
5073
+ lines: z20.number().int().positive().max(1e3).optional().describe("Tail size; default 200, hard cap 1000."),
5074
+ since: z20.string().optional().describe('ISO-8601 timestamp or relative offset (e.g. "-5m", "-1h").'),
5075
+ until: z20.string().optional().describe("ISO-8601 timestamp or relative offset upper bound."),
5076
+ stream: z20.enum(["stdout", "stderr"]).optional().describe("Restrict to one stream."),
5077
+ level: z20.enum(["stdout", "stderr", "trace", "debug", "info", "warn", "error", "fatal"]).optional().describe(
4987
5078
  "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)."
4988
5079
  ),
4989
- search: z19.string().max(100).optional().describe("Case-insensitive substring filter."),
4990
- count_only: z19.boolean().optional().describe("When true, return only { count } \u2014 skips the log payload.")
5080
+ search: z20.string().max(100).optional().describe("Case-insensitive substring filter."),
5081
+ count_only: z20.boolean().optional().describe("When true, return only { count } \u2014 skips the log payload.")
4991
5082
  },
4992
5083
  handler: async (args, ctx) => {
4993
5084
  const teamId = await ctx.resolveTeamId();
@@ -5036,14 +5127,14 @@ defineTool({
5036
5127
  '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 } } }.'
5037
5128
  ].join("\n"),
5038
5129
  input: {
5039
- service_ids: z19.array(z19.string()).min(1).max(10).describe("Service publicIds (1\u201310). Hard cap 10 to bound parallel work."),
5040
- lines_per_service: z19.number().int().positive().max(500).optional().describe("Tail size per service; default 100, hard cap 500."),
5041
- since: z19.string().optional().describe('ISO-8601 timestamp or relative offset (e.g. "-5m", "-1h").'),
5042
- until: z19.string().optional().describe("ISO-8601 timestamp or relative offset upper bound."),
5043
- stream: z19.enum(["stdout", "stderr"]).optional().describe("Restrict to one stream."),
5044
- level: z19.enum(["stdout", "stderr", "trace", "debug", "info", "warn", "error", "fatal"]).optional().describe("Structured log level filter (same as get_service_logs)."),
5045
- search: z19.string().max(100).optional().describe("Case-insensitive substring filter."),
5046
- count_only: z19.boolean().optional().describe("When true, return only counts per service \u2014 skips the log payload.")
5130
+ service_ids: z20.array(z20.string()).min(1).max(10).describe("Service publicIds (1\u201310). Hard cap 10 to bound parallel work."),
5131
+ lines_per_service: z20.number().int().positive().max(500).optional().describe("Tail size per service; default 100, hard cap 500."),
5132
+ since: z20.string().optional().describe('ISO-8601 timestamp or relative offset (e.g. "-5m", "-1h").'),
5133
+ until: z20.string().optional().describe("ISO-8601 timestamp or relative offset upper bound."),
5134
+ stream: z20.enum(["stdout", "stderr"]).optional().describe("Restrict to one stream."),
5135
+ level: z20.enum(["stdout", "stderr", "trace", "debug", "info", "warn", "error", "fatal"]).optional().describe("Structured log level filter (same as get_service_logs)."),
5136
+ search: z20.string().max(100).optional().describe("Case-insensitive substring filter."),
5137
+ count_only: z20.boolean().optional().describe("When true, return only counts per service \u2014 skips the log payload.")
5047
5138
  },
5048
5139
  handler: async (args, ctx) => {
5049
5140
  const teamId = await ctx.resolveTeamId();
@@ -5089,7 +5180,7 @@ defineTool({
5089
5180
  });
5090
5181
 
5091
5182
  // src/tools/uptime.ts
5092
- import { z as z20 } from "zod";
5183
+ import { z as z21 } from "zod";
5093
5184
  var STATUS_MEANING = [
5094
5185
  "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."
5095
5186
  ].join("\n");
@@ -5111,7 +5202,7 @@ defineTool({
5111
5202
  "",
5112
5203
  "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' } }."
5113
5204
  ].join("\n"),
5114
- input: { serviceId: z20.number().int().positive() },
5205
+ input: { serviceId: z21.number().int().positive() },
5115
5206
  handler: async (args, ctx) => {
5116
5207
  const teamId = await ctx.resolveTeamId();
5117
5208
  const response = await ctx.api.get(
@@ -5157,14 +5248,14 @@ defineTool({
5157
5248
  "Example: set_uptime_check({ serviceId: 48, path: '/healthz', intervalSeconds: 60, failureThreshold: 3 }) \u2192 { check: { status: 'unknown', \u2026 } }."
5158
5249
  ].join("\n"),
5159
5250
  input: {
5160
- serviceId: z20.number().int().positive(),
5161
- enabled: z20.boolean().optional(),
5162
- path: z20.string().max(500).optional().describe('Must start with /. Default "/".'),
5163
- method: z20.enum(["GET", "HEAD"]).optional(),
5164
- expectedStatus: z20.number().int().min(100).max(599).optional(),
5165
- timeoutMs: z20.number().int().min(1e3).max(6e4).optional(),
5166
- intervalSeconds: z20.number().int().min(30).max(3600).optional(),
5167
- failureThreshold: z20.number().int().min(1).max(10).optional()
5251
+ serviceId: z21.number().int().positive(),
5252
+ enabled: z21.boolean().optional(),
5253
+ path: z21.string().max(500).optional().describe('Must start with /. Default "/".'),
5254
+ method: z21.enum(["GET", "HEAD"]).optional(),
5255
+ expectedStatus: z21.number().int().min(100).max(599).optional(),
5256
+ timeoutMs: z21.number().int().min(1e3).max(6e4).optional(),
5257
+ intervalSeconds: z21.number().int().min(30).max(3600).optional(),
5258
+ failureThreshold: z21.number().int().min(1).max(10).optional()
5168
5259
  },
5169
5260
  handler: async (args, ctx) => {
5170
5261
  const teamId = await ctx.resolveTeamId();
@@ -5195,7 +5286,7 @@ defineTool({
5195
5286
  "",
5196
5287
  "Example: delete_uptime_check({ serviceId: 48 }) \u2192 { success: true }."
5197
5288
  ].join("\n"),
5198
- input: { serviceId: z20.number().int().positive() },
5289
+ input: { serviceId: z21.number().int().positive() },
5199
5290
  handler: async (args, ctx) => {
5200
5291
  const teamId = await ctx.resolveTeamId();
5201
5292
  await ctx.api.delete(`/api/services/${teamId}/${args.serviceId}/uptime-check`);
@@ -5204,7 +5295,7 @@ defineTool({
5204
5295
  });
5205
5296
 
5206
5297
  // src/tools/volumes.ts
5207
- import { z as z21 } from "zod";
5298
+ import { z as z22 } from "zod";
5208
5299
  var MIN_VOLUME_SIZE_GB = 10;
5209
5300
  defineTool({
5210
5301
  name: "list_volumes",
@@ -5222,7 +5313,7 @@ defineTool({
5222
5313
  'Example: list_volumes({ service_id: "svc_abc" }) \u2192 { items: [{ name: "data", mountPath: "/var/data", sizeGb: 10, status: "active" }] }'
5223
5314
  ].join("\n"),
5224
5315
  input: {
5225
- service_id: z21.string().describe("Service publicId (e.g. svc_abc123).")
5316
+ service_id: z22.string().describe("Service publicId (e.g. svc_abc123).")
5226
5317
  },
5227
5318
  handler: async (args, ctx) => {
5228
5319
  const teamId = await ctx.resolveTeamId();
@@ -5251,15 +5342,15 @@ defineTool({
5251
5342
  '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" } }'
5252
5343
  ].join("\n"),
5253
5344
  input: {
5254
- service_id: z21.string().describe("Service publicId."),
5255
- name: z21.string().min(1).max(64).regex(/^[a-z0-9-]+$/).describe("Volume name (lowercase alphanumeric + hyphens)."),
5256
- mount_path: z21.string().startsWith("/").max(500).describe("In-container mount path (absolute)."),
5345
+ service_id: z22.string().describe("Service publicId."),
5346
+ name: z22.string().min(1).max(64).regex(/^[a-z0-9-]+$/).describe("Volume name (lowercase alphanumeric + hyphens)."),
5347
+ mount_path: z22.string().startsWith("/").max(500).describe("In-container mount path (absolute)."),
5257
5348
  // 10 GB is the real floor: the block-storage backend rejects anything
5258
5349
  // smaller. Advertising 1 GB here (and defaulting to it) meant taking the
5259
5350
  // defaults produced a volume that provisioned with `Hetzner API error:
5260
5351
  // 422` on the NEXT deploy, with nothing tying the failure back to the
5261
5352
  // size. Reject it at the call instead.
5262
- size_gb: z21.number().int().min(MIN_VOLUME_SIZE_GB).max(100).optional().describe(
5353
+ size_gb: z22.number().int().min(MIN_VOLUME_SIZE_GB).max(100).optional().describe(
5263
5354
  `Disk size in GB (minimum ${MIN_VOLUME_SIZE_GB}, default ${MIN_VOLUME_SIZE_GB}, max 100 via MCP).`
5264
5355
  )
5265
5356
  },
@@ -5298,11 +5389,11 @@ defineTool({
5298
5389
  'Example: update_volume({ service_id: "svc_abc", volume_id: "vol_xyz", size_gb: 20 }) \u2192 { volume: { sizeGb: 20, \u2026 } }'
5299
5390
  ].join("\n"),
5300
5391
  input: {
5301
- service_id: z21.string().describe("Service publicId."),
5302
- volume_id: z21.string().describe("Volume publicId (e.g. vol_\u2026)."),
5303
- mount_path: z21.string().startsWith("/").max(500).optional().describe("New mount path."),
5304
- 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).`),
5305
- backup_enabled: z21.boolean().optional().describe("Whether to keep backing this volume up. Past backups are kept either way.")
5392
+ service_id: z22.string().describe("Service publicId."),
5393
+ volume_id: z22.string().describe("Volume publicId (e.g. vol_\u2026)."),
5394
+ mount_path: z22.string().startsWith("/").max(500).optional().describe("New mount path."),
5395
+ 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).`),
5396
+ backup_enabled: z22.boolean().optional().describe("Whether to keep backing this volume up. Past backups are kept either way.")
5306
5397
  },
5307
5398
  handler: async (args, ctx) => {
5308
5399
  const teamId = await ctx.resolveTeamId();
@@ -5341,8 +5432,8 @@ defineTool({
5341
5432
  'Example: delete_volume({ service_id: "svc_abc", volume_id: "vol_xyz" }) \u2192 { ok: true }'
5342
5433
  ].join("\n"),
5343
5434
  input: {
5344
- service_id: z21.string().describe("Service publicId."),
5345
- volume_id: z21.string().describe("Volume publicId.")
5435
+ service_id: z22.string().describe("Service publicId."),
5436
+ volume_id: z22.string().describe("Volume publicId.")
5346
5437
  },
5347
5438
  handler: async (args, ctx) => {
5348
5439
  const teamId = await ctx.resolveTeamId();