@hoststack.dev/mcp 0.9.0 → 0.10.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 +9 -3
- package/dist/hoststack-mcp.js +215 -27
- package/dist/hoststack-mcp.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +214 -26
- 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
|
@@ -834,13 +834,14 @@ defineTool({
|
|
|
834
834
|
name: "trigger_deploy",
|
|
835
835
|
category: "deploys",
|
|
836
836
|
description: [
|
|
837
|
-
"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.",
|
|
838
838
|
"",
|
|
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
|
|
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.',
|
|
840
840
|
"",
|
|
841
841
|
"Inputs:",
|
|
842
842
|
" - service_id: publicId of the service.",
|
|
843
|
-
" -
|
|
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.",
|
|
844
845
|
"",
|
|
845
846
|
'Returns: { deploy: Deploy } \u2014 the new deploy record. Status will start as "pending" then transition through "building" \u2192 "deploying" \u2192 "live" or "failed".',
|
|
846
847
|
"",
|
|
@@ -848,12 +849,14 @@ defineTool({
|
|
|
848
849
|
].join("\n"),
|
|
849
850
|
input: {
|
|
850
851
|
service_id: z6.string().describe("Service publicId."),
|
|
851
|
-
|
|
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).")
|
|
852
854
|
},
|
|
853
855
|
handler: async (args2, ctx) => {
|
|
854
856
|
const teamId = await ctx.resolveTeamId();
|
|
855
857
|
const input = {};
|
|
856
|
-
if (args2.
|
|
858
|
+
if (args2.commit_hash !== void 0) input.commitHash = args2.commit_hash;
|
|
859
|
+
if (args2.branch !== void 0) input.branch = args2.branch;
|
|
857
860
|
const response = await ctx.hoststack.deploys.trigger(teamId, args2.service_id, input);
|
|
858
861
|
const data = { deploy: shapeDeploy(response.deploy) };
|
|
859
862
|
const publicId = data.deploy && "publicId" in data.deploy ? data.deploy.publicId : "unknown";
|
|
@@ -896,7 +899,7 @@ defineTool({
|
|
|
896
899
|
name: "get_deploy_logs",
|
|
897
900
|
category: "deploys",
|
|
898
901
|
description: [
|
|
899
|
-
"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.",
|
|
900
903
|
"",
|
|
901
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.",
|
|
902
905
|
"",
|
|
@@ -1151,20 +1154,23 @@ defineTool({
|
|
|
1151
1154
|
"",
|
|
1152
1155
|
"Inputs:",
|
|
1153
1156
|
' - type: "A" | "AAAA" | "CNAME" | "MX" | "TXT" | "NS" | "SRV" | "CAA" | "ALIAS".',
|
|
1154
|
-
' - name: "@" for the zone apex, or a subdomain label ("www", "api", "_acme-challenge").',
|
|
1155
|
-
" - content: the literal record value. For TXT pass the unquoted string \u2014 the PowerDNS layer handles quoting.",
|
|
1157
|
+
' - name: "@" for the zone apex, or a subdomain label ("www", "api", "_acme-challenge"). Do NOT include the zone suffix \u2014 the backend prepends it. "bounce" on zone "micci.dk" publishes "bounce.micci.dk"; passing "bounce.micci.dk" works (the backend strips the suffix) but is non-idiomatic.',
|
|
1158
|
+
" - content: the literal record value. For TXT pass the unquoted string \u2014 the PowerDNS layer handles quoting. For CNAME / MX / NS / SRV / ALIAS the target may be passed with or without a trailing dot; the adapter FQDN-izes it.",
|
|
1156
1159
|
" - ttl (optional): 60\u201386400 seconds, default 3600.",
|
|
1157
1160
|
" - priority (optional, required for MX and SRV): 0\u201365535.",
|
|
1158
1161
|
"",
|
|
1159
1162
|
"Returns: { record: Record } \u2014 see get_dns_record for the shape.",
|
|
1160
1163
|
"",
|
|
1164
|
+
"Notes:",
|
|
1165
|
+
' - CNAME at the apex is rejected (RFC 1034 forbids CNAME coexisting with NS/SOA). Use type="ALIAS" for apex aliasing.',
|
|
1166
|
+
"",
|
|
1161
1167
|
'Example: create_dns_record({ domain: "micci.dk", type: "TXT", name: "@", content: "google-site-verification=C0V0scK48g2\u2026", ttl: 300 })'
|
|
1162
1168
|
].join("\n"),
|
|
1163
1169
|
input: {
|
|
1164
1170
|
zone_id: z7.string().optional().describe("Zone publicId."),
|
|
1165
1171
|
domain: z7.string().optional().describe("Apex or subdomain to resolve a hosted zone."),
|
|
1166
1172
|
type: z7.enum(DNS_RECORD_TYPES).describe("DNS record type."),
|
|
1167
|
-
name: z7.string().min(1).max(253).describe('"@" for apex, or label
|
|
1173
|
+
name: z7.string().min(1).max(253).describe('"@" for apex, or a bare label. Do NOT include the zone suffix.'),
|
|
1168
1174
|
content: z7.string().min(1).max(2e3).describe("Record value; TXT values are unquoted."),
|
|
1169
1175
|
ttl: z7.number().int().min(60).max(86400).optional().describe("TTL in seconds (default 3600)."),
|
|
1170
1176
|
priority: z7.number().int().min(0).max(65535).optional().describe("Required for MX / SRV.")
|
|
@@ -1218,7 +1224,7 @@ defineTool({
|
|
|
1218
1224
|
input: {
|
|
1219
1225
|
record_id: z7.string().describe("Record publicId."),
|
|
1220
1226
|
type: z7.enum(DNS_RECORD_TYPES).describe("DNS record type."),
|
|
1221
|
-
name: z7.string().min(1).max(253).describe('"@" for apex, or label.'),
|
|
1227
|
+
name: z7.string().min(1).max(253).describe('"@" for apex, or a bare label. Do NOT include the zone suffix.'),
|
|
1222
1228
|
content: z7.string().min(1).max(2e3).describe("Record value; TXT unquoted."),
|
|
1223
1229
|
ttl: z7.number().int().min(60).max(86400).optional().describe("TTL in seconds (default 3600)."),
|
|
1224
1230
|
priority: z7.number().int().min(0).max(65535).optional().describe("Required for MX / SRV.")
|
|
@@ -1337,7 +1343,8 @@ defineTool({
|
|
|
1337
1343
|
"",
|
|
1338
1344
|
"Inputs:",
|
|
1339
1345
|
' - hostname: the domain to add (e.g. "api.example.com").',
|
|
1340
|
-
" - service_id
|
|
1346
|
+
" - service_id: publicId of the service to bind the domain to.",
|
|
1347
|
+
" - 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).",
|
|
1341
1348
|
"",
|
|
1342
1349
|
"Returns: { domain: Domain } \u2014 includes dnsTargets you must configure (CNAME / A records).",
|
|
1343
1350
|
"",
|
|
@@ -1345,12 +1352,16 @@ defineTool({
|
|
|
1345
1352
|
].join("\n"),
|
|
1346
1353
|
input: {
|
|
1347
1354
|
hostname: z8.string().min(3).max(253).describe("Fully-qualified hostname (e.g. api.example.com)."),
|
|
1348
|
-
service_id: z8.string().
|
|
1355
|
+
service_id: z8.string().describe("Service publicId to bind to."),
|
|
1356
|
+
path_prefix: z8.string().optional().describe('Optional URL prefix for path-based routing (e.g. "/api").')
|
|
1349
1357
|
},
|
|
1350
1358
|
handler: async (args2, ctx) => {
|
|
1351
1359
|
const teamId = await ctx.resolveTeamId();
|
|
1352
|
-
const input = {
|
|
1353
|
-
|
|
1360
|
+
const input = {
|
|
1361
|
+
domain: args2.hostname,
|
|
1362
|
+
serviceId: args2.service_id
|
|
1363
|
+
};
|
|
1364
|
+
if (args2.path_prefix !== void 0) input.pathPrefix = args2.path_prefix;
|
|
1354
1365
|
const response = await ctx.hoststack.domains.add(teamId, input);
|
|
1355
1366
|
const data = { domain: shapeDomain(response.domain) };
|
|
1356
1367
|
return respond({
|
|
@@ -1451,7 +1462,7 @@ defineTool({
|
|
|
1451
1462
|
" - service_id: publicId of the service.",
|
|
1452
1463
|
' - key: env-var name (e.g. "DATABASE_URL").',
|
|
1453
1464
|
" - value: new value (will be encrypted at rest if is_secret=true).",
|
|
1454
|
-
" - is_secret (optional): true marks the value as secret (masked on read).
|
|
1465
|
+
" - 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.",
|
|
1455
1466
|
"",
|
|
1456
1467
|
'Returns: { envVar: EnvVar, action: "created" | "updated" }.',
|
|
1457
1468
|
"",
|
|
@@ -1465,18 +1476,16 @@ defineTool({
|
|
|
1465
1476
|
},
|
|
1466
1477
|
handler: async (args2, ctx) => {
|
|
1467
1478
|
const teamId = await ctx.resolveTeamId();
|
|
1468
|
-
const isSecret = args2.is_secret ?? true;
|
|
1469
1479
|
const existing = await ctx.hoststack.envVars.list(teamId, args2.service_id);
|
|
1470
1480
|
const match = existing.envVars.find((v) => v.key === args2.key);
|
|
1471
1481
|
if (match) {
|
|
1482
|
+
const updatePayload = { value: args2.value };
|
|
1483
|
+
if (args2.is_secret !== void 0) updatePayload.isSecret = args2.is_secret;
|
|
1472
1484
|
const response2 = await ctx.hoststack.envVars.update(
|
|
1473
1485
|
teamId,
|
|
1474
1486
|
args2.service_id,
|
|
1475
1487
|
String(match.id),
|
|
1476
|
-
|
|
1477
|
-
value: args2.value,
|
|
1478
|
-
isSecret
|
|
1479
|
-
}
|
|
1488
|
+
updatePayload
|
|
1480
1489
|
);
|
|
1481
1490
|
const data2 = {
|
|
1482
1491
|
envVar: shapeEnvVar(response2.envVar),
|
|
@@ -1487,7 +1496,7 @@ defineTool({
|
|
|
1487
1496
|
const response = await ctx.hoststack.envVars.create(teamId, args2.service_id, {
|
|
1488
1497
|
key: args2.key,
|
|
1489
1498
|
value: args2.value,
|
|
1490
|
-
isSecret
|
|
1499
|
+
isSecret: args2.is_secret ?? true
|
|
1491
1500
|
});
|
|
1492
1501
|
const data = {
|
|
1493
1502
|
envVar: shapeEnvVar(response.envVar),
|
|
@@ -1623,7 +1632,7 @@ defineTool({
|
|
|
1623
1632
|
"",
|
|
1624
1633
|
"Returns: { environment: Environment }.",
|
|
1625
1634
|
"",
|
|
1626
|
-
'Example: create_environment({ project_id: "prj_abc", name: "Staging", type: "staging" }) \u2192 { environment: { id: 7, publicId: "
|
|
1635
|
+
'Example: create_environment({ project_id: "prj_abc", name: "Staging", type: "staging" }) \u2192 { environment: { id: 7, publicId: "env_\u2026", name: "Staging", type: "staging" } }'
|
|
1627
1636
|
].join("\n"),
|
|
1628
1637
|
input: {
|
|
1629
1638
|
project_id: z10.string().describe("Project publicId."),
|
|
@@ -1660,7 +1669,7 @@ defineTool({
|
|
|
1660
1669
|
"",
|
|
1661
1670
|
"Returns: { success: true } on success.",
|
|
1662
1671
|
"",
|
|
1663
|
-
'Example: delete_environment({ project_id: "prj_abc", environment_id: "
|
|
1672
|
+
'Example: delete_environment({ project_id: "prj_abc", environment_id: "env_xyz" }) \u2192 { success: true }'
|
|
1664
1673
|
].join("\n"),
|
|
1665
1674
|
input: {
|
|
1666
1675
|
project_id: z10.string().describe("Project publicId."),
|
|
@@ -1692,7 +1701,7 @@ defineTool({
|
|
|
1692
1701
|
"",
|
|
1693
1702
|
"Returns: { deploy: Deploy } \u2014 the new deploy on the target service.",
|
|
1694
1703
|
"",
|
|
1695
|
-
'Example: promote_deploy({ service_id: "svc_staging", deploy_id: "dpl_built", target_environment_id: "
|
|
1704
|
+
'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" } }'
|
|
1696
1705
|
].join("\n"),
|
|
1697
1706
|
input: {
|
|
1698
1707
|
service_id: z10.string().describe("Source service publicId."),
|
|
@@ -2052,6 +2061,24 @@ defineTool({
|
|
|
2052
2061
|
|
|
2053
2062
|
// src/tools/services.ts
|
|
2054
2063
|
import { z as z13 } from "zod";
|
|
2064
|
+
var DEV_ENV_IMAGE = "hoststack/dev-env:claude";
|
|
2065
|
+
var DEV_ENV_VOLUME = { name: "workspace", mountPath: "/workspace", sizeGb: 10 };
|
|
2066
|
+
var SERVICE_TYPES = [
|
|
2067
|
+
"web_service",
|
|
2068
|
+
"private_service",
|
|
2069
|
+
"worker",
|
|
2070
|
+
"cron_job",
|
|
2071
|
+
"static_site"
|
|
2072
|
+
];
|
|
2073
|
+
var SERVICE_PLANS = [
|
|
2074
|
+
"pico",
|
|
2075
|
+
"nano",
|
|
2076
|
+
"micro",
|
|
2077
|
+
"starter",
|
|
2078
|
+
"standard",
|
|
2079
|
+
"pro_standard",
|
|
2080
|
+
"pro_large"
|
|
2081
|
+
];
|
|
2055
2082
|
defineTool({
|
|
2056
2083
|
name: "list_services",
|
|
2057
2084
|
category: "services",
|
|
@@ -2103,10 +2130,171 @@ defineTool({
|
|
|
2103
2130
|
args2.status ? `status=${args2.status}` : null,
|
|
2104
2131
|
args2.type ? `type=${args2.type}` : null
|
|
2105
2132
|
].filter(Boolean).join(", ");
|
|
2106
|
-
const summary = data.items.length === 0 ? filterDesc ? `No services match the given filters (${filterDesc}).` : "No services yet \u2014 create one with create_service or via the dashboard." : `Found ${data.items.length} service${data.items.length === 1 ? "" : "s"}${filterDesc ? ` matching ${filterDesc}` : ""}.`;
|
|
2133
|
+
const summary = data.items.length === 0 ? filterDesc ? `No services match the given filters (${filterDesc}).` : "No services yet \u2014 create one with create_service (or create_dev_environment for an AI dev box), or via the dashboard." : `Found ${data.items.length} service${data.items.length === 1 ? "" : "s"}${filterDesc ? ` matching ${filterDesc}` : ""}.`;
|
|
2107
2134
|
return respond({ summary, data });
|
|
2108
2135
|
}
|
|
2109
2136
|
});
|
|
2137
|
+
defineTool({
|
|
2138
|
+
name: "create_service",
|
|
2139
|
+
category: "services",
|
|
2140
|
+
description: [
|
|
2141
|
+
"Create a new service in a project. Source is either a connected git repo (github_repo_id) OR a pre-built docker_image \u2014 never both.",
|
|
2142
|
+
"",
|
|
2143
|
+
"When to use: the user wants to deploy something new. For a one-command AI dev environment specifically, prefer create_dev_environment (it also attaches the /workspace volume and sets the MCP keys).",
|
|
2144
|
+
"",
|
|
2145
|
+
"Inputs:",
|
|
2146
|
+
' - project_id: numeric id or publicId ("prj_\u2026") of the target project.',
|
|
2147
|
+
" - name: service name (1\u2013100 chars).",
|
|
2148
|
+
' - type: "web_service" | "private_service" | "worker" | "cron_job" | "static_site".',
|
|
2149
|
+
" - docker_image (optional): pre-built image ref to deploy instead of building from source.",
|
|
2150
|
+
" - github_repo_id (optional): connect a previously-linked GitHub repo by numeric id.",
|
|
2151
|
+
' - branch (optional): git branch (default "main").',
|
|
2152
|
+
" - install_command / build_command / start_command (optional): build/run shell commands. start_command is required for web/private services without a docker_image.",
|
|
2153
|
+
" - cron_schedule (optional): cron expression \u2014 required for cron_job.",
|
|
2154
|
+
' - publish_path (optional): static-site output dir (e.g. "dist").',
|
|
2155
|
+
' - runtime (optional): "node" | "bun" | "python" | \u2026 (auto-detected from a repo when omitted).',
|
|
2156
|
+
' - plan (optional): service size (default "micro").',
|
|
2157
|
+
" - environment_id (optional): bind to a specific environment; defaults to the project Production env.",
|
|
2158
|
+
" - auto_deploy (optional, default true): trigger the first deploy immediately when a source is present.",
|
|
2159
|
+
"",
|
|
2160
|
+
"Returns: { service: Service, deployId: number | null }.",
|
|
2161
|
+
"",
|
|
2162
|
+
'Example: create_service({ project_id: "prj_abc", name: "api", type: "web_service", github_repo_id: 42 }) \u2192 { service: { publicId: "svc_\u2026" }, deployId: 1234 }'
|
|
2163
|
+
].join("\n"),
|
|
2164
|
+
input: {
|
|
2165
|
+
project_id: z13.union([z13.number().int().positive(), z13.string()]).describe("Target project \u2014 numeric id or publicId."),
|
|
2166
|
+
name: z13.string().min(1).max(100).describe("Service name (1\u2013100 chars)."),
|
|
2167
|
+
type: z13.enum(SERVICE_TYPES).describe("Service type."),
|
|
2168
|
+
docker_image: z13.string().max(500).optional().describe("Pre-built image ref. Mutually exclusive with github_repo_id."),
|
|
2169
|
+
github_repo_id: z13.number().int().positive().optional().describe("Linked GitHub repo numeric id. Mutually exclusive with docker_image."),
|
|
2170
|
+
branch: z13.string().max(200).optional().describe('Git branch (default "main").'),
|
|
2171
|
+
install_command: z13.string().max(1e3).optional().describe("Install shell command."),
|
|
2172
|
+
build_command: z13.string().max(1e3).optional().describe("Build shell command."),
|
|
2173
|
+
start_command: z13.string().max(1e3).optional().describe("Start shell command (required for web/private services without an image)."),
|
|
2174
|
+
cron_schedule: z13.string().max(100).optional().describe("Cron expression \u2014 required for cron_job."),
|
|
2175
|
+
publish_path: z13.string().max(500).optional().describe("Static-site output dir."),
|
|
2176
|
+
runtime: z13.string().max(50).optional().describe("Runtime hint (node/bun/python/\u2026)."),
|
|
2177
|
+
plan: z13.enum(SERVICE_PLANS).optional().describe('Service size (default "micro").'),
|
|
2178
|
+
environment_id: z13.union([z13.number().int().positive(), z13.string()]).optional().describe("Bind to a specific environment; defaults to Production."),
|
|
2179
|
+
auto_deploy: z13.boolean().optional().describe("Trigger the first deploy immediately (default true).")
|
|
2180
|
+
},
|
|
2181
|
+
handler: async (args2, ctx) => {
|
|
2182
|
+
const teamId = await ctx.resolveTeamId();
|
|
2183
|
+
const projectId = await ctx.hoststack.resolveId(args2.project_id, {
|
|
2184
|
+
kind: "project",
|
|
2185
|
+
teamId
|
|
2186
|
+
});
|
|
2187
|
+
const input = {
|
|
2188
|
+
name: args2.name,
|
|
2189
|
+
type: args2.type,
|
|
2190
|
+
projectId
|
|
2191
|
+
};
|
|
2192
|
+
if (args2.docker_image !== void 0) input.dockerImage = args2.docker_image;
|
|
2193
|
+
if (args2.github_repo_id !== void 0) input.githubRepoId = args2.github_repo_id;
|
|
2194
|
+
if (args2.branch !== void 0) input.branch = args2.branch;
|
|
2195
|
+
if (args2.install_command !== void 0) input.installCommand = args2.install_command;
|
|
2196
|
+
if (args2.build_command !== void 0) input.buildCommand = args2.build_command;
|
|
2197
|
+
if (args2.start_command !== void 0) input.startCommand = args2.start_command;
|
|
2198
|
+
if (args2.cron_schedule !== void 0) input.cronSchedule = args2.cron_schedule;
|
|
2199
|
+
if (args2.publish_path !== void 0) input.publishPath = args2.publish_path;
|
|
2200
|
+
if (args2.runtime !== void 0) input.runtime = args2.runtime;
|
|
2201
|
+
if (args2.plan !== void 0) input.plan = args2.plan;
|
|
2202
|
+
if (args2.auto_deploy !== void 0) input.autoDeploy = args2.auto_deploy;
|
|
2203
|
+
if (args2.environment_id !== void 0) {
|
|
2204
|
+
input.environmentId = await ctx.hoststack.resolveId(args2.environment_id, {
|
|
2205
|
+
kind: "environment",
|
|
2206
|
+
teamId
|
|
2207
|
+
});
|
|
2208
|
+
}
|
|
2209
|
+
const response = await ctx.hoststack.services.create(teamId, input);
|
|
2210
|
+
const data = {
|
|
2211
|
+
service: shapeService(response.service),
|
|
2212
|
+
deployId: response.deployId ?? null
|
|
2213
|
+
};
|
|
2214
|
+
const publicId = data.service && "publicId" in data.service ? data.service.publicId : "unknown";
|
|
2215
|
+
return respond({ summary: `Created service "${args2.name}" (${publicId}).`, data });
|
|
2216
|
+
}
|
|
2217
|
+
});
|
|
2218
|
+
defineTool({
|
|
2219
|
+
name: "create_dev_environment",
|
|
2220
|
+
category: "services",
|
|
2221
|
+
description: [
|
|
2222
|
+
"Spin up an AI dev environment in one call: a private service from the agentic dev-env image (Claude Code + Codex + OpenCode + MCPs preinstalled), with a persistent /workspace volume and the MCP API keys set, then fire the first deploy.",
|
|
2223
|
+
"",
|
|
2224
|
+
`When to use: the user wants a cloud terminal / dev box they can drive coding agents in (from a desk or a phone). This mirrors the dashboard's "AI Dev Environment" wizard preset \u2014 create (deploy deferred) \u2192 set keys \u2192 attach /workspace \u2192 deploy, so the first container boots with its volume and keys already in place.`,
|
|
2225
|
+
"",
|
|
2226
|
+
"Inputs:",
|
|
2227
|
+
' - project_id: numeric id or publicId ("prj_\u2026") of the target project.',
|
|
2228
|
+
' - name (optional): service name (default "dev-environment").',
|
|
2229
|
+
' - plan (optional): service size (default "micro").',
|
|
2230
|
+
" - disk_gb (optional): /workspace volume size in GB (default 10, 1\u2013100).",
|
|
2231
|
+
" - hoststack_api_key (optional): sets HOSTSTACK_API_KEY so the hoststack MCP works inside the container.",
|
|
2232
|
+
" - poststack_api_key (optional): sets POSTSTACK_API_KEY so the poststack MCP works inside the container.",
|
|
2233
|
+
"",
|
|
2234
|
+
"Returns: { service: Service, volumeAttached: boolean, deployId: number | null }. Open the Terminal tab on the service (dashboard or phone) once it is running; log in once with `claude /login` inside the container.",
|
|
2235
|
+
"",
|
|
2236
|
+
'Example: create_dev_environment({ project_id: "prj_abc", name: "scratch", hoststack_api_key: "hs_live_\u2026" })'
|
|
2237
|
+
].join("\n"),
|
|
2238
|
+
input: {
|
|
2239
|
+
project_id: z13.union([z13.number().int().positive(), z13.string()]).describe("Target project \u2014 numeric id or publicId."),
|
|
2240
|
+
name: z13.string().min(1).max(100).optional().describe('Service name (default "dev-environment").'),
|
|
2241
|
+
plan: z13.enum(SERVICE_PLANS).optional().describe('Service size (default "micro").'),
|
|
2242
|
+
disk_gb: z13.number().int().min(1).max(100).optional().describe("/workspace volume size in GB (default 10)."),
|
|
2243
|
+
hoststack_api_key: z13.string().optional().describe("Value for HOSTSTACK_API_KEY (enables the hoststack MCP in-container)."),
|
|
2244
|
+
poststack_api_key: z13.string().optional().describe("Value for POSTSTACK_API_KEY (enables the poststack MCP in-container).")
|
|
2245
|
+
},
|
|
2246
|
+
handler: async (args2, ctx) => {
|
|
2247
|
+
const teamId = await ctx.resolveTeamId();
|
|
2248
|
+
const projectId = await ctx.hoststack.resolveId(args2.project_id, {
|
|
2249
|
+
kind: "project",
|
|
2250
|
+
teamId
|
|
2251
|
+
});
|
|
2252
|
+
const name = args2.name ?? "dev-environment";
|
|
2253
|
+
const sizeGb = args2.disk_gb ?? DEV_ENV_VOLUME.sizeGb;
|
|
2254
|
+
const createInput = {
|
|
2255
|
+
name,
|
|
2256
|
+
type: "private_service",
|
|
2257
|
+
projectId,
|
|
2258
|
+
dockerImage: DEV_ENV_IMAGE,
|
|
2259
|
+
autoDeploy: false
|
|
2260
|
+
};
|
|
2261
|
+
if (args2.plan !== void 0) createInput.plan = args2.plan;
|
|
2262
|
+
const created = await ctx.hoststack.services.create(teamId, createInput);
|
|
2263
|
+
const service = created.service;
|
|
2264
|
+
const envVars = [];
|
|
2265
|
+
if (args2.hoststack_api_key)
|
|
2266
|
+
envVars.push({
|
|
2267
|
+
key: "HOSTSTACK_API_KEY",
|
|
2268
|
+
value: args2.hoststack_api_key,
|
|
2269
|
+
isSecret: true
|
|
2270
|
+
});
|
|
2271
|
+
if (args2.poststack_api_key)
|
|
2272
|
+
envVars.push({
|
|
2273
|
+
key: "POSTSTACK_API_KEY",
|
|
2274
|
+
value: args2.poststack_api_key,
|
|
2275
|
+
isSecret: true
|
|
2276
|
+
});
|
|
2277
|
+
if (envVars.length > 0) {
|
|
2278
|
+
await ctx.hoststack.envVars.bulkSet(teamId, service.id, { vars: envVars });
|
|
2279
|
+
}
|
|
2280
|
+
let volumeAttached = false;
|
|
2281
|
+
try {
|
|
2282
|
+
await ctx.hoststack.volumes.create(teamId, service.id, {
|
|
2283
|
+
name: DEV_ENV_VOLUME.name,
|
|
2284
|
+
mountPath: DEV_ENV_VOLUME.mountPath,
|
|
2285
|
+
sizeGb
|
|
2286
|
+
});
|
|
2287
|
+
volumeAttached = true;
|
|
2288
|
+
} catch {
|
|
2289
|
+
}
|
|
2290
|
+
const deploy = await ctx.hoststack.deploys.trigger(teamId, service.id);
|
|
2291
|
+
const deployId = deploy.deploy?.id ?? null;
|
|
2292
|
+
return respond({
|
|
2293
|
+
summary: `Created AI dev environment "${name}" (${service.publicId})${volumeAttached ? " with a /workspace volume" : ""} \u2014 deploying. Open its Terminal tab once running.`,
|
|
2294
|
+
data: { service: shapeService(service), volumeAttached, deployId }
|
|
2295
|
+
});
|
|
2296
|
+
}
|
|
2297
|
+
});
|
|
2110
2298
|
defineTool({
|
|
2111
2299
|
name: "get_service",
|
|
2112
2300
|
category: "services",
|
|
@@ -2714,7 +2902,7 @@ defineTool({
|
|
|
2714
2902
|
|
|
2715
2903
|
// src/server-factory.ts
|
|
2716
2904
|
var PACKAGE_NAME = "hoststack";
|
|
2717
|
-
var PACKAGE_VERSION = "0.
|
|
2905
|
+
var PACKAGE_VERSION = "0.9.1";
|
|
2718
2906
|
function createMcpServer(options) {
|
|
2719
2907
|
const baseUrl2 = (options.baseUrl ?? "https://hoststack.dev").replace(/\/$/, "");
|
|
2720
2908
|
const hoststack = new HostStack({ apiKey: options.apiKey, baseUrl: baseUrl2 });
|
|
@@ -2779,7 +2967,7 @@ if (printIdx !== -1) {
|
|
|
2779
2967
|
}
|
|
2780
2968
|
if (target === "claude-code") {
|
|
2781
2969
|
process.stdout.write(
|
|
2782
|
-
`claude mcp add hoststack -- npx -y @hoststack.dev/mcp
|
|
2970
|
+
`claude mcp add hoststack --env HOSTSTACK_API_KEY=${apiKeyPlaceholder} -- npx -y @hoststack.dev/mcp
|
|
2783
2971
|
`
|
|
2784
2972
|
);
|
|
2785
2973
|
process.exit(0);
|