@hoststack.dev/mcp 0.26.0 → 0.28.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 +104 -9
- package/dist/hoststack-mcp.js.map +1 -1
- package/dist/index.js +104 -9
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -97,7 +97,7 @@ use by hand — an MCP client is what should be launching it.
|
|
|
97
97
|
|
|
98
98
|
## Tool inventory
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
113 tools. The headings are the registry's own categories rather than a friendlier regrouping, so the build can diff this table against the registry and fail when the two disagree — which is how an earlier version of it came to advertise a total from three releases back and send agents to the dashboard for a `create_database` that had already shipped.
|
|
101
101
|
|
|
102
102
|
| Category | Read | Write |
|
|
103
103
|
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -106,7 +106,7 @@ use by hand — an MCP client is what should be launching it.
|
|
|
106
106
|
| **logs** | `get_service_logs`, `get_service_logs_bulk` | — |
|
|
107
107
|
| **deploys** | `list_deploys`, `get_deploy`, `get_deploy_logs`, `diagnose_deploy` | `trigger_deploy`, `cancel_deploy` |
|
|
108
108
|
| **environments** | `list_environments` | `create_environment`, `update_environment`, `delete_environment`, `promote_deploy` |
|
|
109
|
-
| **databases** | `list_databases`, `get_database`, `get_database_cluster`, `query_database`
|
|
109
|
+
| **databases** | `list_databases`, `get_database`, `get_database_cluster`, `list_database_backups`, `query_database` | `create_database`, `update_database`, `delete_database`, `suspend_database`, `resume_database`, `restart_database`, `upgrade_database_to_ha`, `upgrade_database_version` |
|
|
110
110
|
| **volumes** | `list_volumes`, `list_volume_backups` | `create_volume`, `update_volume`, `delete_volume`, `restore_volume` |
|
|
111
111
|
| **resource-links** | `list_managed_resources`, `list_service_resources` | `link_resource_to_service`, `unlink_resource_from_service` |
|
|
112
112
|
| **machines** | `list_machines`, `get_machine` | — |
|
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.28.0" : "0.0.0-dev";
|
|
12
12
|
var USER_AGENT = `hoststack-mcp/${MCP_VERSION}`;
|
|
13
13
|
|
|
14
14
|
// src/api-client.ts
|
|
@@ -523,7 +523,7 @@ defineTool({
|
|
|
523
523
|
"Deploy.failed_consecutive entries (v89) carry the offending `commitHash` in lastMetadata, and the streak now dedupes per (service, commitHash) \u2014 one critical per bad commit, not one every 6 retries.",
|
|
524
524
|
"",
|
|
525
525
|
"Inputs (all optional):",
|
|
526
|
-
' - since: ISO-8601 timestamp OR relative offset like "-1h" / "-2d". Default: -24h.',
|
|
526
|
+
' - since: ISO-8601 timestamp OR relative offset like "-1h" / "-2d". Default: -24h, clamped to 30 days back. It bounds the CLEARED history only: an alert that is still open is always returned, however long ago it fired \u2014 so the default view answers "what is on fire" and not "what caught fire today".',
|
|
527
527
|
" - until: ISO-8601 upper bound (ignored when aggregating \u2014 aggregated view always extends to now).",
|
|
528
528
|
" - limit: max rows (default 100, hard cap 500).",
|
|
529
529
|
" - aggregate: true (default) collapses by (action, resourceId); false returns raw rows.",
|
|
@@ -534,7 +534,9 @@ defineTool({
|
|
|
534
534
|
"Example: list_alerts({ since: '-1h' }) \u2192 { alerts: [{ action: 'deploy.failed_consecutive', resourceId: 31, severity: 'critical', active: true, count: 3, lastFiredAt: '\u2026', lastResolvedAt: null, lastMetadata: { commitHash: 'abc1234' }, \u2026 }] }."
|
|
535
535
|
].join("\n"),
|
|
536
536
|
input: {
|
|
537
|
-
since: z3.string().optional().describe(
|
|
537
|
+
since: z3.string().optional().describe(
|
|
538
|
+
'ISO-8601 timestamp or relative offset (e.g. "-1h", "-2d"). Default: -24h. Bounds the cleared history only \u2014 still-open alerts are returned regardless of age.'
|
|
539
|
+
),
|
|
538
540
|
until: z3.string().optional().describe("ISO-8601 upper bound. Only honored when aggregate=false."),
|
|
539
541
|
limit: z3.number().int().positive().max(500).optional().describe("Max rows (default 100, hard cap 500)."),
|
|
540
542
|
aggregate: z3.boolean().optional().describe("Collapse by (action, resourceId). Default true."),
|
|
@@ -759,7 +761,7 @@ defineTool({
|
|
|
759
761
|
" - environment_id (optional): bind to a specific environment; defaults to the project Production env.",
|
|
760
762
|
" - postgis (optional): provision the PostGIS image variant so `CREATE EXTENSION postgis` works. Postgres only.",
|
|
761
763
|
" - pgvector (optional): provision the pgvector image variant so `CREATE EXTENSION vector` works. Postgres only, mutually exclusive with postgis.",
|
|
762
|
-
" - machine (optional): put it on one of the team's OWN enrolled machines (name or id, see list_machines) instead of HostStack compute. It costs nothing there and is still backed up off-site, but it is reachable ONLY from that same machine \u2014 the service that queries it has to run there too, and linking it to a service anywhere else is refused. No HA and no external access on a machine you own.",
|
|
764
|
+
" - machine (optional): put it on one of the team's OWN enrolled machines (name or id, see list_machines) instead of HostStack compute. It costs nothing there and is still backed up off-site, but it is reachable ONLY from that same machine \u2014 the service that queries it has to run there too, and linking it to a service anywhere else is refused. No HA and no external access on a machine you own. An infrastructure machine takes a database of the project it is bound to, and nothing else.",
|
|
763
765
|
"",
|
|
764
766
|
'Returns: { database: Database } \u2014 note BOTH `id` (numeric \u2014 this is what link_resource_to_service wants) and `publicId` ("db_\u2026" \u2014 what the other database tools want).',
|
|
765
767
|
"",
|
|
@@ -1136,6 +1138,37 @@ defineTool({
|
|
|
1136
1138
|
return respond({ summary, data: result });
|
|
1137
1139
|
}
|
|
1138
1140
|
});
|
|
1141
|
+
defineTool({
|
|
1142
|
+
name: "list_database_backups",
|
|
1143
|
+
category: "databases",
|
|
1144
|
+
description: [
|
|
1145
|
+
"List the off-site archives a managed database can be restored from, newest first.",
|
|
1146
|
+
"",
|
|
1147
|
+
'WHY THIS MATTERS: HostStack dumps every managed database nightly to off-site object storage. That is a SCHEDULE \u2014 a promise about what should happen. It does not say any dump exists. A database whose dump has failed every night since it was created (no upload grant, a full disk, a rotated credential, an engine version the dumper does not know) looks identical from the outside to one backed up every night, and the first person to find out is whoever already needed the restore. This tool is how you tell "protected" from "believed to be protected". Check it for anything whose loss is not survivable.',
|
|
1148
|
+
"",
|
|
1149
|
+
"A dump written onto the machine's OWN disk is not listed, on purpose. On a machine of the customer's own with no upload grant that is where the dump lands \u2014 the same disk holding the database \u2014 so counting it would be counting the thing that can be lost.",
|
|
1150
|
+
"",
|
|
1151
|
+
"When to use: before a risky migration, when someone asks what the recovery position is, or when auditing a project's backup exposure. Read it together with `lastOffsiteBackupAt` on get_database: that field says WHEN, this says WHAT.",
|
|
1152
|
+
"",
|
|
1153
|
+
"Inputs:",
|
|
1154
|
+
" - database_id: publicId of the database (db_\u2026).",
|
|
1155
|
+
"",
|
|
1156
|
+
"Returns: { items: RestorePoint[] } \u2014 id, archiveName, sizeBytes (may be null on older agents), createdAt, s3Url. Only the most recent few are kept, matching the retention on the machine: seven rows means seven restore points, not seven backups ever taken.",
|
|
1157
|
+
"",
|
|
1158
|
+
'Example: list_database_backups({ database_id: "db_abc" }) \u2192 { items: [{ id: 12, archiveName: "backup-2026-09-16T02-00-11-004Z.sql.gz", sizeBytes: 314572800, createdAt: "2026-09-16T02:01:42Z" }] }'
|
|
1159
|
+
].join("\n"),
|
|
1160
|
+
input: {
|
|
1161
|
+
database_id: z6.string().describe("Database publicId (e.g. db_\u2026).")
|
|
1162
|
+
},
|
|
1163
|
+
handler: async (args2, ctx) => {
|
|
1164
|
+
const teamId = await ctx.resolveTeamId();
|
|
1165
|
+
const response = await ctx.hoststack.databases.listRestorePoints(teamId, args2.database_id);
|
|
1166
|
+
const data = shapeList(response, "restorePoints", shape);
|
|
1167
|
+
const newest = response.restorePoints[0];
|
|
1168
|
+
const summary = data.items.length === 0 ? `Database ${args2.database_id} has NO off-site restore points. Either no dump has completed yet, or every dump is landing on the machine's own disk \u2014 either way there is currently nothing to restore from. Check lastBackupStatus on get_database for which: offsite_failed means a dump ran and its upload was attempted and failed, and lastBackupError carries the provider's reason.` : `Database ${args2.database_id} has ${data.items.length} restore point${data.items.length === 1 ? "" : "s"}; newest ${newest?.archiveName} from ${newest?.createdAt}.`;
|
|
1169
|
+
return respond({ summary, data });
|
|
1170
|
+
}
|
|
1171
|
+
});
|
|
1139
1172
|
|
|
1140
1173
|
// src/tools/deploys.ts
|
|
1141
1174
|
import { z as z7 } from "zod";
|
|
@@ -3240,6 +3273,7 @@ var NOTIFICATION_EVENTS = [
|
|
|
3240
3273
|
"dns.registry_record_changed",
|
|
3241
3274
|
"domain.registrant_verification_lapsed",
|
|
3242
3275
|
"service.auto_restarted",
|
|
3276
|
+
"project.release_awaiting_start",
|
|
3243
3277
|
"machine.offline",
|
|
3244
3278
|
"machine.online",
|
|
3245
3279
|
"billing.invoice",
|
|
@@ -4061,6 +4095,27 @@ var DEV_BOX_INSIDE = [
|
|
|
4061
4095
|
" - Language runtimes: node, bun, python and php 8.3 are built in. go, java, ruby, rust, elixir and dotnet install on demand with `dev-runtime add <name>` (persisted to /workspace). Together that covers every runtime the platform itself deploys \u2014 a WordPress/Laravel/Rails stack runs in the box. elixir needs `dev-runtime add erlang` first: it is a BEAM distribution with no OTP of its own, and Ubuntu ships one too old to use, so mise builds a current one (takes a few minutes).",
|
|
4062
4096
|
" - `hoststack-status` prints what is running, the box dev URL and agent login state at any time."
|
|
4063
4097
|
].join("\n");
|
|
4098
|
+
var RESOURCE_LINK_TYPES2 = [
|
|
4099
|
+
"database",
|
|
4100
|
+
"object_storage",
|
|
4101
|
+
"queue",
|
|
4102
|
+
"search",
|
|
4103
|
+
"email_domain"
|
|
4104
|
+
];
|
|
4105
|
+
var LINK_ALIAS = {
|
|
4106
|
+
database: "DATABASE",
|
|
4107
|
+
object_storage: "S3",
|
|
4108
|
+
queue: "QUEUE",
|
|
4109
|
+
search: "SEARCH",
|
|
4110
|
+
email_domain: "EMAIL"
|
|
4111
|
+
};
|
|
4112
|
+
function defaultLinkAlias(type, used) {
|
|
4113
|
+
const base = LINK_ALIAS[type];
|
|
4114
|
+
if (!used.includes(base)) return base;
|
|
4115
|
+
let n = 2;
|
|
4116
|
+
while (used.includes(`${base}_${String(n)}`)) n++;
|
|
4117
|
+
return `${base}_${String(n)}`;
|
|
4118
|
+
}
|
|
4064
4119
|
var SERVICE_TYPES = [
|
|
4065
4120
|
"web_service",
|
|
4066
4121
|
"private_service",
|
|
@@ -4179,8 +4234,10 @@ defineTool({
|
|
|
4179
4234
|
" - environment_id (optional): bind to a specific environment; defaults to the project Production env.",
|
|
4180
4235
|
" - auto_deploy (optional, default true): trigger the first deploy immediately when a source is present.",
|
|
4181
4236
|
" - machine (optional): run it on one of the team's OWN enrolled machines (name or id, see list_machines) instead of HostStack compute. Pinned at creation and never moved afterwards, and the service is up only while that machine is.",
|
|
4237
|
+
" - env_vars (optional): [{ key, value, is_secret? }] written before the first deploy, so it boots with them. Beats set_env_var afterwards, which only reaches deploy #2.",
|
|
4238
|
+
" - links (optional): [{ resource_type, resource_id, alias? }] existing managed resources (create_database first) to connect before the first deploy. A database link injects DATABASE_URL / REDIS_URL / MONGO_URL plus <ALIAS>_URL etc. alias defaults to DATABASE, S3, QUEUE, SEARCH or EMAIL by type.",
|
|
4182
4239
|
"",
|
|
4183
|
-
"Returns: { service: Service, deployId: number | null }.",
|
|
4240
|
+
"Returns: { service: Service, deployId: number | null, linkErrors: [{ resourceType, resourceId, error }] } \u2014 a refused link is reported, not fatal.",
|
|
4184
4241
|
"",
|
|
4185
4242
|
'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 }',
|
|
4186
4243
|
"Repo not listed by list_github_repos? Call sync_github_repos \u2014 a repository pushed since the last sync is invisible until then.",
|
|
@@ -4218,7 +4275,23 @@ defineTool({
|
|
|
4218
4275
|
plan: z20.enum(SERVICE_PLANS).optional().describe('Service size (default "micro").'),
|
|
4219
4276
|
environment_id: z20.union([z20.number().int().positive(), z20.string()]).optional().describe("Bind to a specific environment; defaults to Production."),
|
|
4220
4277
|
auto_deploy: z20.boolean().optional().describe("Trigger the first deploy immediately (default true)."),
|
|
4221
|
-
machine: machineInput
|
|
4278
|
+
machine: machineInput,
|
|
4279
|
+
env_vars: z20.array(
|
|
4280
|
+
z20.object({
|
|
4281
|
+
key: z20.string().min(1).max(256),
|
|
4282
|
+
value: z20.string().max(32768),
|
|
4283
|
+
is_secret: z20.boolean().optional()
|
|
4284
|
+
})
|
|
4285
|
+
).max(1e3).optional().describe("Environment variables set before the first deploy."),
|
|
4286
|
+
links: z20.array(
|
|
4287
|
+
z20.object({
|
|
4288
|
+
resource_type: z20.enum(RESOURCE_LINK_TYPES2),
|
|
4289
|
+
resource_id: z20.number().int().positive(),
|
|
4290
|
+
alias: z20.string().min(1).max(48).optional()
|
|
4291
|
+
})
|
|
4292
|
+
).max(20).optional().describe(
|
|
4293
|
+
"Existing managed resources to connect before the first deploy (e.g. a database from create_database)."
|
|
4294
|
+
)
|
|
4222
4295
|
},
|
|
4223
4296
|
handler: async (args2, ctx) => {
|
|
4224
4297
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -4255,10 +4328,26 @@ defineTool({
|
|
|
4255
4328
|
if (args2.machine !== void 0) {
|
|
4256
4329
|
input.machineId = await resolveMachineId(ctx, teamId, args2.machine);
|
|
4257
4330
|
}
|
|
4331
|
+
if (args2.env_vars !== void 0) {
|
|
4332
|
+
input.envVars = args2.env_vars.map((v) => ({
|
|
4333
|
+
key: v.key,
|
|
4334
|
+
value: v.value,
|
|
4335
|
+
isSecret: v.is_secret ?? false
|
|
4336
|
+
}));
|
|
4337
|
+
}
|
|
4338
|
+
if (args2.links !== void 0) {
|
|
4339
|
+
const aliases = [];
|
|
4340
|
+
input.links = args2.links.map((l) => {
|
|
4341
|
+
const alias = l.alias ?? defaultLinkAlias(l.resource_type, aliases);
|
|
4342
|
+
aliases.push(alias);
|
|
4343
|
+
return { resourceType: l.resource_type, resourceId: l.resource_id, alias };
|
|
4344
|
+
});
|
|
4345
|
+
}
|
|
4258
4346
|
const response = await ctx.hoststack.services.create(teamId, input);
|
|
4259
4347
|
const data = {
|
|
4260
4348
|
service: shapeService(response.service),
|
|
4261
|
-
deployId: response.deployId ?? null
|
|
4349
|
+
deployId: response.deployId ?? null,
|
|
4350
|
+
linkErrors: response.linkErrors ?? []
|
|
4262
4351
|
};
|
|
4263
4352
|
const publicId = data.service && "publicId" in data.service ? data.service.publicId : "unknown";
|
|
4264
4353
|
return respond({ summary: `Created service "${args2.name}" (${publicId}).`, data });
|
|
@@ -5396,10 +5485,14 @@ defineTool({
|
|
|
5396
5485
|
"Inputs:",
|
|
5397
5486
|
" - service_id: publicId of the service.",
|
|
5398
5487
|
"",
|
|
5399
|
-
"Returns: { items: Volume[] } \u2014 each entry has id, publicId, name, mountPath, sizeGb, status (pending|active|deleting), backupEnabled, createdAt, updatedAt.",
|
|
5488
|
+
"Returns: { items: Volume[] } \u2014 each entry has id, publicId, name, mountPath, sizeGb, status (pending|active|deleting), backupEnabled, blockBacked, adopted, createdAt, updatedAt.",
|
|
5400
5489
|
"",
|
|
5401
5490
|
"`backupEnabled` says backups are being TAKEN, not that any exist. Call list_volume_backups to see which archives are actually restorable \u2014 a volume can be enabled and have nothing behind it (nothing has completed yet, or the host has no upload grant and is writing tars onto the very disk it is backing up, which is not a copy of anything). Turn it on with update_volume({ backup_enabled: true }).",
|
|
5402
5491
|
"",
|
|
5492
|
+
'READ `backupEnabled: false` AGAINST `blockBacked` BEFORE REPORTING IT. On a block-backed volume it is not a to-do: the disk is already triple-replicated by Hetzner and update_volume refuses to turn the toggle on, so "backups are off" is the wrong finding to hand a user. On every other volume \u2014 `blockBacked: false`, `adopted` either way \u2014 it IS a to-do, and it means the only copy of that data is one disk.',
|
|
5493
|
+
"",
|
|
5494
|
+
'`adopted: true` is an existing Docker volume on an infrastructure machine, mounted in place rather than created by us. `sizeGb` is 0 on one and means "not ours to say", not "empty". Backups can be turned on \u2014 they only read it \u2014 but resize, restore, import and delete are refused.',
|
|
5495
|
+
"",
|
|
5403
5496
|
'Example: list_volumes({ service_id: "svc_abc" }) \u2192 { items: [{ name: "data", mountPath: "/var/data", sizeGb: 10, status: "active", backupEnabled: false }] }'
|
|
5404
5497
|
].join("\n"),
|
|
5405
5498
|
input: {
|
|
@@ -5472,7 +5565,9 @@ defineTool({
|
|
|
5472
5565
|
" - volume_id: publicId of the volume to update.",
|
|
5473
5566
|
" - mount_path (optional): new in-container mount path.",
|
|
5474
5567
|
" - size_gb (optional): new size in GB (must be \u2265 current).",
|
|
5475
|
-
" - backup_enabled (optional): whether this volume is backed up nightly. Turning it off stops future backups; it does not delete the ones already taken. Verify with list_volume_backups \u2014 enabling is not the same as having a backup.",
|
|
5568
|
+
" - backup_enabled (optional): whether this volume is backed up nightly. Turning it off stops future backups; it does not delete the ones already taken. Verify with list_volume_backups \u2014 enabling is not the same as having a backup. Refused on a block-backed volume (Hetzner already replicates it three ways).",
|
|
5569
|
+
"",
|
|
5570
|
+
"On an ADOPTED volume (`adopted: true` from list_volumes \u2014 an infrastructure machine's own Docker volume) this is the ONLY field that can be changed: a backup reads the volume and writes the archive elsewhere, while mount_path and size_gb describe what the machine's operator set up.",
|
|
5476
5571
|
"",
|
|
5477
5572
|
"IMPORTANT about what a volume backup is: a block-level tar of a LIVE filesystem, i.e. CRASH CONSISTENT, not application consistent. Nothing is quiesced. For a container running its own database (WordPress + MariaDB in one box, Postgres on a disk, SQLite under write), the archive captures whatever was on disk mid-write \u2014 the same state the database would face after a power cut. Usually recoverable, occasionally not. Recommend this as the disaster fallback and a scheduled dump as the actual backup; do not present it as a substitute for one.",
|
|
5478
5573
|
"",
|