@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/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.28.0" : "0.0.0-dev";
|
|
7
7
|
var USER_AGENT = `hoststack-mcp/${MCP_VERSION}`;
|
|
8
8
|
|
|
9
9
|
// src/api-client.ts
|
|
@@ -524,7 +524,7 @@ defineTool({
|
|
|
524
524
|
"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.",
|
|
525
525
|
"",
|
|
526
526
|
"Inputs (all optional):",
|
|
527
|
-
' - since: ISO-8601 timestamp OR relative offset like "-1h" / "-2d". Default: -24h.',
|
|
527
|
+
' - 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".',
|
|
528
528
|
" - until: ISO-8601 upper bound (ignored when aggregating \u2014 aggregated view always extends to now).",
|
|
529
529
|
" - limit: max rows (default 100, hard cap 500).",
|
|
530
530
|
" - aggregate: true (default) collapses by (action, resourceId); false returns raw rows.",
|
|
@@ -535,7 +535,9 @@ defineTool({
|
|
|
535
535
|
"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 }] }."
|
|
536
536
|
].join("\n"),
|
|
537
537
|
input: {
|
|
538
|
-
since: z3.string().optional().describe(
|
|
538
|
+
since: z3.string().optional().describe(
|
|
539
|
+
'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.'
|
|
540
|
+
),
|
|
539
541
|
until: z3.string().optional().describe("ISO-8601 upper bound. Only honored when aggregate=false."),
|
|
540
542
|
limit: z3.number().int().positive().max(500).optional().describe("Max rows (default 100, hard cap 500)."),
|
|
541
543
|
aggregate: z3.boolean().optional().describe("Collapse by (action, resourceId). Default true."),
|
|
@@ -760,7 +762,7 @@ defineTool({
|
|
|
760
762
|
" - environment_id (optional): bind to a specific environment; defaults to the project Production env.",
|
|
761
763
|
" - postgis (optional): provision the PostGIS image variant so `CREATE EXTENSION postgis` works. Postgres only.",
|
|
762
764
|
" - pgvector (optional): provision the pgvector image variant so `CREATE EXTENSION vector` works. Postgres only, mutually exclusive with postgis.",
|
|
763
|
-
" - 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.",
|
|
765
|
+
" - 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.",
|
|
764
766
|
"",
|
|
765
767
|
'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).',
|
|
766
768
|
"",
|
|
@@ -1137,6 +1139,37 @@ defineTool({
|
|
|
1137
1139
|
return respond({ summary, data: result });
|
|
1138
1140
|
}
|
|
1139
1141
|
});
|
|
1142
|
+
defineTool({
|
|
1143
|
+
name: "list_database_backups",
|
|
1144
|
+
category: "databases",
|
|
1145
|
+
description: [
|
|
1146
|
+
"List the off-site archives a managed database can be restored from, newest first.",
|
|
1147
|
+
"",
|
|
1148
|
+
'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.',
|
|
1149
|
+
"",
|
|
1150
|
+
"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.",
|
|
1151
|
+
"",
|
|
1152
|
+
"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.",
|
|
1153
|
+
"",
|
|
1154
|
+
"Inputs:",
|
|
1155
|
+
" - database_id: publicId of the database (db_\u2026).",
|
|
1156
|
+
"",
|
|
1157
|
+
"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.",
|
|
1158
|
+
"",
|
|
1159
|
+
'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" }] }'
|
|
1160
|
+
].join("\n"),
|
|
1161
|
+
input: {
|
|
1162
|
+
database_id: z6.string().describe("Database publicId (e.g. db_\u2026).")
|
|
1163
|
+
},
|
|
1164
|
+
handler: async (args, ctx) => {
|
|
1165
|
+
const teamId = await ctx.resolveTeamId();
|
|
1166
|
+
const response = await ctx.hoststack.databases.listRestorePoints(teamId, args.database_id);
|
|
1167
|
+
const data = shapeList(response, "restorePoints", shape);
|
|
1168
|
+
const newest = response.restorePoints[0];
|
|
1169
|
+
const summary = data.items.length === 0 ? `Database ${args.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 ${args.database_id} has ${data.items.length} restore point${data.items.length === 1 ? "" : "s"}; newest ${newest?.archiveName} from ${newest?.createdAt}.`;
|
|
1170
|
+
return respond({ summary, data });
|
|
1171
|
+
}
|
|
1172
|
+
});
|
|
1140
1173
|
|
|
1141
1174
|
// src/tools/deploys.ts
|
|
1142
1175
|
import { z as z7 } from "zod";
|
|
@@ -3241,6 +3274,7 @@ var NOTIFICATION_EVENTS = [
|
|
|
3241
3274
|
"dns.registry_record_changed",
|
|
3242
3275
|
"domain.registrant_verification_lapsed",
|
|
3243
3276
|
"service.auto_restarted",
|
|
3277
|
+
"project.release_awaiting_start",
|
|
3244
3278
|
"machine.offline",
|
|
3245
3279
|
"machine.online",
|
|
3246
3280
|
"billing.invoice",
|
|
@@ -4062,6 +4096,27 @@ var DEV_BOX_INSIDE = [
|
|
|
4062
4096
|
" - 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).",
|
|
4063
4097
|
" - `hoststack-status` prints what is running, the box dev URL and agent login state at any time."
|
|
4064
4098
|
].join("\n");
|
|
4099
|
+
var RESOURCE_LINK_TYPES2 = [
|
|
4100
|
+
"database",
|
|
4101
|
+
"object_storage",
|
|
4102
|
+
"queue",
|
|
4103
|
+
"search",
|
|
4104
|
+
"email_domain"
|
|
4105
|
+
];
|
|
4106
|
+
var LINK_ALIAS = {
|
|
4107
|
+
database: "DATABASE",
|
|
4108
|
+
object_storage: "S3",
|
|
4109
|
+
queue: "QUEUE",
|
|
4110
|
+
search: "SEARCH",
|
|
4111
|
+
email_domain: "EMAIL"
|
|
4112
|
+
};
|
|
4113
|
+
function defaultLinkAlias(type, used) {
|
|
4114
|
+
const base = LINK_ALIAS[type];
|
|
4115
|
+
if (!used.includes(base)) return base;
|
|
4116
|
+
let n = 2;
|
|
4117
|
+
while (used.includes(`${base}_${String(n)}`)) n++;
|
|
4118
|
+
return `${base}_${String(n)}`;
|
|
4119
|
+
}
|
|
4065
4120
|
var SERVICE_TYPES = [
|
|
4066
4121
|
"web_service",
|
|
4067
4122
|
"private_service",
|
|
@@ -4180,8 +4235,10 @@ defineTool({
|
|
|
4180
4235
|
" - environment_id (optional): bind to a specific environment; defaults to the project Production env.",
|
|
4181
4236
|
" - auto_deploy (optional, default true): trigger the first deploy immediately when a source is present.",
|
|
4182
4237
|
" - 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.",
|
|
4238
|
+
" - 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.",
|
|
4239
|
+
" - 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.",
|
|
4183
4240
|
"",
|
|
4184
|
-
"Returns: { service: Service, deployId: number | null }.",
|
|
4241
|
+
"Returns: { service: Service, deployId: number | null, linkErrors: [{ resourceType, resourceId, error }] } \u2014 a refused link is reported, not fatal.",
|
|
4185
4242
|
"",
|
|
4186
4243
|
'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 }',
|
|
4187
4244
|
"Repo not listed by list_github_repos? Call sync_github_repos \u2014 a repository pushed since the last sync is invisible until then.",
|
|
@@ -4219,7 +4276,23 @@ defineTool({
|
|
|
4219
4276
|
plan: z20.enum(SERVICE_PLANS).optional().describe('Service size (default "micro").'),
|
|
4220
4277
|
environment_id: z20.union([z20.number().int().positive(), z20.string()]).optional().describe("Bind to a specific environment; defaults to Production."),
|
|
4221
4278
|
auto_deploy: z20.boolean().optional().describe("Trigger the first deploy immediately (default true)."),
|
|
4222
|
-
machine: machineInput
|
|
4279
|
+
machine: machineInput,
|
|
4280
|
+
env_vars: z20.array(
|
|
4281
|
+
z20.object({
|
|
4282
|
+
key: z20.string().min(1).max(256),
|
|
4283
|
+
value: z20.string().max(32768),
|
|
4284
|
+
is_secret: z20.boolean().optional()
|
|
4285
|
+
})
|
|
4286
|
+
).max(1e3).optional().describe("Environment variables set before the first deploy."),
|
|
4287
|
+
links: z20.array(
|
|
4288
|
+
z20.object({
|
|
4289
|
+
resource_type: z20.enum(RESOURCE_LINK_TYPES2),
|
|
4290
|
+
resource_id: z20.number().int().positive(),
|
|
4291
|
+
alias: z20.string().min(1).max(48).optional()
|
|
4292
|
+
})
|
|
4293
|
+
).max(20).optional().describe(
|
|
4294
|
+
"Existing managed resources to connect before the first deploy (e.g. a database from create_database)."
|
|
4295
|
+
)
|
|
4223
4296
|
},
|
|
4224
4297
|
handler: async (args, ctx) => {
|
|
4225
4298
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -4256,10 +4329,26 @@ defineTool({
|
|
|
4256
4329
|
if (args.machine !== void 0) {
|
|
4257
4330
|
input.machineId = await resolveMachineId(ctx, teamId, args.machine);
|
|
4258
4331
|
}
|
|
4332
|
+
if (args.env_vars !== void 0) {
|
|
4333
|
+
input.envVars = args.env_vars.map((v) => ({
|
|
4334
|
+
key: v.key,
|
|
4335
|
+
value: v.value,
|
|
4336
|
+
isSecret: v.is_secret ?? false
|
|
4337
|
+
}));
|
|
4338
|
+
}
|
|
4339
|
+
if (args.links !== void 0) {
|
|
4340
|
+
const aliases = [];
|
|
4341
|
+
input.links = args.links.map((l) => {
|
|
4342
|
+
const alias = l.alias ?? defaultLinkAlias(l.resource_type, aliases);
|
|
4343
|
+
aliases.push(alias);
|
|
4344
|
+
return { resourceType: l.resource_type, resourceId: l.resource_id, alias };
|
|
4345
|
+
});
|
|
4346
|
+
}
|
|
4259
4347
|
const response = await ctx.hoststack.services.create(teamId, input);
|
|
4260
4348
|
const data = {
|
|
4261
4349
|
service: shapeService(response.service),
|
|
4262
|
-
deployId: response.deployId ?? null
|
|
4350
|
+
deployId: response.deployId ?? null,
|
|
4351
|
+
linkErrors: response.linkErrors ?? []
|
|
4263
4352
|
};
|
|
4264
4353
|
const publicId = data.service && "publicId" in data.service ? data.service.publicId : "unknown";
|
|
4265
4354
|
return respond({ summary: `Created service "${args.name}" (${publicId}).`, data });
|
|
@@ -5397,10 +5486,14 @@ defineTool({
|
|
|
5397
5486
|
"Inputs:",
|
|
5398
5487
|
" - service_id: publicId of the service.",
|
|
5399
5488
|
"",
|
|
5400
|
-
"Returns: { items: Volume[] } \u2014 each entry has id, publicId, name, mountPath, sizeGb, status (pending|active|deleting), backupEnabled, createdAt, updatedAt.",
|
|
5489
|
+
"Returns: { items: Volume[] } \u2014 each entry has id, publicId, name, mountPath, sizeGb, status (pending|active|deleting), backupEnabled, blockBacked, adopted, createdAt, updatedAt.",
|
|
5401
5490
|
"",
|
|
5402
5491
|
"`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 }).",
|
|
5403
5492
|
"",
|
|
5493
|
+
'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.',
|
|
5494
|
+
"",
|
|
5495
|
+
'`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.',
|
|
5496
|
+
"",
|
|
5404
5497
|
'Example: list_volumes({ service_id: "svc_abc" }) \u2192 { items: [{ name: "data", mountPath: "/var/data", sizeGb: 10, status: "active", backupEnabled: false }] }'
|
|
5405
5498
|
].join("\n"),
|
|
5406
5499
|
input: {
|
|
@@ -5473,7 +5566,9 @@ defineTool({
|
|
|
5473
5566
|
" - volume_id: publicId of the volume to update.",
|
|
5474
5567
|
" - mount_path (optional): new in-container mount path.",
|
|
5475
5568
|
" - size_gb (optional): new size in GB (must be \u2265 current).",
|
|
5476
|
-
" - 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.",
|
|
5569
|
+
" - 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).",
|
|
5570
|
+
"",
|
|
5571
|
+
"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.",
|
|
5477
5572
|
"",
|
|
5478
5573
|
"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.",
|
|
5479
5574
|
"",
|