@hoststack.dev/mcp 0.2.1 → 0.4.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 -1
- package/dist/hoststack-mcp.js +335 -31
- package/dist/hoststack-mcp.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +335 -31
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -79,7 +79,7 @@ If `HOSTSTACK_API_KEY` is set in your shell, it gets baked into the snippet; oth
|
|
|
79
79
|
|
|
80
80
|
## Tool inventory
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
35 tools, grouped by resource:
|
|
83
83
|
|
|
84
84
|
| Category | Read | Write |
|
|
85
85
|
|---|---|---|
|
|
@@ -89,6 +89,7 @@ If `HOSTSTACK_API_KEY` is set in your shell, it gets baked into the snippet; oth
|
|
|
89
89
|
| **databases** | `list_databases`, `get_database` | — (use the dashboard for create/delete/credentials) |
|
|
90
90
|
| **domains** | `list_domains` | `add_domain`, `verify_domain`, `remove_domain` |
|
|
91
91
|
| **env-vars** | `list_env_vars` | `set_env_var`, `delete_env_var`, `bulk_set_env_vars` |
|
|
92
|
+
| **environments** | `list_environments` | `create_environment`, `delete_environment`, `promote_deploy` |
|
|
92
93
|
| **cron** | `list_cron_executions`, `get_cron_execution` | — |
|
|
93
94
|
| **activity-log** | `list_activity_log` | — |
|
|
94
95
|
| **meta** | `get_me` | — |
|
package/dist/hoststack-mcp.js
CHANGED
|
@@ -320,6 +320,9 @@ function shapeUser(value) {
|
|
|
320
320
|
function shapeTeam(value) {
|
|
321
321
|
return isObject(value) ? dropNullsAndInternals(value) : {};
|
|
322
322
|
}
|
|
323
|
+
function shapeVolume(value) {
|
|
324
|
+
return isObject(value) ? dropNullsAndInternals(value) : {};
|
|
325
|
+
}
|
|
323
326
|
|
|
324
327
|
// src/resources/hoststack-resources.ts
|
|
325
328
|
defineResource({
|
|
@@ -989,6 +992,146 @@ defineTool({
|
|
|
989
992
|
}
|
|
990
993
|
});
|
|
991
994
|
|
|
995
|
+
// src/tools/environments.ts
|
|
996
|
+
import { z as z8 } from "zod";
|
|
997
|
+
defineTool({
|
|
998
|
+
name: "list_environments",
|
|
999
|
+
category: "environments",
|
|
1000
|
+
description: [
|
|
1001
|
+
"List every environment for a project (production / staging / development / preview).",
|
|
1002
|
+
"",
|
|
1003
|
+
"When to use: the user wants to see what envs exist before creating a service in one or promoting a deploy. Every project has at least Production.",
|
|
1004
|
+
"",
|
|
1005
|
+
"Inputs:",
|
|
1006
|
+
' - project_id: project publicId (e.g. "prj_abc123").',
|
|
1007
|
+
"",
|
|
1008
|
+
"Returns: { items: Environment[] } \u2014 id, publicId, name, type, isDefault, isProtected.",
|
|
1009
|
+
"",
|
|
1010
|
+
'Example: list_environments({ project_id: "prj_abc" }) \u2192 { items: [{ name: "Production", type: "production", isDefault: true }, { name: "Staging", type: "staging" }] }'
|
|
1011
|
+
].join("\n"),
|
|
1012
|
+
input: {
|
|
1013
|
+
project_id: z8.string().describe("Project publicId.")
|
|
1014
|
+
},
|
|
1015
|
+
handler: async (args2, ctx) => {
|
|
1016
|
+
const teamId = await ctx.resolveTeamId();
|
|
1017
|
+
const response = await ctx.hoststack.environments.list(teamId, args2.project_id);
|
|
1018
|
+
const data = shapeList(response, "environments", shape);
|
|
1019
|
+
const summary = data.items.length === 0 ? "No environments yet \u2014 every project should have Production by default." : `Found ${data.items.length} environment${data.items.length === 1 ? "" : "s"}.`;
|
|
1020
|
+
return respond({ summary, data });
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
1023
|
+
defineTool({
|
|
1024
|
+
name: "create_environment",
|
|
1025
|
+
category: "environments",
|
|
1026
|
+
description: [
|
|
1027
|
+
"Create a new environment in a project (e.g. Staging, QA, Preview).",
|
|
1028
|
+
"",
|
|
1029
|
+
"When to use: the user wants to add an env so they can run a sibling service alongside production for testing or staging before release.",
|
|
1030
|
+
"",
|
|
1031
|
+
"Inputs:",
|
|
1032
|
+
" - project_id: project publicId.",
|
|
1033
|
+
" - name: human-readable name (1\u201364 chars). Shown in the env switcher.",
|
|
1034
|
+
' - type: "production" | "staging" | "development" | "preview". Determines the hostname suffix on services in this env (production stays clean; others get -staging / -dev / -preview).',
|
|
1035
|
+
" - is_protected (optional): require admin role for destructive actions in this env. Default false.",
|
|
1036
|
+
"",
|
|
1037
|
+
"Returns: { environment: Environment }.",
|
|
1038
|
+
"",
|
|
1039
|
+
'Example: create_environment({ project_id: "prj_abc", name: "Staging", type: "staging" }) \u2192 { environment: { id: 7, publicId: "environment_\u2026", name: "Staging", type: "staging" } }'
|
|
1040
|
+
].join("\n"),
|
|
1041
|
+
input: {
|
|
1042
|
+
project_id: z8.string().describe("Project publicId."),
|
|
1043
|
+
name: z8.string().min(1).max(64).describe("Environment name (1\u201364 chars)."),
|
|
1044
|
+
type: z8.enum(["production", "staging", "development", "preview"]).describe("Environment type \u2014 drives the hostname suffix."),
|
|
1045
|
+
is_protected: z8.boolean().optional().describe("Require admin role for destructive actions. Default false.")
|
|
1046
|
+
},
|
|
1047
|
+
handler: async (args2, ctx) => {
|
|
1048
|
+
const teamId = await ctx.resolveTeamId();
|
|
1049
|
+
const input = {
|
|
1050
|
+
name: args2.name,
|
|
1051
|
+
type: args2.type
|
|
1052
|
+
};
|
|
1053
|
+
if (args2.is_protected !== void 0) input.isProtected = args2.is_protected;
|
|
1054
|
+
const response = await ctx.hoststack.environments.create(teamId, args2.project_id, input);
|
|
1055
|
+
const data = { environment: shape(response.environment) };
|
|
1056
|
+
return respond({
|
|
1057
|
+
summary: `Created environment "${args2.name}" (${args2.type}) in project ${args2.project_id}.`,
|
|
1058
|
+
data
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
});
|
|
1062
|
+
defineTool({
|
|
1063
|
+
name: "delete_environment",
|
|
1064
|
+
category: "environments",
|
|
1065
|
+
description: [
|
|
1066
|
+
"Delete an environment from a project.",
|
|
1067
|
+
"",
|
|
1068
|
+
"When to use: an env is no longer used (e.g. tearing down a feature branch staging). Blocked when the env still has live services or databases attached \u2014 destroy or move them first. The default env (usually Production) cannot be deleted.",
|
|
1069
|
+
"",
|
|
1070
|
+
"Inputs:",
|
|
1071
|
+
" - project_id: project publicId.",
|
|
1072
|
+
" - environment_id: environment publicId or numeric id.",
|
|
1073
|
+
"",
|
|
1074
|
+
"Returns: { success: true } on success.",
|
|
1075
|
+
"",
|
|
1076
|
+
'Example: delete_environment({ project_id: "prj_abc", environment_id: "environment_xyz" }) \u2192 { success: true }'
|
|
1077
|
+
].join("\n"),
|
|
1078
|
+
input: {
|
|
1079
|
+
project_id: z8.string().describe("Project publicId."),
|
|
1080
|
+
environment_id: z8.union([z8.string(), z8.number()]).describe("Environment publicId or numeric id.")
|
|
1081
|
+
},
|
|
1082
|
+
handler: async (args2, ctx) => {
|
|
1083
|
+
const teamId = await ctx.resolveTeamId();
|
|
1084
|
+
await ctx.hoststack.environments.delete(teamId, args2.project_id, args2.environment_id);
|
|
1085
|
+
return respond({
|
|
1086
|
+
summary: `Deleted environment ${String(args2.environment_id)} from project ${args2.project_id}.`,
|
|
1087
|
+
data: { success: true }
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
});
|
|
1091
|
+
defineTool({
|
|
1092
|
+
name: "promote_deploy",
|
|
1093
|
+
category: "environments",
|
|
1094
|
+
description: [
|
|
1095
|
+
"Promote a built deploy from one environment to another (build-once, run-many).",
|
|
1096
|
+
"",
|
|
1097
|
+
"When to use: the user has tested a deploy in staging and wants to ship that exact image to production without rebuilding. Atomic and image-based \u2014 same docker image runs on the sibling service in the target env.",
|
|
1098
|
+
"",
|
|
1099
|
+
"Inputs:",
|
|
1100
|
+
" - service_id: source service publicId (the one that owns the deploy).",
|
|
1101
|
+
" - deploy_id: source deploy publicId. Must have `dockerImageId` set (i.e. a successful build).",
|
|
1102
|
+
" - target_environment_id: env publicId or numeric id to promote into. Must be in the same project. Cannot be the source service's own env.",
|
|
1103
|
+
"",
|
|
1104
|
+
"Behaviour: if a sibling service with the same name already exists in the target env, the new deploy lands on it. Otherwise the API auto-clones the source service's build/runtime config into the target env first. Env-specific config (env vars, secret files, volumes, IP allowlists, custom domains) is NOT copied \u2014 those are per-env by design.",
|
|
1105
|
+
"",
|
|
1106
|
+
"Returns: { deploy: Deploy } \u2014 the new deploy on the target service.",
|
|
1107
|
+
"",
|
|
1108
|
+
'Example: promote_deploy({ service_id: "svc_staging", deploy_id: "dpl_built", target_environment_id: "environment_prod" }) \u2192 { deploy: { id: 99, status: "pending", trigger: "rollback", dockerImageId: "sha256:\u2026" } }'
|
|
1109
|
+
].join("\n"),
|
|
1110
|
+
input: {
|
|
1111
|
+
service_id: z8.string().describe("Source service publicId."),
|
|
1112
|
+
deploy_id: z8.string().describe("Source deploy publicId (must have a built image)."),
|
|
1113
|
+
target_environment_id: z8.union([z8.string(), z8.number()]).describe("Target environment publicId or numeric id.")
|
|
1114
|
+
},
|
|
1115
|
+
handler: async (args2, ctx) => {
|
|
1116
|
+
const teamId = await ctx.resolveTeamId();
|
|
1117
|
+
const targetEnvId = await ctx.hoststack.resolveId(args2.target_environment_id, {
|
|
1118
|
+
kind: "environment",
|
|
1119
|
+
teamId: await ctx.hoststack.resolveId(teamId, { kind: "team" })
|
|
1120
|
+
});
|
|
1121
|
+
const response = await ctx.hoststack.deploys.promote(
|
|
1122
|
+
teamId,
|
|
1123
|
+
args2.service_id,
|
|
1124
|
+
args2.deploy_id,
|
|
1125
|
+
targetEnvId
|
|
1126
|
+
);
|
|
1127
|
+
const data = { deploy: shape(response.deploy) };
|
|
1128
|
+
return respond({
|
|
1129
|
+
summary: `Promoted deploy ${args2.deploy_id} from service ${args2.service_id} to env ${String(args2.target_environment_id)}.`,
|
|
1130
|
+
data
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
});
|
|
1134
|
+
|
|
992
1135
|
// src/tools/meta.ts
|
|
993
1136
|
defineTool({
|
|
994
1137
|
name: "get_me",
|
|
@@ -1016,7 +1159,7 @@ defineTool({
|
|
|
1016
1159
|
});
|
|
1017
1160
|
|
|
1018
1161
|
// src/tools/projects.ts
|
|
1019
|
-
import { z as
|
|
1162
|
+
import { z as z9 } from "zod";
|
|
1020
1163
|
defineTool({
|
|
1021
1164
|
name: "list_projects",
|
|
1022
1165
|
category: "projects",
|
|
@@ -1056,9 +1199,9 @@ defineTool({
|
|
|
1056
1199
|
'Example: create_project({ name: "billing-api", description: "Stripe webhooks", region: "fsn1" }) \u2192 { project: { id: 12, publicId: "prj_\u2026", \u2026 } }'
|
|
1057
1200
|
].join("\n"),
|
|
1058
1201
|
input: {
|
|
1059
|
-
name:
|
|
1060
|
-
description:
|
|
1061
|
-
region:
|
|
1202
|
+
name: z9.string().min(1).max(60).describe("Project name (1\u201360 chars)."),
|
|
1203
|
+
description: z9.string().max(500).optional().describe("Short description (\u2264500 chars)."),
|
|
1204
|
+
region: z9.enum(["fsn1", "nbg1", "hel1"]).optional().describe("Hetzner region: fsn1 | nbg1 | hel1.")
|
|
1062
1205
|
},
|
|
1063
1206
|
handler: async (args2, ctx) => {
|
|
1064
1207
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -1089,9 +1232,9 @@ defineTool({
|
|
|
1089
1232
|
'Example: update_project({ project_id: "prj_abc", name: "billing-prod" }) \u2192 { project: { name: "billing-prod", \u2026 } }'
|
|
1090
1233
|
].join("\n"),
|
|
1091
1234
|
input: {
|
|
1092
|
-
project_id:
|
|
1093
|
-
name:
|
|
1094
|
-
description:
|
|
1235
|
+
project_id: z9.string().describe("Project publicId."),
|
|
1236
|
+
name: z9.string().min(1).max(60).optional().describe("New name (1\u201360 chars)."),
|
|
1237
|
+
description: z9.string().max(500).optional().describe("New description (\u2264500 chars).")
|
|
1095
1238
|
},
|
|
1096
1239
|
handler: async (args2, ctx) => {
|
|
1097
1240
|
if (args2.name === void 0 && args2.description === void 0) {
|
|
@@ -1125,7 +1268,7 @@ defineTool({
|
|
|
1125
1268
|
'Example: get_project({ project_id: "prj_abc" }) \u2192 { project: { id: 12, name: "billing", \u2026 } }'
|
|
1126
1269
|
].join("\n"),
|
|
1127
1270
|
input: {
|
|
1128
|
-
project_id:
|
|
1271
|
+
project_id: z9.string().describe("Project publicId (e.g. prj_abc123).")
|
|
1129
1272
|
},
|
|
1130
1273
|
handler: async (args2, ctx) => {
|
|
1131
1274
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -1137,7 +1280,7 @@ defineTool({
|
|
|
1137
1280
|
});
|
|
1138
1281
|
|
|
1139
1282
|
// src/tools/services.ts
|
|
1140
|
-
import { z as
|
|
1283
|
+
import { z as z10 } from "zod";
|
|
1141
1284
|
defineTool({
|
|
1142
1285
|
name: "list_services",
|
|
1143
1286
|
category: "services",
|
|
@@ -1175,7 +1318,7 @@ defineTool({
|
|
|
1175
1318
|
'Example: get_service({ service_id: "svc_abc" }) \u2192 { service: { type: "web", status: "running", \u2026 } }'
|
|
1176
1319
|
].join("\n"),
|
|
1177
1320
|
input: {
|
|
1178
|
-
service_id:
|
|
1321
|
+
service_id: z10.string().describe("Service publicId (e.g. svc_abc123).")
|
|
1179
1322
|
},
|
|
1180
1323
|
handler: async (args2, ctx) => {
|
|
1181
1324
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -1201,7 +1344,7 @@ defineTool({
|
|
|
1201
1344
|
'Example: get_service_metrics({ service_id: "svc_abc" }) \u2192 { metrics: { cpu: 0.42, memory: 0.71, \u2026 } }'
|
|
1202
1345
|
].join("\n"),
|
|
1203
1346
|
input: {
|
|
1204
|
-
service_id:
|
|
1347
|
+
service_id: z10.string().describe("Service publicId.")
|
|
1205
1348
|
},
|
|
1206
1349
|
handler: async (args2, ctx) => {
|
|
1207
1350
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -1227,8 +1370,8 @@ defineTool({
|
|
|
1227
1370
|
'Example: update_service({ service_id: "svc_abc", name: "api-prod" }) \u2192 { service: { name: "api-prod", \u2026 } }'
|
|
1228
1371
|
].join("\n"),
|
|
1229
1372
|
input: {
|
|
1230
|
-
service_id:
|
|
1231
|
-
name:
|
|
1373
|
+
service_id: z10.string().describe("Service publicId."),
|
|
1374
|
+
name: z10.string().min(1).max(60).describe("New service name (1\u201360 chars).")
|
|
1232
1375
|
},
|
|
1233
1376
|
handler: async (args2, ctx) => {
|
|
1234
1377
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -1261,15 +1404,15 @@ defineTool({
|
|
|
1261
1404
|
'Example: update_service_config({ service_id: "svc_abc", start_command: "bun apps/api/src/index.ts" }) \u2192 { service: { startCommand: "bun apps/api/src/index.ts", \u2026 } }'
|
|
1262
1405
|
].join("\n"),
|
|
1263
1406
|
input: {
|
|
1264
|
-
service_id:
|
|
1265
|
-
install_command:
|
|
1266
|
-
build_command:
|
|
1267
|
-
start_command:
|
|
1268
|
-
branch:
|
|
1269
|
-
root_directory:
|
|
1270
|
-
dockerfile_path:
|
|
1271
|
-
auto_deploy:
|
|
1272
|
-
instance_count:
|
|
1407
|
+
service_id: z10.string().describe("Service publicId."),
|
|
1408
|
+
install_command: z10.string().nullable().optional().describe("Install shell command. Null clears."),
|
|
1409
|
+
build_command: z10.string().nullable().optional().describe("Build shell command. Null clears."),
|
|
1410
|
+
start_command: z10.string().nullable().optional().describe("Start shell command. Null clears."),
|
|
1411
|
+
branch: z10.string().optional().describe("Git branch to track."),
|
|
1412
|
+
root_directory: z10.string().optional().describe("Build context root."),
|
|
1413
|
+
dockerfile_path: z10.string().nullable().optional().describe("Path to Dockerfile relative to root. Null clears."),
|
|
1414
|
+
auto_deploy: z10.boolean().optional().describe("Auto-deploy on push."),
|
|
1415
|
+
instance_count: z10.number().int().positive().max(50).optional().describe("Pin min and max instances to this value (1\u201350).")
|
|
1273
1416
|
},
|
|
1274
1417
|
handler: async (args2, ctx) => {
|
|
1275
1418
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -1334,7 +1477,7 @@ defineTool({
|
|
|
1334
1477
|
'Example: suspend_service({ service_id: "svc_dev" }) \u2192 { ok: true }'
|
|
1335
1478
|
].join("\n"),
|
|
1336
1479
|
input: {
|
|
1337
|
-
service_id:
|
|
1480
|
+
service_id: z10.string().describe("Service publicId.")
|
|
1338
1481
|
},
|
|
1339
1482
|
handler: async (args2, ctx) => {
|
|
1340
1483
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -1358,7 +1501,7 @@ defineTool({
|
|
|
1358
1501
|
'Example: resume_service({ service_id: "svc_dev" }) \u2192 { ok: true }'
|
|
1359
1502
|
].join("\n"),
|
|
1360
1503
|
input: {
|
|
1361
|
-
service_id:
|
|
1504
|
+
service_id: z10.string().describe("Service publicId.")
|
|
1362
1505
|
},
|
|
1363
1506
|
handler: async (args2, ctx) => {
|
|
1364
1507
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -1377,18 +1520,29 @@ defineTool({
|
|
|
1377
1520
|
"Inputs:",
|
|
1378
1521
|
" - service_id: publicId of the service.",
|
|
1379
1522
|
" - lines (optional): tail size (default 200, max 1000).",
|
|
1380
|
-
|
|
1523
|
+
' - since/until (optional): ISO-8601 timestamp OR a relative offset like "-5m", "-1h", "-2d".',
|
|
1381
1524
|
' - stream (optional): "stdout" | "stderr". Omit to combine.',
|
|
1525
|
+
" - level (optional): friendly synonym \u2014 info/debug \u2192 stdout, warn/error \u2192 stderr.",
|
|
1526
|
+
" - search (optional): case-insensitive substring grep, \u2264100 chars.",
|
|
1527
|
+
' - count_only (optional): when true, returns { count } only \u2014 much cheaper for "how many error lines in last 5m" polling.',
|
|
1528
|
+
"",
|
|
1529
|
+
"Returns: { logs: LogEntry[] | string } when count_only is false (each entry has { timestamp, level?, stream?, message }), or { count: number } when count_only is true.",
|
|
1382
1530
|
"",
|
|
1383
|
-
|
|
1531
|
+
'Example: get_service_logs({ service_id: "svc_abc", search: "OOM", since: "-15m" }) \u2192 { logs: [...] }.',
|
|
1384
1532
|
"",
|
|
1385
|
-
|
|
1533
|
+
"More examples:",
|
|
1534
|
+
' - Last 50 stderr lines from the past hour: get_service_logs({ service_id: "svc_abc", lines: 50, stream: "stderr", since: "-1h" })',
|
|
1535
|
+
' - Just count error lines without fetching them: get_service_logs({ service_id: "svc_abc", level: "error", since: "-5m", count_only: true }) \u2192 { count: 47 }'
|
|
1386
1536
|
].join("\n"),
|
|
1387
1537
|
input: {
|
|
1388
|
-
service_id:
|
|
1389
|
-
lines:
|
|
1390
|
-
since:
|
|
1391
|
-
|
|
1538
|
+
service_id: z10.string().describe("Service publicId."),
|
|
1539
|
+
lines: z10.number().int().positive().max(1e3).optional().describe("Tail size; default 200, hard cap 1000."),
|
|
1540
|
+
since: z10.string().optional().describe('ISO-8601 timestamp or relative offset (e.g. "-5m", "-1h").'),
|
|
1541
|
+
until: z10.string().optional().describe("ISO-8601 timestamp or relative offset upper bound."),
|
|
1542
|
+
stream: z10.enum(["stdout", "stderr"]).optional().describe("Restrict to one stream."),
|
|
1543
|
+
level: z10.enum(["stdout", "stderr", "info", "warn", "error", "debug"]).optional().describe("Friendly stream alias: info/debug\u2192stdout, warn/error\u2192stderr."),
|
|
1544
|
+
search: z10.string().max(100).optional().describe("Case-insensitive substring filter."),
|
|
1545
|
+
count_only: z10.boolean().optional().describe("When true, return only { count } \u2014 skips the log payload.")
|
|
1392
1546
|
},
|
|
1393
1547
|
handler: async (args2, ctx) => {
|
|
1394
1548
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -1396,8 +1550,18 @@ defineTool({
|
|
|
1396
1550
|
lines: args2.lines ?? 200
|
|
1397
1551
|
};
|
|
1398
1552
|
if (args2.since) opts.since = args2.since;
|
|
1553
|
+
if (args2.until) opts.until = args2.until;
|
|
1399
1554
|
if (args2.stream) opts.stream = args2.stream;
|
|
1555
|
+
if (args2.level) opts.level = args2.level;
|
|
1556
|
+
if (args2.search) opts.search = args2.search;
|
|
1557
|
+
if (args2.count_only) opts.countOnly = args2.count_only;
|
|
1400
1558
|
const response = await ctx.hoststack.services.getRuntimeLogs(teamId, args2.service_id, opts);
|
|
1559
|
+
if ("count" in response) {
|
|
1560
|
+
return respond({
|
|
1561
|
+
summary: `${response.count} matching log line${response.count === 1 ? "" : "s"} for service ${args2.service_id}.`,
|
|
1562
|
+
data: { count: response.count }
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1401
1565
|
const count = Array.isArray(response.logs) ? response.logs.length : typeof response.logs === "string" ? response.logs.split("\n").length : 0;
|
|
1402
1566
|
return respond({
|
|
1403
1567
|
summary: `Fetched ${count} log line${count === 1 ? "" : "s"} for service ${args2.service_id}.`,
|
|
@@ -1406,6 +1570,146 @@ defineTool({
|
|
|
1406
1570
|
}
|
|
1407
1571
|
});
|
|
1408
1572
|
|
|
1573
|
+
// src/tools/volumes.ts
|
|
1574
|
+
import { z as z11 } from "zod";
|
|
1575
|
+
defineTool({
|
|
1576
|
+
name: "list_volumes",
|
|
1577
|
+
category: "volumes",
|
|
1578
|
+
description: [
|
|
1579
|
+
"List persistent disks attached to a service. Volumes mount writable storage into the container at the path you choose, surviving redeploys and restarts.",
|
|
1580
|
+
"",
|
|
1581
|
+
"When to use: auditing what disks are attached, checking sizes before adding more, or confirming a volume took effect after creation. Note: storage is metered for billing \u2014 use this to spot oversized disks.",
|
|
1582
|
+
"",
|
|
1583
|
+
"Inputs:",
|
|
1584
|
+
" - service_id: publicId of the service.",
|
|
1585
|
+
"",
|
|
1586
|
+
"Returns: { items: Volume[] } \u2014 each entry has id, publicId, name, mountPath, sizeGb, status (pending|active|deleting), createdAt, updatedAt.",
|
|
1587
|
+
"",
|
|
1588
|
+
'Example: list_volumes({ service_id: "svc_abc" }) \u2192 { items: [{ name: "data", mountPath: "/var/data", sizeGb: 10, status: "active" }] }'
|
|
1589
|
+
].join("\n"),
|
|
1590
|
+
input: {
|
|
1591
|
+
service_id: z11.string().describe("Service publicId (e.g. svc_abc123).")
|
|
1592
|
+
},
|
|
1593
|
+
handler: async (args2, ctx) => {
|
|
1594
|
+
const teamId = await ctx.resolveTeamId();
|
|
1595
|
+
const response = await ctx.hoststack.volumes.list(teamId, args2.service_id);
|
|
1596
|
+
const data = shapeList(response, "volumes", shapeVolume);
|
|
1597
|
+
const summary = data.items.length === 0 ? `No volumes attached to service ${args2.service_id}.` : `Found ${data.items.length} volume${data.items.length === 1 ? "" : "s"} on service ${args2.service_id}.`;
|
|
1598
|
+
return respond({ summary, data });
|
|
1599
|
+
}
|
|
1600
|
+
});
|
|
1601
|
+
defineTool({
|
|
1602
|
+
name: "create_volume",
|
|
1603
|
+
category: "volumes",
|
|
1604
|
+
description: [
|
|
1605
|
+
"Attach a new persistent disk to a service. The disk gets provisioned on the host and bind-mounted into the container at `mount_path` on the next deploy. Use this when an app needs writable persistent storage \u2014 uploads, caches, SQLite databases, anything that must survive container restarts.",
|
|
1606
|
+
"",
|
|
1607
|
+
"When to use: the user mentions storing files, uploads, or any data that needs to outlive a container restart. For purely ephemeral scratch space, point the app at /tmp instead (already tmpfs-mounted, free, no provisioning needed) \u2014 see HOSTSTACK_TMP_DIR.",
|
|
1608
|
+
"",
|
|
1609
|
+
"Inputs:",
|
|
1610
|
+
" - service_id: publicId of the service to attach to.",
|
|
1611
|
+
" - name: lowercase alphanumeric + hyphens, \u226464 chars (used as the docker volume identifier \u2014 change with care once data is written).",
|
|
1612
|
+
' - mount_path: in-container absolute path (e.g. "/var/data").',
|
|
1613
|
+
" - size_gb: optional, 1\u2013100, default 1. Counts against your plan storage quota and is metered for billing.",
|
|
1614
|
+
"",
|
|
1615
|
+
"Returns: { volume: Volume } \u2014 the created record.",
|
|
1616
|
+
"",
|
|
1617
|
+
'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: "active" } }'
|
|
1618
|
+
].join("\n"),
|
|
1619
|
+
input: {
|
|
1620
|
+
service_id: z11.string().describe("Service publicId."),
|
|
1621
|
+
name: z11.string().min(1).max(64).regex(/^[a-z0-9-]+$/).describe("Volume name (lowercase alphanumeric + hyphens)."),
|
|
1622
|
+
mount_path: z11.string().startsWith("/").max(500).describe("In-container mount path (absolute)."),
|
|
1623
|
+
size_gb: z11.number().int().min(1).max(100).optional().describe("Disk size in GB (default 1, max 100).")
|
|
1624
|
+
},
|
|
1625
|
+
handler: async (args2, ctx) => {
|
|
1626
|
+
const teamId = await ctx.resolveTeamId();
|
|
1627
|
+
const input = {
|
|
1628
|
+
name: args2.name,
|
|
1629
|
+
mountPath: args2.mount_path
|
|
1630
|
+
};
|
|
1631
|
+
if (args2.size_gb !== void 0) input.sizeGb = args2.size_gb;
|
|
1632
|
+
const response = await ctx.hoststack.volumes.create(teamId, args2.service_id, input);
|
|
1633
|
+
const data = { volume: shape(response.volume) };
|
|
1634
|
+
return respond({
|
|
1635
|
+
summary: `Attached volume "${args2.name}" (${args2.size_gb ?? 1}GB) at ${args2.mount_path} on service ${args2.service_id}.`,
|
|
1636
|
+
data
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
});
|
|
1640
|
+
defineTool({
|
|
1641
|
+
name: "update_volume",
|
|
1642
|
+
category: "volumes",
|
|
1643
|
+
description: [
|
|
1644
|
+
"Update a volume's mountPath or sizeGb. Resizes take effect on the next deploy; shrinking is rejected at the storage layer (filesystems can't safely shrink under live data).",
|
|
1645
|
+
"",
|
|
1646
|
+
"When to use: the user wants to grow the disk for an existing volume, or move where it mounts inside the container.",
|
|
1647
|
+
"",
|
|
1648
|
+
"Inputs:",
|
|
1649
|
+
" - service_id: publicId of the service.",
|
|
1650
|
+
" - volume_id: publicId of the volume to update.",
|
|
1651
|
+
" - mount_path (optional): new in-container mount path.",
|
|
1652
|
+
" - size_gb (optional): new size in GB (must be \u2265 current).",
|
|
1653
|
+
"",
|
|
1654
|
+
"Returns: { volume: Volume } \u2014 the updated record.",
|
|
1655
|
+
"",
|
|
1656
|
+
'Example: update_volume({ service_id: "svc_abc", volume_id: "vol_xyz", size_gb: 20 }) \u2192 { volume: { sizeGb: 20, \u2026 } }'
|
|
1657
|
+
].join("\n"),
|
|
1658
|
+
input: {
|
|
1659
|
+
service_id: z11.string().describe("Service publicId."),
|
|
1660
|
+
volume_id: z11.string().describe("Volume publicId (e.g. vol_\u2026)."),
|
|
1661
|
+
mount_path: z11.string().startsWith("/").max(500).optional().describe("New mount path."),
|
|
1662
|
+
size_gb: z11.number().int().min(1).max(100).optional().describe("New size in GB.")
|
|
1663
|
+
},
|
|
1664
|
+
handler: async (args2, ctx) => {
|
|
1665
|
+
const teamId = await ctx.resolveTeamId();
|
|
1666
|
+
const input = {};
|
|
1667
|
+
if (args2.mount_path !== void 0) input.mountPath = args2.mount_path;
|
|
1668
|
+
if (args2.size_gb !== void 0) input.sizeGb = args2.size_gb;
|
|
1669
|
+
if (Object.keys(input).length === 0) {
|
|
1670
|
+
return respond({ summary: "No fields to update.", data: {} });
|
|
1671
|
+
}
|
|
1672
|
+
const response = await ctx.hoststack.volumes.update(
|
|
1673
|
+
teamId,
|
|
1674
|
+
args2.service_id,
|
|
1675
|
+
args2.volume_id,
|
|
1676
|
+
input
|
|
1677
|
+
);
|
|
1678
|
+
const data = { volume: shape(response.volume) };
|
|
1679
|
+
const fields = Object.keys(input).join(", ");
|
|
1680
|
+
return respond({ summary: `Updated ${fields} on volume ${args2.volume_id}.`, data });
|
|
1681
|
+
}
|
|
1682
|
+
});
|
|
1683
|
+
defineTool({
|
|
1684
|
+
name: "delete_volume",
|
|
1685
|
+
category: "volumes",
|
|
1686
|
+
description: [
|
|
1687
|
+
"Detach and deprovision a volume. The underlying disk is destroyed \u2014 back up any data first. Async: marks the row deleting, the host agent finalises removal once it acks. A periodic reaper retries if the agent was offline at delete time.",
|
|
1688
|
+
"",
|
|
1689
|
+
"When to use: the user explicitly says to remove a volume, or you're cleaning up after retiring a service. Confirm before calling on production data.",
|
|
1690
|
+
"",
|
|
1691
|
+
"Inputs:",
|
|
1692
|
+
" - service_id: publicId of the service.",
|
|
1693
|
+
" - volume_id: publicId of the volume.",
|
|
1694
|
+
"",
|
|
1695
|
+
"Returns: { ok: true }.",
|
|
1696
|
+
"",
|
|
1697
|
+
'Example: delete_volume({ service_id: "svc_abc", volume_id: "vol_xyz" }) \u2192 { ok: true }'
|
|
1698
|
+
].join("\n"),
|
|
1699
|
+
input: {
|
|
1700
|
+
service_id: z11.string().describe("Service publicId."),
|
|
1701
|
+
volume_id: z11.string().describe("Volume publicId.")
|
|
1702
|
+
},
|
|
1703
|
+
handler: async (args2, ctx) => {
|
|
1704
|
+
const teamId = await ctx.resolveTeamId();
|
|
1705
|
+
await ctx.hoststack.volumes.delete(teamId, args2.service_id, args2.volume_id);
|
|
1706
|
+
return respond({
|
|
1707
|
+
summary: `Detached volume ${args2.volume_id} from service ${args2.service_id} (deprovisioning).`,
|
|
1708
|
+
data: { ok: true }
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
});
|
|
1712
|
+
|
|
1409
1713
|
// src/server-factory.ts
|
|
1410
1714
|
var PACKAGE_NAME = "hoststack";
|
|
1411
1715
|
var PACKAGE_VERSION = "0.1.2";
|