@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/README.md +2 -2
- package/dist/hoststack-mcp.js +288 -197
- package/dist/hoststack-mcp.js.map +1 -1
- package/dist/index.js +288 -197
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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.
|
|
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((
|
|
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((
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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 ${
|
|
2933
|
-
data: {
|
|
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
|
|
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:
|
|
2962
|
-
description:
|
|
2963
|
-
severity:
|
|
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:
|
|
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:
|
|
2970
|
-
databaseId:
|
|
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
|
|
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:
|
|
3153
|
-
name:
|
|
3154
|
-
webhook_url:
|
|
3155
|
-
events:
|
|
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:
|
|
3194
|
-
name:
|
|
3195
|
-
active:
|
|
3196
|
-
events:
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
3319
|
-
description:
|
|
3320
|
-
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:
|
|
3354
|
-
name:
|
|
3355
|
-
description:
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
3479
|
-
title:
|
|
3480
|
-
body:
|
|
3481
|
-
service_id:
|
|
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:
|
|
3528
|
-
status:
|
|
3529
|
-
title:
|
|
3530
|
-
body:
|
|
3531
|
-
service_id:
|
|
3532
|
-
provider:
|
|
3533
|
-
permission_mode:
|
|
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
|
|
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:
|
|
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:
|
|
3657
|
-
resource_type:
|
|
3658
|
-
resource_id:
|
|
3659
|
-
alias:
|
|
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:
|
|
3696
|
-
link_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
|
|
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,
|
|
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:
|
|
3966
|
-
environment_id:
|
|
3967
|
-
status:
|
|
3968
|
-
type:
|
|
3969
|
-
dev_environment:
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
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:
|
|
4043
|
-
name:
|
|
4044
|
-
type:
|
|
4045
|
-
docker_image:
|
|
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
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
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:
|
|
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:
|
|
4063
|
-
environment_id:
|
|
4064
|
-
auto_deploy:
|
|
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:
|
|
4136
|
-
name:
|
|
4137
|
-
plan:
|
|
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:
|
|
4141
|
-
hoststack_api_key:
|
|
4142
|
-
poststack_api_key:
|
|
4143
|
-
repo_url:
|
|
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:
|
|
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:
|
|
4264
|
-
include_database_clone:
|
|
4265
|
-
name:
|
|
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:
|
|
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:
|
|
4345
|
-
size:
|
|
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):
|
|
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:
|
|
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:
|
|
4499
|
-
github_repo_id:
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
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:
|
|
4507
|
-
|
|
4508
|
-
provider:
|
|
4509
|
-
account_id:
|
|
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/
|
|
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:
|
|
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:
|
|
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:
|
|
4653
|
-
from:
|
|
4654
|
-
to:
|
|
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:
|
|
4699
|
-
name:
|
|
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:
|
|
4752
|
-
install_command:
|
|
4753
|
-
build_command:
|
|
4754
|
-
start_command:
|
|
4755
|
-
branch:
|
|
4756
|
-
root_directory:
|
|
4757
|
-
dockerfile_path:
|
|
4758
|
-
auto_deploy:
|
|
4759
|
-
health_check_path:
|
|
4760
|
-
health_check_enabled:
|
|
4761
|
-
allow_search_indexing:
|
|
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:
|
|
4765
|
-
health_check_timeout:
|
|
4766
|
-
health_check_grace_period_sec:
|
|
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:
|
|
4770
|
-
cpu_shares:
|
|
4771
|
-
disk_size_gb:
|
|
4772
|
-
port:
|
|
4773
|
-
protocol:
|
|
4774
|
-
restart_policy:
|
|
4775
|
-
deploy_strategy:
|
|
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:
|
|
4779
|
-
instance_count:
|
|
4780
|
-
min_instances:
|
|
4781
|
-
max_instances:
|
|
4782
|
-
scale_cpu_threshold:
|
|
4783
|
-
scale_memory_threshold:
|
|
4784
|
-
scale_requests_per_sec_threshold:
|
|
4785
|
-
build_timeout_seconds:
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
4982
|
-
lines:
|
|
4983
|
-
since:
|
|
4984
|
-
until:
|
|
4985
|
-
stream:
|
|
4986
|
-
level:
|
|
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:
|
|
4990
|
-
count_only:
|
|
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:
|
|
5040
|
-
lines_per_service:
|
|
5041
|
-
since:
|
|
5042
|
-
until:
|
|
5043
|
-
stream:
|
|
5044
|
-
level:
|
|
5045
|
-
search:
|
|
5046
|
-
count_only:
|
|
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
|
|
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:
|
|
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:
|
|
5161
|
-
enabled:
|
|
5162
|
-
path:
|
|
5163
|
-
method:
|
|
5164
|
-
expectedStatus:
|
|
5165
|
-
timeoutMs:
|
|
5166
|
-
intervalSeconds:
|
|
5167
|
-
failureThreshold:
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
5255
|
-
name:
|
|
5256
|
-
mount_path:
|
|
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:
|
|
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:
|
|
5302
|
-
volume_id:
|
|
5303
|
-
mount_path:
|
|
5304
|
-
size_gb:
|
|
5305
|
-
backup_enabled:
|
|
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:
|
|
5345
|
-
volume_id:
|
|
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();
|