@hoststack.dev/mcp 0.8.0 → 0.9.1
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 +9 -3
- package/dist/hoststack-mcp.js +83 -23
- package/dist/hoststack-mcp.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +82 -22
- package/dist/index.js.map +1 -1
- package/manifest.json +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -52,7 +52,13 @@ Add to `~/.cursor/mcp.json` (or via Settings → MCP):
|
|
|
52
52
|
#### Claude Code
|
|
53
53
|
|
|
54
54
|
```bash
|
|
55
|
-
claude mcp add hoststack -- npx -y @hoststack.dev/mcp
|
|
55
|
+
claude mcp add hoststack --env HOSTSTACK_API_KEY=hs_live_... -- npx -y @hoststack.dev/mcp
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Or — equivalently — use the hosted HTTP transport so credentials live only in your Claude Code config, not on disk:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
claude mcp add --transport http hoststack https://hoststack.dev/api/mcp --header "Authorization: Bearer hs_live_..."
|
|
56
62
|
```
|
|
57
63
|
|
|
58
64
|
#### Any other client (hosted)
|
|
@@ -79,7 +85,7 @@ If `HOSTSTACK_API_KEY` is set in your shell, it gets baked into the snippet; oth
|
|
|
79
85
|
|
|
80
86
|
## Tool inventory
|
|
81
87
|
|
|
82
|
-
|
|
88
|
+
58 tools, grouped by resource:
|
|
83
89
|
|
|
84
90
|
| Category | Read | Write |
|
|
85
91
|
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
@@ -87,7 +93,7 @@ If `HOSTSTACK_API_KEY` is set in your shell, it gets baked into the snippet; oth
|
|
|
87
93
|
| **services** | `list_services`, `get_service`, `get_service_metrics`, `get_service_metrics_history`, `get_service_logs`, `get_service_logs_bulk` | `update_service`, `update_service_config`, `suspend_service`, `resume_service` |
|
|
88
94
|
| **deploys** | `list_deploys`, `get_deploy`, `get_deploy_logs`, `diagnose_deploy` | `trigger_deploy`, `cancel_deploy` |
|
|
89
95
|
| **environments** | `list_environments` | `create_environment`, `delete_environment`, `promote_deploy` |
|
|
90
|
-
| **databases** | `list_databases`, `get_database`
|
|
96
|
+
| **databases** | `list_databases`, `get_database`, `get_database_cluster`, `query_database` | `update_database`, `upgrade_database_to_ha` (use the dashboard for create/delete/credentials) |
|
|
91
97
|
| **volumes** | `list_volumes` | `create_volume`, `update_volume`, `delete_volume` |
|
|
92
98
|
| **domains** | `list_domains` | `add_domain`, `verify_domain`, `remove_domain` |
|
|
93
99
|
| **dns** | `list_dns_zones`, `list_dns_records`, `get_dns_record` | `create_dns_record`, `update_dns_record`, `delete_dns_record` |
|
package/dist/hoststack-mcp.js
CHANGED
|
@@ -702,7 +702,7 @@ defineTool({
|
|
|
702
702
|
"",
|
|
703
703
|
"Returns: { columns: string[], rows: string[][], rowCount: number, truncated: boolean, durationMs: number }. Values come back as strings (psql --csv); cast on the caller side when needed.",
|
|
704
704
|
"",
|
|
705
|
-
"
|
|
705
|
+
"Example:",
|
|
706
706
|
' - query_database({ database_id: "db_abc", sql: "SELECT count(*) FROM users" }) \u2192 { columns: ["count"], rows: [["1234"]], \u2026 }',
|
|
707
707
|
` - query_database({ database_id: "db_abc", sql: "SELECT id, email FROM users WHERE created_at > now() - interval '1 day' LIMIT 50" })`
|
|
708
708
|
].join("\n"),
|
|
@@ -719,6 +719,60 @@ defineTool({
|
|
|
719
719
|
return respond({ summary, data: result });
|
|
720
720
|
}
|
|
721
721
|
});
|
|
722
|
+
defineTool({
|
|
723
|
+
name: "upgrade_database_to_ha",
|
|
724
|
+
category: "databases",
|
|
725
|
+
description: [
|
|
726
|
+
"Trigger a single-node \u2192 HA migration on a managed Postgres database. The agent runs pg_dump \u2192 bootstrap 3-node Patroni cluster (1 leader + 2 sync replicas + HAProxy) \u2192 pg_restore. Customer connections to this database are briefly read-only during the cutover; the standalone container stays running (read-only) for 24 h as a rollback target before automatic decommission.",
|
|
727
|
+
"",
|
|
728
|
+
"Requires: PATRONI_ENABLED env on the HostStack deployment + `teams.ha_beta = true` on the calling team. Returns 403 otherwise. Returns 400 if the database is already HA, not Postgres, not on the standard or pro tier, or has a pg_database_size over 1 GB (in-memory archive transfer cap).",
|
|
729
|
+
"",
|
|
730
|
+
"When to use: a customer needs failover protection on an existing standalone Postgres. For new databases on an HA-eligible team, just call create_database \u2014 the cluster path is automatic.",
|
|
731
|
+
"",
|
|
732
|
+
"Inputs:",
|
|
733
|
+
" - database_id: publicId of the postgres database to upgrade.",
|
|
734
|
+
"",
|
|
735
|
+
'Returns: 202 Accepted. The upgrade is async \u2014 poll get_database until `pg_engine_type === "patroni"` and `status === "available"` to confirm completion. The Cluster tab on the dashboard mirrors the same data.',
|
|
736
|
+
"",
|
|
737
|
+
'Example: upgrade_database_to_ha({ database_id: "db_abc" }) \u2192 { ok: true }'
|
|
738
|
+
].join("\n"),
|
|
739
|
+
input: {
|
|
740
|
+
database_id: z5.string().describe("Database publicId (e.g. db_abc) to upgrade to HA.")
|
|
741
|
+
},
|
|
742
|
+
handler: async (args2, ctx) => {
|
|
743
|
+
const teamId = await ctx.resolveTeamId();
|
|
744
|
+
await ctx.hoststack.databases.upgradeToHa(teamId, args2.database_id);
|
|
745
|
+
return respond({
|
|
746
|
+
summary: `HA migration started for ${args2.database_id}. Poll get_database until pg_engine_type=patroni to confirm.`
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
defineTool({
|
|
751
|
+
name: "get_database_cluster",
|
|
752
|
+
category: "databases",
|
|
753
|
+
description: [
|
|
754
|
+
"Get the live + retired cluster members and failover history for a Patroni HA Postgres database.",
|
|
755
|
+
"",
|
|
756
|
+
"When to use: confirm an HA cluster is healthy (1 leader + 2 sync replicas + 1 proxy + 1 etcd live) or investigate a failover that fired. For standalone databases this returns 400 \u2014 call get_database first if you need to branch.",
|
|
757
|
+
"",
|
|
758
|
+
"Inputs:",
|
|
759
|
+
" - database_id: publicId of the postgres database (must be HA).",
|
|
760
|
+
"",
|
|
761
|
+
"Returns: { members: { id, memberRole: 'etcd' | 'primary-candidate' | 'replica' | 'proxy', containerId, workerHostId, joinedAt, leftAt }[], failovers: { id, createdAt, oldLeader, newLeader }[] }.",
|
|
762
|
+
"",
|
|
763
|
+
'Example: get_database_cluster({ database_id: "db_abc" }) \u2192 { members: [...5 live rows], failovers: [] }'
|
|
764
|
+
].join("\n"),
|
|
765
|
+
input: {
|
|
766
|
+
database_id: z5.string().describe("Database publicId for the HA cluster.")
|
|
767
|
+
},
|
|
768
|
+
handler: async (args2, ctx) => {
|
|
769
|
+
const teamId = await ctx.resolveTeamId();
|
|
770
|
+
const result = await ctx.hoststack.databases.getCluster(teamId, args2.database_id);
|
|
771
|
+
const live = result.members.filter((m) => m.leftAt === null);
|
|
772
|
+
const summary = `${live.length} live members (${live.map((m) => m.memberRole).join(", ")}); ${result.failovers.length} historical failovers.`;
|
|
773
|
+
return respond({ summary, data: result });
|
|
774
|
+
}
|
|
775
|
+
});
|
|
722
776
|
|
|
723
777
|
// src/tools/deploys.ts
|
|
724
778
|
import { z as z6 } from "zod";
|
|
@@ -780,13 +834,14 @@ defineTool({
|
|
|
780
834
|
name: "trigger_deploy",
|
|
781
835
|
category: "deploys",
|
|
782
836
|
description: [
|
|
783
|
-
"Kick off a new deploy
|
|
837
|
+
"Kick off a new deploy. Defaults to the latest commit on the service branch; pass commit_hash or branch to deploy something else.",
|
|
784
838
|
"",
|
|
785
|
-
'When to use: the user explicitly asks to deploy ("ship it", "redeploy", "kick off a new build"). For services with auto-deploy enabled, a fresh git push already triggers a deploy automatically \u2014 only call this for manual triggers
|
|
839
|
+
'When to use: the user explicitly asks to deploy ("ship it", "redeploy", "kick off a new build"). For services with auto-deploy enabled, a fresh git push already triggers a deploy automatically \u2014 only call this for manual triggers or after a config change.',
|
|
786
840
|
"",
|
|
787
841
|
"Inputs:",
|
|
788
842
|
" - service_id: publicId of the service.",
|
|
789
|
-
" -
|
|
843
|
+
" - commit_hash (optional): build a specific commit instead of HEAD on the configured branch.",
|
|
844
|
+
" - branch (optional): build the tip of a non-default branch.",
|
|
790
845
|
"",
|
|
791
846
|
'Returns: { deploy: Deploy } \u2014 the new deploy record. Status will start as "pending" then transition through "building" \u2192 "deploying" \u2192 "live" or "failed".',
|
|
792
847
|
"",
|
|
@@ -794,12 +849,14 @@ defineTool({
|
|
|
794
849
|
].join("\n"),
|
|
795
850
|
input: {
|
|
796
851
|
service_id: z6.string().describe("Service publicId."),
|
|
797
|
-
|
|
852
|
+
commit_hash: z6.string().max(40).optional().describe("Specific commit to build (default: branch HEAD)."),
|
|
853
|
+
branch: z6.string().max(200).optional().describe("Branch to build (default: service configured branch).")
|
|
798
854
|
},
|
|
799
855
|
handler: async (args2, ctx) => {
|
|
800
856
|
const teamId = await ctx.resolveTeamId();
|
|
801
857
|
const input = {};
|
|
802
|
-
if (args2.
|
|
858
|
+
if (args2.commit_hash !== void 0) input.commitHash = args2.commit_hash;
|
|
859
|
+
if (args2.branch !== void 0) input.branch = args2.branch;
|
|
803
860
|
const response = await ctx.hoststack.deploys.trigger(teamId, args2.service_id, input);
|
|
804
861
|
const data = { deploy: shapeDeploy(response.deploy) };
|
|
805
862
|
const publicId = data.deploy && "publicId" in data.deploy ? data.deploy.publicId : "unknown";
|
|
@@ -842,7 +899,7 @@ defineTool({
|
|
|
842
899
|
name: "get_deploy_logs",
|
|
843
900
|
category: "deploys",
|
|
844
901
|
description: [
|
|
845
|
-
"Fetch the build/deploy logs for a single deploy. Returns the entire log buffer the agent can
|
|
902
|
+
"Fetch the build/deploy logs for a single deploy. Returns the entire log buffer the agent can then search for errors.",
|
|
846
903
|
"",
|
|
847
904
|
"When to use: a deploy failed and you need to read the build output to diagnose. Pair with get_deploy to find a failed deploy_id, then call this. For runtime (post-deploy) logs of the running container, use get_service_logs instead.",
|
|
848
905
|
"",
|
|
@@ -1283,7 +1340,8 @@ defineTool({
|
|
|
1283
1340
|
"",
|
|
1284
1341
|
"Inputs:",
|
|
1285
1342
|
' - hostname: the domain to add (e.g. "api.example.com").',
|
|
1286
|
-
" - service_id
|
|
1343
|
+
" - service_id: publicId of the service to bind the domain to.",
|
|
1344
|
+
" - path_prefix (optional): when set, only requests under this URL prefix route to this service. Use for path-based fan-out (e.g. `/api` to an api service, `/` to a web service on the same hostname).",
|
|
1287
1345
|
"",
|
|
1288
1346
|
"Returns: { domain: Domain } \u2014 includes dnsTargets you must configure (CNAME / A records).",
|
|
1289
1347
|
"",
|
|
@@ -1291,12 +1349,16 @@ defineTool({
|
|
|
1291
1349
|
].join("\n"),
|
|
1292
1350
|
input: {
|
|
1293
1351
|
hostname: z8.string().min(3).max(253).describe("Fully-qualified hostname (e.g. api.example.com)."),
|
|
1294
|
-
service_id: z8.string().
|
|
1352
|
+
service_id: z8.string().describe("Service publicId to bind to."),
|
|
1353
|
+
path_prefix: z8.string().optional().describe('Optional URL prefix for path-based routing (e.g. "/api").')
|
|
1295
1354
|
},
|
|
1296
1355
|
handler: async (args2, ctx) => {
|
|
1297
1356
|
const teamId = await ctx.resolveTeamId();
|
|
1298
|
-
const input = {
|
|
1299
|
-
|
|
1357
|
+
const input = {
|
|
1358
|
+
domain: args2.hostname,
|
|
1359
|
+
serviceId: args2.service_id
|
|
1360
|
+
};
|
|
1361
|
+
if (args2.path_prefix !== void 0) input.pathPrefix = args2.path_prefix;
|
|
1300
1362
|
const response = await ctx.hoststack.domains.add(teamId, input);
|
|
1301
1363
|
const data = { domain: shapeDomain(response.domain) };
|
|
1302
1364
|
return respond({
|
|
@@ -1397,7 +1459,7 @@ defineTool({
|
|
|
1397
1459
|
" - service_id: publicId of the service.",
|
|
1398
1460
|
' - key: env-var name (e.g. "DATABASE_URL").',
|
|
1399
1461
|
" - value: new value (will be encrypted at rest if is_secret=true).",
|
|
1400
|
-
" - is_secret (optional): true marks the value as secret (masked on read).
|
|
1462
|
+
" - is_secret (optional): true marks the value as secret (masked on read). On create, defaults to true for safety. On update, omitting it leaves the existing flag untouched \u2014 pass it explicitly only when you want to change classification.",
|
|
1401
1463
|
"",
|
|
1402
1464
|
'Returns: { envVar: EnvVar, action: "created" | "updated" }.',
|
|
1403
1465
|
"",
|
|
@@ -1411,18 +1473,16 @@ defineTool({
|
|
|
1411
1473
|
},
|
|
1412
1474
|
handler: async (args2, ctx) => {
|
|
1413
1475
|
const teamId = await ctx.resolveTeamId();
|
|
1414
|
-
const isSecret = args2.is_secret ?? true;
|
|
1415
1476
|
const existing = await ctx.hoststack.envVars.list(teamId, args2.service_id);
|
|
1416
1477
|
const match = existing.envVars.find((v) => v.key === args2.key);
|
|
1417
1478
|
if (match) {
|
|
1479
|
+
const updatePayload = { value: args2.value };
|
|
1480
|
+
if (args2.is_secret !== void 0) updatePayload.isSecret = args2.is_secret;
|
|
1418
1481
|
const response2 = await ctx.hoststack.envVars.update(
|
|
1419
1482
|
teamId,
|
|
1420
1483
|
args2.service_id,
|
|
1421
1484
|
String(match.id),
|
|
1422
|
-
|
|
1423
|
-
value: args2.value,
|
|
1424
|
-
isSecret
|
|
1425
|
-
}
|
|
1485
|
+
updatePayload
|
|
1426
1486
|
);
|
|
1427
1487
|
const data2 = {
|
|
1428
1488
|
envVar: shapeEnvVar(response2.envVar),
|
|
@@ -1433,7 +1493,7 @@ defineTool({
|
|
|
1433
1493
|
const response = await ctx.hoststack.envVars.create(teamId, args2.service_id, {
|
|
1434
1494
|
key: args2.key,
|
|
1435
1495
|
value: args2.value,
|
|
1436
|
-
isSecret
|
|
1496
|
+
isSecret: args2.is_secret ?? true
|
|
1437
1497
|
});
|
|
1438
1498
|
const data = {
|
|
1439
1499
|
envVar: shapeEnvVar(response.envVar),
|
|
@@ -1569,7 +1629,7 @@ defineTool({
|
|
|
1569
1629
|
"",
|
|
1570
1630
|
"Returns: { environment: Environment }.",
|
|
1571
1631
|
"",
|
|
1572
|
-
'Example: create_environment({ project_id: "prj_abc", name: "Staging", type: "staging" }) \u2192 { environment: { id: 7, publicId: "
|
|
1632
|
+
'Example: create_environment({ project_id: "prj_abc", name: "Staging", type: "staging" }) \u2192 { environment: { id: 7, publicId: "env_\u2026", name: "Staging", type: "staging" } }'
|
|
1573
1633
|
].join("\n"),
|
|
1574
1634
|
input: {
|
|
1575
1635
|
project_id: z10.string().describe("Project publicId."),
|
|
@@ -1606,7 +1666,7 @@ defineTool({
|
|
|
1606
1666
|
"",
|
|
1607
1667
|
"Returns: { success: true } on success.",
|
|
1608
1668
|
"",
|
|
1609
|
-
'Example: delete_environment({ project_id: "prj_abc", environment_id: "
|
|
1669
|
+
'Example: delete_environment({ project_id: "prj_abc", environment_id: "env_xyz" }) \u2192 { success: true }'
|
|
1610
1670
|
].join("\n"),
|
|
1611
1671
|
input: {
|
|
1612
1672
|
project_id: z10.string().describe("Project publicId."),
|
|
@@ -1638,7 +1698,7 @@ defineTool({
|
|
|
1638
1698
|
"",
|
|
1639
1699
|
"Returns: { deploy: Deploy } \u2014 the new deploy on the target service.",
|
|
1640
1700
|
"",
|
|
1641
|
-
'Example: promote_deploy({ service_id: "svc_staging", deploy_id: "dpl_built", target_environment_id: "
|
|
1701
|
+
'Example: promote_deploy({ service_id: "svc_staging", deploy_id: "dpl_built", target_environment_id: "env_prod" }) \u2192 { deploy: { id: 99, status: "pending", trigger: "rollback", dockerImageId: "sha256:\u2026" } }'
|
|
1642
1702
|
].join("\n"),
|
|
1643
1703
|
input: {
|
|
1644
1704
|
service_id: z10.string().describe("Source service publicId."),
|
|
@@ -2660,7 +2720,7 @@ defineTool({
|
|
|
2660
2720
|
|
|
2661
2721
|
// src/server-factory.ts
|
|
2662
2722
|
var PACKAGE_NAME = "hoststack";
|
|
2663
|
-
var PACKAGE_VERSION = "0.
|
|
2723
|
+
var PACKAGE_VERSION = "0.9.1";
|
|
2664
2724
|
function createMcpServer(options) {
|
|
2665
2725
|
const baseUrl2 = (options.baseUrl ?? "https://hoststack.dev").replace(/\/$/, "");
|
|
2666
2726
|
const hoststack = new HostStack({ apiKey: options.apiKey, baseUrl: baseUrl2 });
|
|
@@ -2725,7 +2785,7 @@ if (printIdx !== -1) {
|
|
|
2725
2785
|
}
|
|
2726
2786
|
if (target === "claude-code") {
|
|
2727
2787
|
process.stdout.write(
|
|
2728
|
-
`claude mcp add hoststack -- npx -y @hoststack.dev/mcp
|
|
2788
|
+
`claude mcp add hoststack --env HOSTSTACK_API_KEY=${apiKeyPlaceholder} -- npx -y @hoststack.dev/mcp
|
|
2729
2789
|
`
|
|
2730
2790
|
);
|
|
2731
2791
|
process.exit(0);
|