@hoststack.dev/mcp 0.2.0 → 0.3.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/dist/hoststack-mcp.js +217 -37
- package/dist/hoststack-mcp.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +217 -37
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ declare class ApiClient {
|
|
|
20
20
|
private handle;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
type ToolCategory = 'projects' | 'services' | 'deploys' | 'databases' | 'domains' | 'env-vars' | 'cron' | 'logs' | 'activity-log' | 'meta';
|
|
23
|
+
type ToolCategory = 'projects' | 'services' | 'deploys' | 'databases' | 'domains' | 'volumes' | 'env-vars' | 'cron' | 'logs' | 'activity-log' | 'meta';
|
|
24
24
|
interface ToolContext {
|
|
25
25
|
hoststack: HostStack;
|
|
26
26
|
api: ApiClient;
|
package/dist/index.js
CHANGED
|
@@ -324,6 +324,9 @@ function shapeUser(value) {
|
|
|
324
324
|
function shapeTeam(value) {
|
|
325
325
|
return isObject(value) ? dropNullsAndInternals(value) : {};
|
|
326
326
|
}
|
|
327
|
+
function shapeVolume(value) {
|
|
328
|
+
return isObject(value) ? dropNullsAndInternals(value) : {};
|
|
329
|
+
}
|
|
327
330
|
|
|
328
331
|
// src/resources/hoststack-resources.ts
|
|
329
332
|
defineResource({
|
|
@@ -589,8 +592,8 @@ defineTool({
|
|
|
589
592
|
handler: async (args, ctx) => {
|
|
590
593
|
const teamId = await ctx.resolveTeamId();
|
|
591
594
|
const response = await ctx.hoststack.deploys.list(teamId, args.service_id);
|
|
592
|
-
const data = shapeList(response, "
|
|
593
|
-
const summary = data.items.length === 0 ? `No deploys yet for service ${args.service_id}.` : `Found ${data.items.length} deploy${data.items.length === 1 ? "" : "s"} for service ${args.service_id}.`;
|
|
595
|
+
const data = shapeList(response, "data", shapeDeploy);
|
|
596
|
+
const summary = data.items.length === 0 ? `No deploys yet for service ${args.service_id}.` : `Found ${data.items.length} deploy${data.items.length === 1 ? "" : "s"} for service ${args.service_id} (page ${response.page} of ${response.totalPages}, ${response.total} total).`;
|
|
594
597
|
return respond({ summary, data });
|
|
595
598
|
}
|
|
596
599
|
});
|
|
@@ -696,22 +699,32 @@ defineTool({
|
|
|
696
699
|
" - service_id: publicId of the service.",
|
|
697
700
|
" - deploy_id: publicId of the deploy.",
|
|
698
701
|
"",
|
|
699
|
-
"Returns: { logs: string } \u2014
|
|
702
|
+
"Returns: { logs: string, lineCount: number, nextAfterId: number | null } \u2014 flattened build output joined with newlines, line count, and a cursor for pagination (pass back as after_id to fetch the next page).",
|
|
700
703
|
"",
|
|
701
|
-
'Example: get_deploy_logs({ service_id: "svc_abc", deploy_id: "dpl_xyz" }) \u2192 { logs: "Building\u2026\\nStep 1/8\u2026\\n\u2026" }'
|
|
704
|
+
'Example: get_deploy_logs({ service_id: "svc_abc", deploy_id: "dpl_xyz" }) \u2192 { logs: "Building\u2026\\nStep 1/8\u2026\\n\u2026", lineCount: 42, nextAfterId: 1234 }'
|
|
702
705
|
].join("\n"),
|
|
703
706
|
input: {
|
|
704
707
|
service_id: z5.string().describe("Service publicId."),
|
|
705
|
-
deploy_id: z5.string().describe("Deploy publicId.")
|
|
708
|
+
deploy_id: z5.string().describe("Deploy publicId."),
|
|
709
|
+
after_id: z5.number().int().optional().describe("Pagination cursor from a previous response\u2019s nextAfterId."),
|
|
710
|
+
limit: z5.number().int().min(1).max(5e3).optional().describe("Max log entries to fetch (default 500, hard cap 5000).")
|
|
706
711
|
},
|
|
707
712
|
handler: async (args, ctx) => {
|
|
708
713
|
const teamId = await ctx.resolveTeamId();
|
|
709
|
-
const response = await ctx.hoststack.deploys.getLogs(
|
|
710
|
-
|
|
711
|
-
|
|
714
|
+
const response = await ctx.hoststack.deploys.getLogs(
|
|
715
|
+
teamId,
|
|
716
|
+
args.service_id,
|
|
717
|
+
args.deploy_id,
|
|
718
|
+
{
|
|
719
|
+
...args.after_id !== void 0 ? { afterId: args.after_id } : {},
|
|
720
|
+
...args.limit !== void 0 ? { limit: args.limit } : {}
|
|
721
|
+
}
|
|
722
|
+
);
|
|
723
|
+
const entries = response.logs;
|
|
724
|
+
const logs = entries.map((e) => e.message).join("\n");
|
|
712
725
|
return respond({
|
|
713
|
-
summary: `Fetched ${
|
|
714
|
-
data: { logs }
|
|
726
|
+
summary: `Fetched ${entries.length} log line${entries.length === 1 ? "" : "s"} for deploy ${args.deploy_id}.`,
|
|
727
|
+
data: { logs, lineCount: entries.length, nextAfterId: response.nextAfterId }
|
|
715
728
|
});
|
|
716
729
|
}
|
|
717
730
|
});
|
|
@@ -1237,52 +1250,79 @@ defineTool({
|
|
|
1237
1250
|
name: "update_service_config",
|
|
1238
1251
|
category: "services",
|
|
1239
1252
|
description: [
|
|
1240
|
-
"Update build/runtime configuration for a service: build command, start command, branch, root directory, dockerfile path, auto-deploy flag, instance count
|
|
1253
|
+
"Update build/runtime configuration for a service: build command, start command, install command, branch, root directory, dockerfile path, auto-deploy flag, instance count. All fields optional \u2014 pass only what you want to change.",
|
|
1241
1254
|
"",
|
|
1242
|
-
"When to use: the user wants to tweak how a service builds or runs
|
|
1255
|
+
"When to use: the user wants to tweak how a service builds or runs. Build/runtime fields (branch, install/build/start command, root, dockerfile) take effect on the next deploy \u2014 call trigger_deploy after if you need them applied immediately. instance_count rescales without a redeploy.",
|
|
1243
1256
|
"",
|
|
1244
1257
|
"Inputs:",
|
|
1245
1258
|
" - service_id: publicId of the service.",
|
|
1246
|
-
" - build_command, start_command (optional): shell commands.",
|
|
1259
|
+
" - install_command, build_command, start_command (optional): shell commands. Pass null to clear.",
|
|
1247
1260
|
" - branch (optional): git branch to track.",
|
|
1248
|
-
" - root_directory
|
|
1261
|
+
" - root_directory (optional): build context root inside the repo.",
|
|
1262
|
+
" - dockerfile_path (optional): path to Dockerfile relative to root_directory. Pass null to clear.",
|
|
1249
1263
|
" - auto_deploy (optional): boolean \u2014 auto-deploy on git push.",
|
|
1250
|
-
" - instance_count (optional): integer \u22651 \u2014
|
|
1251
|
-
' - plan (optional): plan tier slug (e.g. "starter", "standard", "pro").',
|
|
1264
|
+
" - instance_count (optional): integer \u22651 \u2014 pin both min and max instances to this value.",
|
|
1252
1265
|
"",
|
|
1253
|
-
"Returns: { config
|
|
1266
|
+
"Returns: { service?: Service, config?: ServiceConfig } \u2014 whichever rows were touched.",
|
|
1254
1267
|
"",
|
|
1255
|
-
'Example: update_service_config({ service_id: "svc_abc",
|
|
1268
|
+
'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 } }'
|
|
1256
1269
|
].join("\n"),
|
|
1257
1270
|
input: {
|
|
1258
1271
|
service_id: z9.string().describe("Service publicId."),
|
|
1259
|
-
|
|
1260
|
-
|
|
1272
|
+
install_command: z9.string().nullable().optional().describe("Install shell command. Null clears."),
|
|
1273
|
+
build_command: z9.string().nullable().optional().describe("Build shell command. Null clears."),
|
|
1274
|
+
start_command: z9.string().nullable().optional().describe("Start shell command. Null clears."),
|
|
1261
1275
|
branch: z9.string().optional().describe("Git branch to track."),
|
|
1262
1276
|
root_directory: z9.string().optional().describe("Build context root."),
|
|
1263
|
-
dockerfile_path: z9.string().optional().describe("Path to Dockerfile relative to root."),
|
|
1277
|
+
dockerfile_path: z9.string().nullable().optional().describe("Path to Dockerfile relative to root. Null clears."),
|
|
1264
1278
|
auto_deploy: z9.boolean().optional().describe("Auto-deploy on push."),
|
|
1265
|
-
instance_count: z9.number().int().positive().optional().describe("
|
|
1266
|
-
plan: z9.string().optional().describe("Plan tier slug.")
|
|
1279
|
+
instance_count: z9.number().int().positive().max(50).optional().describe("Pin min and max instances to this value (1\u201350).")
|
|
1267
1280
|
},
|
|
1268
1281
|
handler: async (args, ctx) => {
|
|
1269
1282
|
const teamId = await ctx.resolveTeamId();
|
|
1270
|
-
const
|
|
1271
|
-
if (args.
|
|
1272
|
-
|
|
1273
|
-
if (args.
|
|
1274
|
-
if (args.
|
|
1275
|
-
if (args.dockerfile_path !== void 0)
|
|
1276
|
-
|
|
1277
|
-
if (args.
|
|
1278
|
-
if (args.
|
|
1279
|
-
|
|
1283
|
+
const serviceUpdate = {};
|
|
1284
|
+
if (args.install_command !== void 0)
|
|
1285
|
+
serviceUpdate["installCommand"] = args.install_command;
|
|
1286
|
+
if (args.build_command !== void 0) serviceUpdate["buildCommand"] = args.build_command;
|
|
1287
|
+
if (args.start_command !== void 0) serviceUpdate["startCommand"] = args.start_command;
|
|
1288
|
+
if (args.dockerfile_path !== void 0)
|
|
1289
|
+
serviceUpdate["dockerfilePath"] = args.dockerfile_path;
|
|
1290
|
+
if (args.branch !== void 0) serviceUpdate["branch"] = args.branch;
|
|
1291
|
+
if (args.root_directory !== void 0)
|
|
1292
|
+
serviceUpdate["rootDirectory"] = args.root_directory;
|
|
1293
|
+
if (args.auto_deploy !== void 0) serviceUpdate["autoDeploy"] = args.auto_deploy;
|
|
1294
|
+
const configUpdate = {};
|
|
1295
|
+
if (args.instance_count !== void 0) {
|
|
1296
|
+
configUpdate["minInstances"] = args.instance_count;
|
|
1297
|
+
configUpdate["maxInstances"] = args.instance_count;
|
|
1298
|
+
}
|
|
1299
|
+
if (Object.keys(serviceUpdate).length === 0 && Object.keys(configUpdate).length === 0) {
|
|
1280
1300
|
return respond({ summary: "No fields to update.", data: {} });
|
|
1281
1301
|
}
|
|
1282
|
-
const
|
|
1283
|
-
const
|
|
1284
|
-
|
|
1285
|
-
|
|
1302
|
+
const data = {};
|
|
1303
|
+
const touchedFields = [];
|
|
1304
|
+
if (Object.keys(serviceUpdate).length > 0) {
|
|
1305
|
+
const serviceResponse = await ctx.hoststack.services.update(
|
|
1306
|
+
teamId,
|
|
1307
|
+
args.service_id,
|
|
1308
|
+
serviceUpdate
|
|
1309
|
+
);
|
|
1310
|
+
data["service"] = shape(serviceResponse.service);
|
|
1311
|
+
touchedFields.push(...Object.keys(serviceUpdate));
|
|
1312
|
+
}
|
|
1313
|
+
if (Object.keys(configUpdate).length > 0) {
|
|
1314
|
+
const configResponse = await ctx.hoststack.services.updateConfig(
|
|
1315
|
+
teamId,
|
|
1316
|
+
args.service_id,
|
|
1317
|
+
configUpdate
|
|
1318
|
+
);
|
|
1319
|
+
data["config"] = shape(configResponse.config);
|
|
1320
|
+
touchedFields.push(...Object.keys(configUpdate));
|
|
1321
|
+
}
|
|
1322
|
+
return respond({
|
|
1323
|
+
summary: `Updated ${touchedFields.join(", ")} on service ${args.service_id}.`,
|
|
1324
|
+
data
|
|
1325
|
+
});
|
|
1286
1326
|
}
|
|
1287
1327
|
});
|
|
1288
1328
|
defineTool({
|
|
@@ -1373,6 +1413,146 @@ defineTool({
|
|
|
1373
1413
|
}
|
|
1374
1414
|
});
|
|
1375
1415
|
|
|
1416
|
+
// src/tools/volumes.ts
|
|
1417
|
+
import { z as z10 } from "zod";
|
|
1418
|
+
defineTool({
|
|
1419
|
+
name: "list_volumes",
|
|
1420
|
+
category: "volumes",
|
|
1421
|
+
description: [
|
|
1422
|
+
"List persistent disks attached to a service. Volumes mount writable storage into the container at the path you choose, surviving redeploys and restarts.",
|
|
1423
|
+
"",
|
|
1424
|
+
"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.",
|
|
1425
|
+
"",
|
|
1426
|
+
"Inputs:",
|
|
1427
|
+
" - service_id: publicId of the service.",
|
|
1428
|
+
"",
|
|
1429
|
+
"Returns: { items: Volume[] } \u2014 each entry has id, publicId, name, mountPath, sizeGb, status (pending|active|deleting), createdAt, updatedAt.",
|
|
1430
|
+
"",
|
|
1431
|
+
'Example: list_volumes({ service_id: "svc_abc" }) \u2192 { items: [{ name: "data", mountPath: "/var/data", sizeGb: 10, status: "active" }] }'
|
|
1432
|
+
].join("\n"),
|
|
1433
|
+
input: {
|
|
1434
|
+
service_id: z10.string().describe("Service publicId (e.g. svc_abc123).")
|
|
1435
|
+
},
|
|
1436
|
+
handler: async (args, ctx) => {
|
|
1437
|
+
const teamId = await ctx.resolveTeamId();
|
|
1438
|
+
const response = await ctx.hoststack.volumes.list(teamId, args.service_id);
|
|
1439
|
+
const data = shapeList(response, "volumes", shapeVolume);
|
|
1440
|
+
const summary = data.items.length === 0 ? `No volumes attached to service ${args.service_id}.` : `Found ${data.items.length} volume${data.items.length === 1 ? "" : "s"} on service ${args.service_id}.`;
|
|
1441
|
+
return respond({ summary, data });
|
|
1442
|
+
}
|
|
1443
|
+
});
|
|
1444
|
+
defineTool({
|
|
1445
|
+
name: "create_volume",
|
|
1446
|
+
category: "volumes",
|
|
1447
|
+
description: [
|
|
1448
|
+
"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.",
|
|
1449
|
+
"",
|
|
1450
|
+
"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.",
|
|
1451
|
+
"",
|
|
1452
|
+
"Inputs:",
|
|
1453
|
+
" - service_id: publicId of the service to attach to.",
|
|
1454
|
+
" - name: lowercase alphanumeric + hyphens, \u226464 chars (used as the docker volume identifier \u2014 change with care once data is written).",
|
|
1455
|
+
' - mount_path: in-container absolute path (e.g. "/var/data").',
|
|
1456
|
+
" - size_gb: optional, 1\u2013100, default 1. Counts against your plan storage quota and is metered for billing.",
|
|
1457
|
+
"",
|
|
1458
|
+
"Returns: { volume: Volume } \u2014 the created record.",
|
|
1459
|
+
"",
|
|
1460
|
+
'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" } }'
|
|
1461
|
+
].join("\n"),
|
|
1462
|
+
input: {
|
|
1463
|
+
service_id: z10.string().describe("Service publicId."),
|
|
1464
|
+
name: z10.string().min(1).max(64).regex(/^[a-z0-9-]+$/).describe("Volume name (lowercase alphanumeric + hyphens)."),
|
|
1465
|
+
mount_path: z10.string().startsWith("/").max(500).describe("In-container mount path (absolute)."),
|
|
1466
|
+
size_gb: z10.number().int().min(1).max(100).optional().describe("Disk size in GB (default 1, max 100).")
|
|
1467
|
+
},
|
|
1468
|
+
handler: async (args, ctx) => {
|
|
1469
|
+
const teamId = await ctx.resolveTeamId();
|
|
1470
|
+
const input = {
|
|
1471
|
+
name: args.name,
|
|
1472
|
+
mountPath: args.mount_path
|
|
1473
|
+
};
|
|
1474
|
+
if (args.size_gb !== void 0) input.sizeGb = args.size_gb;
|
|
1475
|
+
const response = await ctx.hoststack.volumes.create(teamId, args.service_id, input);
|
|
1476
|
+
const data = { volume: shape(response.volume) };
|
|
1477
|
+
return respond({
|
|
1478
|
+
summary: `Attached volume "${args.name}" (${args.size_gb ?? 1}GB) at ${args.mount_path} on service ${args.service_id}.`,
|
|
1479
|
+
data
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
});
|
|
1483
|
+
defineTool({
|
|
1484
|
+
name: "update_volume",
|
|
1485
|
+
category: "volumes",
|
|
1486
|
+
description: [
|
|
1487
|
+
"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).",
|
|
1488
|
+
"",
|
|
1489
|
+
"When to use: the user wants to grow the disk for an existing volume, or move where it mounts inside the container.",
|
|
1490
|
+
"",
|
|
1491
|
+
"Inputs:",
|
|
1492
|
+
" - service_id: publicId of the service.",
|
|
1493
|
+
" - volume_id: publicId of the volume to update.",
|
|
1494
|
+
" - mount_path (optional): new in-container mount path.",
|
|
1495
|
+
" - size_gb (optional): new size in GB (must be \u2265 current).",
|
|
1496
|
+
"",
|
|
1497
|
+
"Returns: { volume: Volume } \u2014 the updated record.",
|
|
1498
|
+
"",
|
|
1499
|
+
'Example: update_volume({ service_id: "svc_abc", volume_id: "vol_xyz", size_gb: 20 }) \u2192 { volume: { sizeGb: 20, \u2026 } }'
|
|
1500
|
+
].join("\n"),
|
|
1501
|
+
input: {
|
|
1502
|
+
service_id: z10.string().describe("Service publicId."),
|
|
1503
|
+
volume_id: z10.string().describe("Volume publicId (e.g. vol_\u2026)."),
|
|
1504
|
+
mount_path: z10.string().startsWith("/").max(500).optional().describe("New mount path."),
|
|
1505
|
+
size_gb: z10.number().int().min(1).max(100).optional().describe("New size in GB.")
|
|
1506
|
+
},
|
|
1507
|
+
handler: async (args, ctx) => {
|
|
1508
|
+
const teamId = await ctx.resolveTeamId();
|
|
1509
|
+
const input = {};
|
|
1510
|
+
if (args.mount_path !== void 0) input.mountPath = args.mount_path;
|
|
1511
|
+
if (args.size_gb !== void 0) input.sizeGb = args.size_gb;
|
|
1512
|
+
if (Object.keys(input).length === 0) {
|
|
1513
|
+
return respond({ summary: "No fields to update.", data: {} });
|
|
1514
|
+
}
|
|
1515
|
+
const response = await ctx.hoststack.volumes.update(
|
|
1516
|
+
teamId,
|
|
1517
|
+
args.service_id,
|
|
1518
|
+
args.volume_id,
|
|
1519
|
+
input
|
|
1520
|
+
);
|
|
1521
|
+
const data = { volume: shape(response.volume) };
|
|
1522
|
+
const fields = Object.keys(input).join(", ");
|
|
1523
|
+
return respond({ summary: `Updated ${fields} on volume ${args.volume_id}.`, data });
|
|
1524
|
+
}
|
|
1525
|
+
});
|
|
1526
|
+
defineTool({
|
|
1527
|
+
name: "delete_volume",
|
|
1528
|
+
category: "volumes",
|
|
1529
|
+
description: [
|
|
1530
|
+
"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.",
|
|
1531
|
+
"",
|
|
1532
|
+
"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.",
|
|
1533
|
+
"",
|
|
1534
|
+
"Inputs:",
|
|
1535
|
+
" - service_id: publicId of the service.",
|
|
1536
|
+
" - volume_id: publicId of the volume.",
|
|
1537
|
+
"",
|
|
1538
|
+
"Returns: { ok: true }.",
|
|
1539
|
+
"",
|
|
1540
|
+
'Example: delete_volume({ service_id: "svc_abc", volume_id: "vol_xyz" }) \u2192 { ok: true }'
|
|
1541
|
+
].join("\n"),
|
|
1542
|
+
input: {
|
|
1543
|
+
service_id: z10.string().describe("Service publicId."),
|
|
1544
|
+
volume_id: z10.string().describe("Volume publicId.")
|
|
1545
|
+
},
|
|
1546
|
+
handler: async (args, ctx) => {
|
|
1547
|
+
const teamId = await ctx.resolveTeamId();
|
|
1548
|
+
await ctx.hoststack.volumes.delete(teamId, args.service_id, args.volume_id);
|
|
1549
|
+
return respond({
|
|
1550
|
+
summary: `Detached volume ${args.volume_id} from service ${args.service_id} (deprovisioning).`,
|
|
1551
|
+
data: { ok: true }
|
|
1552
|
+
});
|
|
1553
|
+
}
|
|
1554
|
+
});
|
|
1555
|
+
|
|
1376
1556
|
// src/server-factory.ts
|
|
1377
1557
|
var PACKAGE_NAME = "hoststack";
|
|
1378
1558
|
var PACKAGE_VERSION = "0.1.2";
|