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