@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 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 -e HOSTSTACK_API_KEY=hs_live_...
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
- 55 tools, grouped by resource:
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` | `update_database` (use the dashboard for create/delete/credentials) |
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` |
@@ -834,13 +834,14 @@ defineTool({
834
834
  name: "trigger_deploy",
835
835
  category: "deploys",
836
836
  description: [
837
- "Kick off a new deploy from the latest commit on the service branch. Optionally clear the build cache for a clean rebuild.",
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, cache-clearing, or after a config change.',
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
- " - clear_cache (optional): boolean \u2014 discard the cached build layers. Default false.",
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
- clear_cache: z6.boolean().optional().describe("Clear the build cache (default false).")
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.clear_cache !== void 0) input.clearCache = args2.clear_cache;
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 the search for errors.",
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 (no trailing dot).'),
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 (optional): publicId of the service to bind to. If omitted, the domain is added unbound and you can attach it later with update_domain.",
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().optional().describe("Optional service publicId to bind to.")
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 = { domain: args2.hostname };
1353
- if (args2.service_id !== void 0) input.serviceId = args2.service_id;
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). Default true for safety; pass false for boring config like PORT or NODE_ENV.",
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: "environment_\u2026", name: "Staging", type: "staging" } }'
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: "environment_xyz" }) \u2192 { success: true }'
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: "environment_prod" }) \u2192 { deploy: { id: 99, status: "pending", trigger: "rollback", dockerImageId: "sha256:\u2026" } }'
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.6.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 -e HOSTSTACK_API_KEY=${apiKeyPlaceholder}
2970
+ `claude mcp add hoststack --env HOSTSTACK_API_KEY=${apiKeyPlaceholder} -- npx -y @hoststack.dev/mcp
2783
2971
  `
2784
2972
  );
2785
2973
  process.exit(0);