@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/dist/index.d.ts CHANGED
@@ -83,7 +83,7 @@ interface CreateServerOptions {
83
83
  onToolCall?: ToolCallSink;
84
84
  }
85
85
  declare const PACKAGE_NAME = "hoststack";
86
- declare const PACKAGE_VERSION = "0.6.0";
86
+ declare const PACKAGE_VERSION = "0.9.1";
87
87
  /**
88
88
  * Build a fully-wired MCP server. Used by both the stdio CLI entry-point and
89
89
  * the HTTP transport mounted by the API server. A new server (and its
package/dist/index.js CHANGED
@@ -838,13 +838,14 @@ defineTool({
838
838
  name: "trigger_deploy",
839
839
  category: "deploys",
840
840
  description: [
841
- "Kick off a new deploy from the latest commit on the service branch. Optionally clear the build cache for a clean rebuild.",
841
+ "Kick off a new deploy. Defaults to the latest commit on the service branch; pass commit_hash or branch to deploy something else.",
842
842
  "",
843
- '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.',
843
+ '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.',
844
844
  "",
845
845
  "Inputs:",
846
846
  " - service_id: publicId of the service.",
847
- " - clear_cache (optional): boolean \u2014 discard the cached build layers. Default false.",
847
+ " - commit_hash (optional): build a specific commit instead of HEAD on the configured branch.",
848
+ " - branch (optional): build the tip of a non-default branch.",
848
849
  "",
849
850
  'Returns: { deploy: Deploy } \u2014 the new deploy record. Status will start as "pending" then transition through "building" \u2192 "deploying" \u2192 "live" or "failed".',
850
851
  "",
@@ -852,12 +853,14 @@ defineTool({
852
853
  ].join("\n"),
853
854
  input: {
854
855
  service_id: z6.string().describe("Service publicId."),
855
- clear_cache: z6.boolean().optional().describe("Clear the build cache (default false).")
856
+ commit_hash: z6.string().max(40).optional().describe("Specific commit to build (default: branch HEAD)."),
857
+ branch: z6.string().max(200).optional().describe("Branch to build (default: service configured branch).")
856
858
  },
857
859
  handler: async (args, ctx) => {
858
860
  const teamId = await ctx.resolveTeamId();
859
861
  const input = {};
860
- if (args.clear_cache !== void 0) input.clearCache = args.clear_cache;
862
+ if (args.commit_hash !== void 0) input.commitHash = args.commit_hash;
863
+ if (args.branch !== void 0) input.branch = args.branch;
861
864
  const response = await ctx.hoststack.deploys.trigger(teamId, args.service_id, input);
862
865
  const data = { deploy: shapeDeploy(response.deploy) };
863
866
  const publicId = data.deploy && "publicId" in data.deploy ? data.deploy.publicId : "unknown";
@@ -900,7 +903,7 @@ defineTool({
900
903
  name: "get_deploy_logs",
901
904
  category: "deploys",
902
905
  description: [
903
- "Fetch the build/deploy logs for a single deploy. Returns the entire log buffer the agent can the search for errors.",
906
+ "Fetch the build/deploy logs for a single deploy. Returns the entire log buffer the agent can then search for errors.",
904
907
  "",
905
908
  "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.",
906
909
  "",
@@ -1155,20 +1158,23 @@ defineTool({
1155
1158
  "",
1156
1159
  "Inputs:",
1157
1160
  ' - type: "A" | "AAAA" | "CNAME" | "MX" | "TXT" | "NS" | "SRV" | "CAA" | "ALIAS".',
1158
- ' - name: "@" for the zone apex, or a subdomain label ("www", "api", "_acme-challenge").',
1159
- " - content: the literal record value. For TXT pass the unquoted string \u2014 the PowerDNS layer handles quoting.",
1161
+ ' - 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.',
1162
+ " - 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.",
1160
1163
  " - ttl (optional): 60\u201386400 seconds, default 3600.",
1161
1164
  " - priority (optional, required for MX and SRV): 0\u201365535.",
1162
1165
  "",
1163
1166
  "Returns: { record: Record } \u2014 see get_dns_record for the shape.",
1164
1167
  "",
1168
+ "Notes:",
1169
+ ' - CNAME at the apex is rejected (RFC 1034 forbids CNAME coexisting with NS/SOA). Use type="ALIAS" for apex aliasing.',
1170
+ "",
1165
1171
  'Example: create_dns_record({ domain: "micci.dk", type: "TXT", name: "@", content: "google-site-verification=C0V0scK48g2\u2026", ttl: 300 })'
1166
1172
  ].join("\n"),
1167
1173
  input: {
1168
1174
  zone_id: z7.string().optional().describe("Zone publicId."),
1169
1175
  domain: z7.string().optional().describe("Apex or subdomain to resolve a hosted zone."),
1170
1176
  type: z7.enum(DNS_RECORD_TYPES).describe("DNS record type."),
1171
- name: z7.string().min(1).max(253).describe('"@" for apex, or label (no trailing dot).'),
1177
+ name: z7.string().min(1).max(253).describe('"@" for apex, or a bare label. Do NOT include the zone suffix.'),
1172
1178
  content: z7.string().min(1).max(2e3).describe("Record value; TXT values are unquoted."),
1173
1179
  ttl: z7.number().int().min(60).max(86400).optional().describe("TTL in seconds (default 3600)."),
1174
1180
  priority: z7.number().int().min(0).max(65535).optional().describe("Required for MX / SRV.")
@@ -1222,7 +1228,7 @@ defineTool({
1222
1228
  input: {
1223
1229
  record_id: z7.string().describe("Record publicId."),
1224
1230
  type: z7.enum(DNS_RECORD_TYPES).describe("DNS record type."),
1225
- name: z7.string().min(1).max(253).describe('"@" for apex, or label.'),
1231
+ name: z7.string().min(1).max(253).describe('"@" for apex, or a bare label. Do NOT include the zone suffix.'),
1226
1232
  content: z7.string().min(1).max(2e3).describe("Record value; TXT unquoted."),
1227
1233
  ttl: z7.number().int().min(60).max(86400).optional().describe("TTL in seconds (default 3600)."),
1228
1234
  priority: z7.number().int().min(0).max(65535).optional().describe("Required for MX / SRV.")
@@ -1341,7 +1347,8 @@ defineTool({
1341
1347
  "",
1342
1348
  "Inputs:",
1343
1349
  ' - hostname: the domain to add (e.g. "api.example.com").',
1344
- " - 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.",
1350
+ " - service_id: publicId of the service to bind the domain to.",
1351
+ " - 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).",
1345
1352
  "",
1346
1353
  "Returns: { domain: Domain } \u2014 includes dnsTargets you must configure (CNAME / A records).",
1347
1354
  "",
@@ -1349,12 +1356,16 @@ defineTool({
1349
1356
  ].join("\n"),
1350
1357
  input: {
1351
1358
  hostname: z8.string().min(3).max(253).describe("Fully-qualified hostname (e.g. api.example.com)."),
1352
- service_id: z8.string().optional().describe("Optional service publicId to bind to.")
1359
+ service_id: z8.string().describe("Service publicId to bind to."),
1360
+ path_prefix: z8.string().optional().describe('Optional URL prefix for path-based routing (e.g. "/api").')
1353
1361
  },
1354
1362
  handler: async (args, ctx) => {
1355
1363
  const teamId = await ctx.resolveTeamId();
1356
- const input = { domain: args.hostname };
1357
- if (args.service_id !== void 0) input.serviceId = args.service_id;
1364
+ const input = {
1365
+ domain: args.hostname,
1366
+ serviceId: args.service_id
1367
+ };
1368
+ if (args.path_prefix !== void 0) input.pathPrefix = args.path_prefix;
1358
1369
  const response = await ctx.hoststack.domains.add(teamId, input);
1359
1370
  const data = { domain: shapeDomain(response.domain) };
1360
1371
  return respond({
@@ -1455,7 +1466,7 @@ defineTool({
1455
1466
  " - service_id: publicId of the service.",
1456
1467
  ' - key: env-var name (e.g. "DATABASE_URL").',
1457
1468
  " - value: new value (will be encrypted at rest if is_secret=true).",
1458
- " - 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.",
1469
+ " - 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.",
1459
1470
  "",
1460
1471
  'Returns: { envVar: EnvVar, action: "created" | "updated" }.',
1461
1472
  "",
@@ -1469,18 +1480,16 @@ defineTool({
1469
1480
  },
1470
1481
  handler: async (args, ctx) => {
1471
1482
  const teamId = await ctx.resolveTeamId();
1472
- const isSecret = args.is_secret ?? true;
1473
1483
  const existing = await ctx.hoststack.envVars.list(teamId, args.service_id);
1474
1484
  const match = existing.envVars.find((v) => v.key === args.key);
1475
1485
  if (match) {
1486
+ const updatePayload = { value: args.value };
1487
+ if (args.is_secret !== void 0) updatePayload.isSecret = args.is_secret;
1476
1488
  const response2 = await ctx.hoststack.envVars.update(
1477
1489
  teamId,
1478
1490
  args.service_id,
1479
1491
  String(match.id),
1480
- {
1481
- value: args.value,
1482
- isSecret
1483
- }
1492
+ updatePayload
1484
1493
  );
1485
1494
  const data2 = {
1486
1495
  envVar: shapeEnvVar(response2.envVar),
@@ -1491,7 +1500,7 @@ defineTool({
1491
1500
  const response = await ctx.hoststack.envVars.create(teamId, args.service_id, {
1492
1501
  key: args.key,
1493
1502
  value: args.value,
1494
- isSecret
1503
+ isSecret: args.is_secret ?? true
1495
1504
  });
1496
1505
  const data = {
1497
1506
  envVar: shapeEnvVar(response.envVar),
@@ -1627,7 +1636,7 @@ defineTool({
1627
1636
  "",
1628
1637
  "Returns: { environment: Environment }.",
1629
1638
  "",
1630
- 'Example: create_environment({ project_id: "prj_abc", name: "Staging", type: "staging" }) \u2192 { environment: { id: 7, publicId: "environment_\u2026", name: "Staging", type: "staging" } }'
1639
+ 'Example: create_environment({ project_id: "prj_abc", name: "Staging", type: "staging" }) \u2192 { environment: { id: 7, publicId: "env_\u2026", name: "Staging", type: "staging" } }'
1631
1640
  ].join("\n"),
1632
1641
  input: {
1633
1642
  project_id: z10.string().describe("Project publicId."),
@@ -1664,7 +1673,7 @@ defineTool({
1664
1673
  "",
1665
1674
  "Returns: { success: true } on success.",
1666
1675
  "",
1667
- 'Example: delete_environment({ project_id: "prj_abc", environment_id: "environment_xyz" }) \u2192 { success: true }'
1676
+ 'Example: delete_environment({ project_id: "prj_abc", environment_id: "env_xyz" }) \u2192 { success: true }'
1668
1677
  ].join("\n"),
1669
1678
  input: {
1670
1679
  project_id: z10.string().describe("Project publicId."),
@@ -1696,7 +1705,7 @@ defineTool({
1696
1705
  "",
1697
1706
  "Returns: { deploy: Deploy } \u2014 the new deploy on the target service.",
1698
1707
  "",
1699
- '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" } }'
1708
+ '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" } }'
1700
1709
  ].join("\n"),
1701
1710
  input: {
1702
1711
  service_id: z10.string().describe("Source service publicId."),
@@ -2056,6 +2065,24 @@ defineTool({
2056
2065
 
2057
2066
  // src/tools/services.ts
2058
2067
  import { z as z13 } from "zod";
2068
+ var DEV_ENV_IMAGE = "hoststack/dev-env:claude";
2069
+ var DEV_ENV_VOLUME = { name: "workspace", mountPath: "/workspace", sizeGb: 10 };
2070
+ var SERVICE_TYPES = [
2071
+ "web_service",
2072
+ "private_service",
2073
+ "worker",
2074
+ "cron_job",
2075
+ "static_site"
2076
+ ];
2077
+ var SERVICE_PLANS = [
2078
+ "pico",
2079
+ "nano",
2080
+ "micro",
2081
+ "starter",
2082
+ "standard",
2083
+ "pro_standard",
2084
+ "pro_large"
2085
+ ];
2059
2086
  defineTool({
2060
2087
  name: "list_services",
2061
2088
  category: "services",
@@ -2107,10 +2134,171 @@ defineTool({
2107
2134
  args.status ? `status=${args.status}` : null,
2108
2135
  args.type ? `type=${args.type}` : null
2109
2136
  ].filter(Boolean).join(", ");
2110
- 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}` : ""}.`;
2137
+ 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}` : ""}.`;
2111
2138
  return respond({ summary, data });
2112
2139
  }
2113
2140
  });
2141
+ defineTool({
2142
+ name: "create_service",
2143
+ category: "services",
2144
+ description: [
2145
+ "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.",
2146
+ "",
2147
+ "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).",
2148
+ "",
2149
+ "Inputs:",
2150
+ ' - project_id: numeric id or publicId ("prj_\u2026") of the target project.',
2151
+ " - name: service name (1\u2013100 chars).",
2152
+ ' - type: "web_service" | "private_service" | "worker" | "cron_job" | "static_site".',
2153
+ " - docker_image (optional): pre-built image ref to deploy instead of building from source.",
2154
+ " - github_repo_id (optional): connect a previously-linked GitHub repo by numeric id.",
2155
+ ' - branch (optional): git branch (default "main").',
2156
+ " - install_command / build_command / start_command (optional): build/run shell commands. start_command is required for web/private services without a docker_image.",
2157
+ " - cron_schedule (optional): cron expression \u2014 required for cron_job.",
2158
+ ' - publish_path (optional): static-site output dir (e.g. "dist").',
2159
+ ' - runtime (optional): "node" | "bun" | "python" | \u2026 (auto-detected from a repo when omitted).',
2160
+ ' - plan (optional): service size (default "micro").',
2161
+ " - environment_id (optional): bind to a specific environment; defaults to the project Production env.",
2162
+ " - auto_deploy (optional, default true): trigger the first deploy immediately when a source is present.",
2163
+ "",
2164
+ "Returns: { service: Service, deployId: number | null }.",
2165
+ "",
2166
+ 'Example: create_service({ project_id: "prj_abc", name: "api", type: "web_service", github_repo_id: 42 }) \u2192 { service: { publicId: "svc_\u2026" }, deployId: 1234 }'
2167
+ ].join("\n"),
2168
+ input: {
2169
+ project_id: z13.union([z13.number().int().positive(), z13.string()]).describe("Target project \u2014 numeric id or publicId."),
2170
+ name: z13.string().min(1).max(100).describe("Service name (1\u2013100 chars)."),
2171
+ type: z13.enum(SERVICE_TYPES).describe("Service type."),
2172
+ docker_image: z13.string().max(500).optional().describe("Pre-built image ref. Mutually exclusive with github_repo_id."),
2173
+ github_repo_id: z13.number().int().positive().optional().describe("Linked GitHub repo numeric id. Mutually exclusive with docker_image."),
2174
+ branch: z13.string().max(200).optional().describe('Git branch (default "main").'),
2175
+ install_command: z13.string().max(1e3).optional().describe("Install shell command."),
2176
+ build_command: z13.string().max(1e3).optional().describe("Build shell command."),
2177
+ start_command: z13.string().max(1e3).optional().describe("Start shell command (required for web/private services without an image)."),
2178
+ cron_schedule: z13.string().max(100).optional().describe("Cron expression \u2014 required for cron_job."),
2179
+ publish_path: z13.string().max(500).optional().describe("Static-site output dir."),
2180
+ runtime: z13.string().max(50).optional().describe("Runtime hint (node/bun/python/\u2026)."),
2181
+ plan: z13.enum(SERVICE_PLANS).optional().describe('Service size (default "micro").'),
2182
+ environment_id: z13.union([z13.number().int().positive(), z13.string()]).optional().describe("Bind to a specific environment; defaults to Production."),
2183
+ auto_deploy: z13.boolean().optional().describe("Trigger the first deploy immediately (default true).")
2184
+ },
2185
+ handler: async (args, ctx) => {
2186
+ const teamId = await ctx.resolveTeamId();
2187
+ const projectId = await ctx.hoststack.resolveId(args.project_id, {
2188
+ kind: "project",
2189
+ teamId
2190
+ });
2191
+ const input = {
2192
+ name: args.name,
2193
+ type: args.type,
2194
+ projectId
2195
+ };
2196
+ if (args.docker_image !== void 0) input.dockerImage = args.docker_image;
2197
+ if (args.github_repo_id !== void 0) input.githubRepoId = args.github_repo_id;
2198
+ if (args.branch !== void 0) input.branch = args.branch;
2199
+ if (args.install_command !== void 0) input.installCommand = args.install_command;
2200
+ if (args.build_command !== void 0) input.buildCommand = args.build_command;
2201
+ if (args.start_command !== void 0) input.startCommand = args.start_command;
2202
+ if (args.cron_schedule !== void 0) input.cronSchedule = args.cron_schedule;
2203
+ if (args.publish_path !== void 0) input.publishPath = args.publish_path;
2204
+ if (args.runtime !== void 0) input.runtime = args.runtime;
2205
+ if (args.plan !== void 0) input.plan = args.plan;
2206
+ if (args.auto_deploy !== void 0) input.autoDeploy = args.auto_deploy;
2207
+ if (args.environment_id !== void 0) {
2208
+ input.environmentId = await ctx.hoststack.resolveId(args.environment_id, {
2209
+ kind: "environment",
2210
+ teamId
2211
+ });
2212
+ }
2213
+ const response = await ctx.hoststack.services.create(teamId, input);
2214
+ const data = {
2215
+ service: shapeService(response.service),
2216
+ deployId: response.deployId ?? null
2217
+ };
2218
+ const publicId = data.service && "publicId" in data.service ? data.service.publicId : "unknown";
2219
+ return respond({ summary: `Created service "${args.name}" (${publicId}).`, data });
2220
+ }
2221
+ });
2222
+ defineTool({
2223
+ name: "create_dev_environment",
2224
+ category: "services",
2225
+ description: [
2226
+ "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.",
2227
+ "",
2228
+ `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.`,
2229
+ "",
2230
+ "Inputs:",
2231
+ ' - project_id: numeric id or publicId ("prj_\u2026") of the target project.',
2232
+ ' - name (optional): service name (default "dev-environment").',
2233
+ ' - plan (optional): service size (default "micro").',
2234
+ " - disk_gb (optional): /workspace volume size in GB (default 10, 1\u2013100).",
2235
+ " - hoststack_api_key (optional): sets HOSTSTACK_API_KEY so the hoststack MCP works inside the container.",
2236
+ " - poststack_api_key (optional): sets POSTSTACK_API_KEY so the poststack MCP works inside the container.",
2237
+ "",
2238
+ "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.",
2239
+ "",
2240
+ 'Example: create_dev_environment({ project_id: "prj_abc", name: "scratch", hoststack_api_key: "hs_live_\u2026" })'
2241
+ ].join("\n"),
2242
+ input: {
2243
+ project_id: z13.union([z13.number().int().positive(), z13.string()]).describe("Target project \u2014 numeric id or publicId."),
2244
+ name: z13.string().min(1).max(100).optional().describe('Service name (default "dev-environment").'),
2245
+ plan: z13.enum(SERVICE_PLANS).optional().describe('Service size (default "micro").'),
2246
+ disk_gb: z13.number().int().min(1).max(100).optional().describe("/workspace volume size in GB (default 10)."),
2247
+ hoststack_api_key: z13.string().optional().describe("Value for HOSTSTACK_API_KEY (enables the hoststack MCP in-container)."),
2248
+ poststack_api_key: z13.string().optional().describe("Value for POSTSTACK_API_KEY (enables the poststack MCP in-container).")
2249
+ },
2250
+ handler: async (args, ctx) => {
2251
+ const teamId = await ctx.resolveTeamId();
2252
+ const projectId = await ctx.hoststack.resolveId(args.project_id, {
2253
+ kind: "project",
2254
+ teamId
2255
+ });
2256
+ const name = args.name ?? "dev-environment";
2257
+ const sizeGb = args.disk_gb ?? DEV_ENV_VOLUME.sizeGb;
2258
+ const createInput = {
2259
+ name,
2260
+ type: "private_service",
2261
+ projectId,
2262
+ dockerImage: DEV_ENV_IMAGE,
2263
+ autoDeploy: false
2264
+ };
2265
+ if (args.plan !== void 0) createInput.plan = args.plan;
2266
+ const created = await ctx.hoststack.services.create(teamId, createInput);
2267
+ const service = created.service;
2268
+ const envVars = [];
2269
+ if (args.hoststack_api_key)
2270
+ envVars.push({
2271
+ key: "HOSTSTACK_API_KEY",
2272
+ value: args.hoststack_api_key,
2273
+ isSecret: true
2274
+ });
2275
+ if (args.poststack_api_key)
2276
+ envVars.push({
2277
+ key: "POSTSTACK_API_KEY",
2278
+ value: args.poststack_api_key,
2279
+ isSecret: true
2280
+ });
2281
+ if (envVars.length > 0) {
2282
+ await ctx.hoststack.envVars.bulkSet(teamId, service.id, { vars: envVars });
2283
+ }
2284
+ let volumeAttached = false;
2285
+ try {
2286
+ await ctx.hoststack.volumes.create(teamId, service.id, {
2287
+ name: DEV_ENV_VOLUME.name,
2288
+ mountPath: DEV_ENV_VOLUME.mountPath,
2289
+ sizeGb
2290
+ });
2291
+ volumeAttached = true;
2292
+ } catch {
2293
+ }
2294
+ const deploy = await ctx.hoststack.deploys.trigger(teamId, service.id);
2295
+ const deployId = deploy.deploy?.id ?? null;
2296
+ return respond({
2297
+ summary: `Created AI dev environment "${name}" (${service.publicId})${volumeAttached ? " with a /workspace volume" : ""} \u2014 deploying. Open its Terminal tab once running.`,
2298
+ data: { service: shapeService(service), volumeAttached, deployId }
2299
+ });
2300
+ }
2301
+ });
2114
2302
  defineTool({
2115
2303
  name: "get_service",
2116
2304
  category: "services",
@@ -2718,7 +2906,7 @@ defineTool({
2718
2906
 
2719
2907
  // src/server-factory.ts
2720
2908
  var PACKAGE_NAME = "hoststack";
2721
- var PACKAGE_VERSION = "0.6.0";
2909
+ var PACKAGE_VERSION = "0.9.1";
2722
2910
  function createMcpServer(options) {
2723
2911
  const baseUrl = (options.baseUrl ?? "https://hoststack.dev").replace(/\/$/, "");
2724
2912
  const hoststack = new HostStack({ apiKey: options.apiKey, baseUrl });