@hoststack.dev/mcp 0.24.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/hoststack-mcp.js +270 -203
- package/dist/hoststack-mcp.js.map +1 -1
- package/dist/index.js +270 -203
- 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,8 @@ 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.",
|
|
4024
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.",
|
|
4025
4083
|
' - branch (optional): git branch (default "main").',
|
|
4026
4084
|
" - install_command / build_command / start_command (optional): build/run shell commands. start_command is required for web/private services without a docker_image.",
|
|
@@ -4036,36 +4094,42 @@ defineTool({
|
|
|
4036
4094
|
"",
|
|
4037
4095
|
"Returns: { service: Service, deployId: number | null }.",
|
|
4038
4096
|
"",
|
|
4039
|
-
'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.",
|
|
4040
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.'
|
|
4041
4100
|
].join("\n"),
|
|
4042
4101
|
input: {
|
|
4043
|
-
project_id:
|
|
4044
|
-
name:
|
|
4045
|
-
type:
|
|
4046
|
-
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(
|
|
4047
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."
|
|
4048
4107
|
),
|
|
4049
|
-
|
|
4050
|
-
|
|
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(
|
|
4051
4115
|
"Public https git URL. No embedded credentials \u2014 private repos need a provider connection. Mutually exclusive with github_repo_id and docker_image."
|
|
4052
4116
|
),
|
|
4053
|
-
branch:
|
|
4054
|
-
install_command:
|
|
4055
|
-
build_command:
|
|
4056
|
-
start_command:
|
|
4057
|
-
cron_schedule:
|
|
4058
|
-
publish_path:
|
|
4059
|
-
runtime:
|
|
4060
|
-
port:
|
|
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(
|
|
4061
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."
|
|
4062
4126
|
),
|
|
4063
|
-
template_id:
|
|
4127
|
+
template_id: z20.string().max(64).optional().describe(
|
|
4064
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."
|
|
4065
4129
|
),
|
|
4066
|
-
plan:
|
|
4067
|
-
environment_id:
|
|
4068
|
-
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)."),
|
|
4069
4133
|
machine: machineInput
|
|
4070
4134
|
},
|
|
4071
4135
|
handler: async (args, ctx) => {
|
|
@@ -4080,6 +4144,7 @@ defineTool({
|
|
|
4080
4144
|
projectId
|
|
4081
4145
|
};
|
|
4082
4146
|
if (args.docker_image !== void 0) input.dockerImage = args.docker_image;
|
|
4147
|
+
if (args.github_repo !== void 0) input.githubRepo = args.github_repo;
|
|
4083
4148
|
if (args.github_repo_id !== void 0) input.githubRepoId = args.github_repo_id;
|
|
4084
4149
|
if (args.git_repo_url !== void 0) input.gitRepoUrl = args.git_repo_url;
|
|
4085
4150
|
if (args.branch !== void 0) input.branch = args.branch;
|
|
@@ -4137,18 +4202,18 @@ defineTool({
|
|
|
4137
4202
|
'Example: create_dev_environment({ project_id: "prj_abc", name: "scratch", hoststack_api_key: "hs_live_\u2026" })'
|
|
4138
4203
|
].join("\n"),
|
|
4139
4204
|
input: {
|
|
4140
|
-
project_id:
|
|
4141
|
-
name:
|
|
4142
|
-
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(
|
|
4143
4208
|
'Box size (default "standard" \u2014 2 GB, the OOM-safe floor; a smaller plan is clamped up to "standard").'
|
|
4144
4209
|
),
|
|
4145
|
-
disk_gb:
|
|
4146
|
-
hoststack_api_key:
|
|
4147
|
-
poststack_api_key:
|
|
4148
|
-
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(
|
|
4149
4214
|
"Clone this git URL into /workspace on first boot (HTTPS, or SSH once a key is set)."
|
|
4150
4215
|
),
|
|
4151
|
-
branch:
|
|
4216
|
+
branch: z20.string().max(200).optional().describe("Branch to clone (with repo_url)."),
|
|
4152
4217
|
machine: machineInput
|
|
4153
4218
|
},
|
|
4154
4219
|
handler: async (args, ctx) => {
|
|
@@ -4265,9 +4330,9 @@ defineTool({
|
|
|
4265
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.'
|
|
4266
4331
|
].join("\n"),
|
|
4267
4332
|
input: {
|
|
4268
|
-
service_id:
|
|
4269
|
-
include_database_clone:
|
|
4270
|
-
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").')
|
|
4271
4336
|
},
|
|
4272
4337
|
handler: async (args, ctx) => {
|
|
4273
4338
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -4308,7 +4373,7 @@ defineTool({
|
|
|
4308
4373
|
'Example: delete_dev_environment({ service_id: "svc_api_dev" }) \u2192 removes the dev box, its cloned database, and the /workspace volume.'
|
|
4309
4374
|
].join("\n"),
|
|
4310
4375
|
input: {
|
|
4311
|
-
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.")
|
|
4312
4377
|
},
|
|
4313
4378
|
handler: async (args, ctx) => {
|
|
4314
4379
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -4346,8 +4411,8 @@ defineTool({
|
|
|
4346
4411
|
'Example: resize_dev_environment({ service_id: "svc_skyskraber_dev", size: "large" }) \u2192 bumps the box to the large tier, applied live.'
|
|
4347
4412
|
].join("\n"),
|
|
4348
4413
|
input: {
|
|
4349
|
-
service_id:
|
|
4350
|
-
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(
|
|
4351
4416
|
'Target size tier (service catalog size, e.g. "standard", "large", "xlarge").'
|
|
4352
4417
|
)
|
|
4353
4418
|
},
|
|
@@ -4484,7 +4549,7 @@ defineTool({
|
|
|
4484
4549
|
"Inputs:",
|
|
4485
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.',
|
|
4486
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).',
|
|
4487
|
-
" - 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.",
|
|
4488
4553
|
" - clone_url (for url): an http(s) git clone URL.",
|
|
4489
4554
|
" - branch (optional): branch to clone.",
|
|
4490
4555
|
' - databases (optional): companion services to attach \u2014 any of "postgres", "redis", "meilisearch".',
|
|
@@ -4494,24 +4559,26 @@ defineTool({
|
|
|
4494
4559
|
"",
|
|
4495
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.",
|
|
4496
4561
|
"",
|
|
4497
|
-
'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"] })'
|
|
4498
4563
|
].join("\n"),
|
|
4499
4564
|
input: {
|
|
4500
|
-
name:
|
|
4565
|
+
name: z20.string().min(1).max(100).optional().describe(
|
|
4501
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.'
|
|
4502
4567
|
),
|
|
4503
|
-
source_kind:
|
|
4504
|
-
github_repo_id:
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
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(
|
|
4509
4576
|
'Box size (default "standard" \u2014 2 GB; a smaller plan is floored to "standard").'
|
|
4510
4577
|
),
|
|
4511
|
-
agent_accounts:
|
|
4512
|
-
|
|
4513
|
-
provider:
|
|
4514
|
-
account_id:
|
|
4578
|
+
agent_accounts: z20.array(
|
|
4579
|
+
z20.object({
|
|
4580
|
+
provider: z20.enum(["claude", "codex", "opencode"]),
|
|
4581
|
+
account_id: z20.number().int().positive()
|
|
4515
4582
|
})
|
|
4516
4583
|
).max(3).optional().describe(
|
|
4517
4584
|
"Bind saved agent logins by account id per provider. Omit to inherit the box owner's default logins automatically."
|
|
@@ -4584,12 +4651,12 @@ defineTool({
|
|
|
4584
4651
|
"Inputs:",
|
|
4585
4652
|
' - service_id: publicId of the service (e.g. "svc_abc123").',
|
|
4586
4653
|
"",
|
|
4587
|
-
'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.',
|
|
4588
4655
|
"",
|
|
4589
4656
|
'Example: get_service({ service_id: "svc_abc" }) \u2192 { service: { type: "web", status: "running", \u2026 }, config: { healthCheckGracePeriodSec: 120, \u2026 } }'
|
|
4590
4657
|
].join("\n"),
|
|
4591
4658
|
input: {
|
|
4592
|
-
service_id:
|
|
4659
|
+
service_id: z20.string().describe("Service publicId (e.g. svc_abc123).")
|
|
4593
4660
|
},
|
|
4594
4661
|
handler: async (args, ctx) => {
|
|
4595
4662
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -4621,7 +4688,7 @@ defineTool({
|
|
|
4621
4688
|
'Example: get_service_metrics({ service_id: "svc_abc" }) \u2192 { metrics: { cpu: 0.42, memory: 0.71, \u2026 } }'
|
|
4622
4689
|
].join("\n"),
|
|
4623
4690
|
input: {
|
|
4624
|
-
service_id:
|
|
4691
|
+
service_id: z20.string().describe("Service publicId.")
|
|
4625
4692
|
},
|
|
4626
4693
|
handler: async (args, ctx) => {
|
|
4627
4694
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -4654,9 +4721,9 @@ defineTool({
|
|
|
4654
4721
|
'Example: get_service_metrics_history({ service_id: "svc_abc", from: "-1h" }) \u2192 ~120 points for the last hour.'
|
|
4655
4722
|
].join("\n"),
|
|
4656
4723
|
input: {
|
|
4657
|
-
service_id:
|
|
4658
|
-
from:
|
|
4659
|
-
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.")
|
|
4660
4727
|
},
|
|
4661
4728
|
handler: async (args, ctx) => {
|
|
4662
4729
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -4700,8 +4767,8 @@ defineTool({
|
|
|
4700
4767
|
'Example: update_service({ service_id: "svc_abc", name: "api-prod" }) \u2192 { service: { name: "api-prod", \u2026 } }'
|
|
4701
4768
|
].join("\n"),
|
|
4702
4769
|
input: {
|
|
4703
|
-
service_id:
|
|
4704
|
-
name:
|
|
4770
|
+
service_id: z20.string().describe("Service publicId."),
|
|
4771
|
+
name: z20.string().min(1).max(60).describe("New service name (1\u201360 chars).")
|
|
4705
4772
|
},
|
|
4706
4773
|
handler: async (args, ctx) => {
|
|
4707
4774
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -4758,54 +4825,54 @@ defineTool({
|
|
|
4758
4825
|
'Example: update_service_config({ service_id: "svc_abc", health_check_grace_period_sec: 180 }) \u2192 { config: { healthCheckGracePeriodSec: 180, \u2026 } }'
|
|
4759
4826
|
].join("\n"),
|
|
4760
4827
|
input: {
|
|
4761
|
-
service_id:
|
|
4762
|
-
install_command:
|
|
4763
|
-
build_command:
|
|
4764
|
-
start_command:
|
|
4765
|
-
branch:
|
|
4766
|
-
root_directory:
|
|
4767
|
-
dockerfile_path:
|
|
4768
|
-
auto_deploy:
|
|
4769
|
-
health_check_path:
|
|
4770
|
-
health_check_enabled:
|
|
4771
|
-
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(
|
|
4772
4839
|
"Let search engines index the free *.hoststack.dev platform URL (off by default). Custom domains are always indexable. Applies on the next deploy."
|
|
4773
4840
|
),
|
|
4774
|
-
health_check_interval:
|
|
4775
|
-
health_check_timeout:
|
|
4776
|
-
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(
|
|
4777
4844
|
"Startup grace period in seconds (1\u20131800). Raise this if the app needs more time to boot before health checks start counting failures."
|
|
4778
4845
|
),
|
|
4779
|
-
memory_mb:
|
|
4780
|
-
cpu_shares:
|
|
4781
|
-
disk_size_gb:
|
|
4782
|
-
port:
|
|
4783
|
-
protocol:
|
|
4784
|
-
restart_policy:
|
|
4785
|
-
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(
|
|
4786
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.'
|
|
4787
4854
|
),
|
|
4788
|
-
pre_deploy_command:
|
|
4789
|
-
instance_count:
|
|
4790
|
-
min_instances:
|
|
4791
|
-
max_instances:
|
|
4792
|
-
scale_cpu_threshold:
|
|
4793
|
-
scale_memory_threshold:
|
|
4794
|
-
scale_requests_per_sec_threshold:
|
|
4795
|
-
build_timeout_seconds:
|
|
4796
|
-
plan:
|
|
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(
|
|
4797
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."
|
|
4798
4865
|
),
|
|
4799
|
-
docker_image:
|
|
4866
|
+
docker_image: z20.string().max(500).nullable().optional().describe(
|
|
4800
4867
|
"Repoint an image-backed service. Applies on the next deploy. null clears it."
|
|
4801
4868
|
),
|
|
4802
|
-
cron_schedule:
|
|
4803
|
-
publish_path:
|
|
4804
|
-
runtime:
|
|
4805
|
-
log_filter_rules:
|
|
4806
|
-
|
|
4807
|
-
pattern:
|
|
4808
|
-
action:
|
|
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"])
|
|
4809
4876
|
})
|
|
4810
4877
|
).max(50).optional().describe(
|
|
4811
4878
|
"Runtime-log filter rules. Empty array [] clears all rules. Each pattern is case-insensitive substring match against the message."
|
|
@@ -4913,7 +4980,7 @@ defineTool({
|
|
|
4913
4980
|
'Example: suspend_service({ service_id: "svc_dev" }) \u2192 { ok: true }'
|
|
4914
4981
|
].join("\n"),
|
|
4915
4982
|
input: {
|
|
4916
|
-
service_id:
|
|
4983
|
+
service_id: z20.string().describe("Service publicId.")
|
|
4917
4984
|
},
|
|
4918
4985
|
handler: async (args, ctx) => {
|
|
4919
4986
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -4937,7 +5004,7 @@ defineTool({
|
|
|
4937
5004
|
'Example: resume_service({ service_id: "svc_dev" }) \u2192 { ok: true }'
|
|
4938
5005
|
].join("\n"),
|
|
4939
5006
|
input: {
|
|
4940
|
-
service_id:
|
|
5007
|
+
service_id: z20.string().describe("Service publicId.")
|
|
4941
5008
|
},
|
|
4942
5009
|
handler: async (args, ctx) => {
|
|
4943
5010
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -4963,7 +5030,7 @@ defineTool({
|
|
|
4963
5030
|
'Example: delete_service({ service_id: "svc_abandoned" }) \u2192 { ok: true }'
|
|
4964
5031
|
].join("\n"),
|
|
4965
5032
|
input: {
|
|
4966
|
-
service_id:
|
|
5033
|
+
service_id: z20.string().describe("Service publicId.")
|
|
4967
5034
|
},
|
|
4968
5035
|
handler: async (args, ctx) => {
|
|
4969
5036
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -5002,16 +5069,16 @@ defineTool({
|
|
|
5002
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 }'
|
|
5003
5070
|
].join("\n"),
|
|
5004
5071
|
input: {
|
|
5005
|
-
service_id:
|
|
5006
|
-
lines:
|
|
5007
|
-
since:
|
|
5008
|
-
until:
|
|
5009
|
-
stream:
|
|
5010
|
-
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(
|
|
5011
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)."
|
|
5012
5079
|
),
|
|
5013
|
-
search:
|
|
5014
|
-
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.")
|
|
5015
5082
|
},
|
|
5016
5083
|
handler: async (args, ctx) => {
|
|
5017
5084
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -5060,14 +5127,14 @@ defineTool({
|
|
|
5060
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 } } }.'
|
|
5061
5128
|
].join("\n"),
|
|
5062
5129
|
input: {
|
|
5063
|
-
service_ids:
|
|
5064
|
-
lines_per_service:
|
|
5065
|
-
since:
|
|
5066
|
-
until:
|
|
5067
|
-
stream:
|
|
5068
|
-
level:
|
|
5069
|
-
search:
|
|
5070
|
-
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.")
|
|
5071
5138
|
},
|
|
5072
5139
|
handler: async (args, ctx) => {
|
|
5073
5140
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -5113,7 +5180,7 @@ defineTool({
|
|
|
5113
5180
|
});
|
|
5114
5181
|
|
|
5115
5182
|
// src/tools/uptime.ts
|
|
5116
|
-
import { z as
|
|
5183
|
+
import { z as z21 } from "zod";
|
|
5117
5184
|
var STATUS_MEANING = [
|
|
5118
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."
|
|
5119
5186
|
].join("\n");
|
|
@@ -5135,7 +5202,7 @@ defineTool({
|
|
|
5135
5202
|
"",
|
|
5136
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' } }."
|
|
5137
5204
|
].join("\n"),
|
|
5138
|
-
input: { serviceId:
|
|
5205
|
+
input: { serviceId: z21.number().int().positive() },
|
|
5139
5206
|
handler: async (args, ctx) => {
|
|
5140
5207
|
const teamId = await ctx.resolveTeamId();
|
|
5141
5208
|
const response = await ctx.api.get(
|
|
@@ -5181,14 +5248,14 @@ defineTool({
|
|
|
5181
5248
|
"Example: set_uptime_check({ serviceId: 48, path: '/healthz', intervalSeconds: 60, failureThreshold: 3 }) \u2192 { check: { status: 'unknown', \u2026 } }."
|
|
5182
5249
|
].join("\n"),
|
|
5183
5250
|
input: {
|
|
5184
|
-
serviceId:
|
|
5185
|
-
enabled:
|
|
5186
|
-
path:
|
|
5187
|
-
method:
|
|
5188
|
-
expectedStatus:
|
|
5189
|
-
timeoutMs:
|
|
5190
|
-
intervalSeconds:
|
|
5191
|
-
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()
|
|
5192
5259
|
},
|
|
5193
5260
|
handler: async (args, ctx) => {
|
|
5194
5261
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -5219,7 +5286,7 @@ defineTool({
|
|
|
5219
5286
|
"",
|
|
5220
5287
|
"Example: delete_uptime_check({ serviceId: 48 }) \u2192 { success: true }."
|
|
5221
5288
|
].join("\n"),
|
|
5222
|
-
input: { serviceId:
|
|
5289
|
+
input: { serviceId: z21.number().int().positive() },
|
|
5223
5290
|
handler: async (args, ctx) => {
|
|
5224
5291
|
const teamId = await ctx.resolveTeamId();
|
|
5225
5292
|
await ctx.api.delete(`/api/services/${teamId}/${args.serviceId}/uptime-check`);
|
|
@@ -5228,7 +5295,7 @@ defineTool({
|
|
|
5228
5295
|
});
|
|
5229
5296
|
|
|
5230
5297
|
// src/tools/volumes.ts
|
|
5231
|
-
import { z as
|
|
5298
|
+
import { z as z22 } from "zod";
|
|
5232
5299
|
var MIN_VOLUME_SIZE_GB = 10;
|
|
5233
5300
|
defineTool({
|
|
5234
5301
|
name: "list_volumes",
|
|
@@ -5246,7 +5313,7 @@ defineTool({
|
|
|
5246
5313
|
'Example: list_volumes({ service_id: "svc_abc" }) \u2192 { items: [{ name: "data", mountPath: "/var/data", sizeGb: 10, status: "active" }] }'
|
|
5247
5314
|
].join("\n"),
|
|
5248
5315
|
input: {
|
|
5249
|
-
service_id:
|
|
5316
|
+
service_id: z22.string().describe("Service publicId (e.g. svc_abc123).")
|
|
5250
5317
|
},
|
|
5251
5318
|
handler: async (args, ctx) => {
|
|
5252
5319
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -5275,15 +5342,15 @@ defineTool({
|
|
|
5275
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" } }'
|
|
5276
5343
|
].join("\n"),
|
|
5277
5344
|
input: {
|
|
5278
|
-
service_id:
|
|
5279
|
-
name:
|
|
5280
|
-
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)."),
|
|
5281
5348
|
// 10 GB is the real floor: the block-storage backend rejects anything
|
|
5282
5349
|
// smaller. Advertising 1 GB here (and defaulting to it) meant taking the
|
|
5283
5350
|
// defaults produced a volume that provisioned with `Hetzner API error:
|
|
5284
5351
|
// 422` on the NEXT deploy, with nothing tying the failure back to the
|
|
5285
5352
|
// size. Reject it at the call instead.
|
|
5286
|
-
size_gb:
|
|
5353
|
+
size_gb: z22.number().int().min(MIN_VOLUME_SIZE_GB).max(100).optional().describe(
|
|
5287
5354
|
`Disk size in GB (minimum ${MIN_VOLUME_SIZE_GB}, default ${MIN_VOLUME_SIZE_GB}, max 100 via MCP).`
|
|
5288
5355
|
)
|
|
5289
5356
|
},
|
|
@@ -5322,11 +5389,11 @@ defineTool({
|
|
|
5322
5389
|
'Example: update_volume({ service_id: "svc_abc", volume_id: "vol_xyz", size_gb: 20 }) \u2192 { volume: { sizeGb: 20, \u2026 } }'
|
|
5323
5390
|
].join("\n"),
|
|
5324
5391
|
input: {
|
|
5325
|
-
service_id:
|
|
5326
|
-
volume_id:
|
|
5327
|
-
mount_path:
|
|
5328
|
-
size_gb:
|
|
5329
|
-
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.")
|
|
5330
5397
|
},
|
|
5331
5398
|
handler: async (args, ctx) => {
|
|
5332
5399
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -5365,8 +5432,8 @@ defineTool({
|
|
|
5365
5432
|
'Example: delete_volume({ service_id: "svc_abc", volume_id: "vol_xyz" }) \u2192 { ok: true }'
|
|
5366
5433
|
].join("\n"),
|
|
5367
5434
|
input: {
|
|
5368
|
-
service_id:
|
|
5369
|
-
volume_id:
|
|
5435
|
+
service_id: z22.string().describe("Service publicId."),
|
|
5436
|
+
volume_id: z22.string().describe("Volume publicId.")
|
|
5370
5437
|
},
|
|
5371
5438
|
handler: async (args, ctx) => {
|
|
5372
5439
|
const teamId = await ctx.resolveTeamId();
|