@hoststack.dev/mcp 0.13.1 → 0.15.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.
@@ -7,6 +7,10 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
7
7
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
8
  import { HostStack } from "@hoststack.dev/sdk";
9
9
 
10
+ // src/version.ts
11
+ var MCP_VERSION = true ? "0.15.0" : "0.0.0-dev";
12
+ var USER_AGENT = `hoststack-mcp/${MCP_VERSION}`;
13
+
10
14
  // src/api-client.ts
11
15
  var ApiClient = class {
12
16
  constructor(apiKey2, baseUrl2) {
@@ -16,7 +20,8 @@ var ApiClient = class {
16
20
  get headers() {
17
21
  return {
18
22
  Authorization: `Bearer ${this.apiKey}`,
19
- "Content-Type": "application/json"
23
+ "Content-Type": "application/json",
24
+ "User-Agent": USER_AGENT
20
25
  };
21
26
  }
22
27
  async get(path, params) {
@@ -134,6 +139,9 @@ function defineTool(def) {
134
139
  handler: def.handler
135
140
  });
136
141
  }
142
+ function listToolDefinitions() {
143
+ return tools;
144
+ }
137
145
  function attachTools(server2, ctx, sink) {
138
146
  const register = server2.tool.bind(server2);
139
147
  for (const def of tools) {
@@ -1050,10 +1058,20 @@ defineTool({
1050
1058
 
1051
1059
  // src/tools/dns-records.ts
1052
1060
  import { z as z7 } from "zod";
1053
- var DNS_RECORD_TYPES = ["A", "AAAA", "CNAME", "MX", "TXT", "NS", "SRV", "CAA", "ALIAS"];
1054
- async function resolveZonePublicId(api, teamId, input) {
1061
+ var DNS_RECORD_TYPES = [
1062
+ "A",
1063
+ "AAAA",
1064
+ "CNAME",
1065
+ "MX",
1066
+ "TXT",
1067
+ "NS",
1068
+ "SRV",
1069
+ "CAA",
1070
+ "ALIAS"
1071
+ ];
1072
+ async function resolveZonePublicId(hoststack, teamId, input) {
1055
1073
  if (input.zone_id) {
1056
- const { zones: zones2 } = await api.get(`/api/dns-zones/${teamId}`);
1074
+ const { zones: zones2 } = await hoststack.dns.listZones(teamId);
1057
1075
  const match = zones2.find((z15) => z15.publicId === input.zone_id);
1058
1076
  if (!match) {
1059
1077
  throw new Error(`Zone ${input.zone_id} not found on this team.`);
@@ -1063,7 +1081,7 @@ async function resolveZonePublicId(api, teamId, input) {
1063
1081
  if (!input.domain) {
1064
1082
  throw new Error("Provide either zone_id or domain.");
1065
1083
  }
1066
- const { zones } = await api.get(`/api/dns-zones/${teamId}`);
1084
+ const { zones } = await hoststack.dns.listZones(teamId);
1067
1085
  const fqdn = input.domain.toLowerCase().replace(/\.$/, "");
1068
1086
  const labels = fqdn.split(".");
1069
1087
  for (let i = 0; i < labels.length - 1; i++) {
@@ -1092,7 +1110,7 @@ defineTool({
1092
1110
  input: {},
1093
1111
  handler: async (_args, ctx) => {
1094
1112
  const teamId = await ctx.resolveTeamId();
1095
- const response = await ctx.api.get(`/api/dns-zones/${teamId}`);
1113
+ const response = await ctx.hoststack.dns.listZones(teamId);
1096
1114
  const items = Array.isArray(response.zones) ? response.zones.map(shape) : [];
1097
1115
  const summary = items.length === 0 ? "No DNS zones hosted on this team. Create one in the dashboard (Domains \u2192 DNS)." : `Found ${items.length} hosted DNS zone${items.length === 1 ? "" : "s"}.`;
1098
1116
  return respond({ summary, data: { items } });
@@ -1121,10 +1139,8 @@ defineTool({
1121
1139
  const zoneInput = {};
1122
1140
  if (args2.zone_id !== void 0) zoneInput.zone_id = args2.zone_id;
1123
1141
  if (args2.domain !== void 0) zoneInput.domain = args2.domain;
1124
- const zone = await resolveZonePublicId(ctx.api, teamId, zoneInput);
1125
- const response = await ctx.api.get(
1126
- `/api/dns-zones/${teamId}/${zone.publicId}/records`
1127
- );
1142
+ const zone = await resolveZonePublicId(ctx.hoststack, teamId, zoneInput);
1143
+ const response = await ctx.hoststack.dns.listRecords(teamId, zone.publicId);
1128
1144
  const items = Array.isArray(response.records) ? response.records.map(shape) : [];
1129
1145
  const summary = items.length === 0 ? `Zone ${zone.domainName} has no records yet.` : `Returned ${items.length} record${items.length === 1 ? "" : "s"} for ${zone.domainName}.`;
1130
1146
  return respond({
@@ -1153,11 +1169,9 @@ defineTool({
1153
1169
  },
1154
1170
  handler: async (args2, ctx) => {
1155
1171
  const teamId = await ctx.resolveTeamId();
1156
- const { zones } = await ctx.api.get(`/api/dns-zones/${teamId}`);
1172
+ const { zones } = await ctx.hoststack.dns.listZones(teamId);
1157
1173
  for (const zone of zones) {
1158
- const { records } = await ctx.api.get(
1159
- `/api/dns-zones/${teamId}/${zone.publicId}/records`
1160
- );
1174
+ const { records } = await ctx.hoststack.dns.listRecords(teamId, zone.publicId);
1161
1175
  const match = records.find((r) => r.publicId === args2.record_id);
1162
1176
  if (match) {
1163
1177
  return respond({
@@ -1213,7 +1227,7 @@ defineTool({
1213
1227
  const zoneInput = {};
1214
1228
  if (args2.zone_id !== void 0) zoneInput.zone_id = args2.zone_id;
1215
1229
  if (args2.domain !== void 0) zoneInput.domain = args2.domain;
1216
- const zone = await resolveZonePublicId(ctx.api, teamId, zoneInput);
1230
+ const zone = await resolveZonePublicId(ctx.hoststack, teamId, zoneInput);
1217
1231
  const body = {
1218
1232
  type: args2.type,
1219
1233
  name: args2.name,
@@ -1221,10 +1235,7 @@ defineTool({
1221
1235
  };
1222
1236
  if (args2.ttl !== void 0) body.ttl = args2.ttl;
1223
1237
  if (args2.priority !== void 0) body.priority = args2.priority;
1224
- const response = await ctx.api.post(
1225
- `/api/dns-zones/${teamId}/${zone.publicId}/records`,
1226
- body
1227
- );
1238
+ const response = await ctx.hoststack.dns.createRecord(teamId, zone.publicId, body);
1228
1239
  return respond({
1229
1240
  summary: `Created ${args2.type} ${args2.name} on ${zone.domainName}.`,
1230
1241
  data: {
@@ -1264,7 +1275,7 @@ defineTool({
1264
1275
  return respondError(`Priority is required for ${args2.type} records (0\u201365535).`);
1265
1276
  }
1266
1277
  const teamId = await ctx.resolveTeamId();
1267
- const target = await findRecordZone(ctx.api, teamId, args2.record_id);
1278
+ const target = await findRecordZone(ctx.hoststack, teamId, args2.record_id);
1268
1279
  if (!target) {
1269
1280
  return respondError(
1270
1281
  `Record ${args2.record_id} not found on any zone owned by this team.`
@@ -1277,8 +1288,10 @@ defineTool({
1277
1288
  };
1278
1289
  if (args2.ttl !== void 0) body.ttl = args2.ttl;
1279
1290
  if (args2.priority !== void 0) body.priority = args2.priority;
1280
- const response = await ctx.api.put(
1281
- `/api/dns-zones/${teamId}/${target.zone.publicId}/records/${args2.record_id}`,
1291
+ const response = await ctx.hoststack.dns.updateRecord(
1292
+ teamId,
1293
+ target.zone.publicId,
1294
+ args2.record_id,
1282
1295
  body
1283
1296
  );
1284
1297
  return respond({
@@ -1313,27 +1326,23 @@ defineTool({
1313
1326
  },
1314
1327
  handler: async (args2, ctx) => {
1315
1328
  const teamId = await ctx.resolveTeamId();
1316
- const target = await findRecordZone(ctx.api, teamId, args2.record_id);
1329
+ const target = await findRecordZone(ctx.hoststack, teamId, args2.record_id);
1317
1330
  if (!target) {
1318
1331
  return respondError(
1319
1332
  `Record ${args2.record_id} not found on any zone owned by this team.`
1320
1333
  );
1321
1334
  }
1322
- await ctx.api.delete(
1323
- `/api/dns-zones/${teamId}/${target.zone.publicId}/records/${args2.record_id}`
1324
- );
1335
+ await ctx.hoststack.dns.deleteRecord(teamId, target.zone.publicId, args2.record_id);
1325
1336
  return respond({
1326
1337
  summary: `Deleted record ${args2.record_id} from ${target.zone.domainName}.`,
1327
1338
  data: { ok: true }
1328
1339
  });
1329
1340
  }
1330
1341
  });
1331
- async function findRecordZone(api, teamId, recordPublicId) {
1332
- const { zones } = await api.get(`/api/dns-zones/${teamId}`);
1342
+ async function findRecordZone(hoststack, teamId, recordPublicId) {
1343
+ const { zones } = await hoststack.dns.listZones(teamId);
1333
1344
  for (const zone of zones) {
1334
- const { records } = await api.get(
1335
- `/api/dns-zones/${teamId}/${zone.publicId}/records`
1336
- );
1345
+ const { records } = await hoststack.dns.listRecords(teamId, zone.publicId);
1337
1346
  const match = records.find((r) => r.publicId === recordPublicId);
1338
1347
  if (match) return { zone, record: match };
1339
1348
  }
@@ -1493,6 +1502,7 @@ defineTool({
1493
1502
  ' - key: env-var name (e.g. "DATABASE_URL").',
1494
1503
  " - value: new value (will be encrypted at rest if is_secret=true).",
1495
1504
  " - 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.",
1505
+ ' - target (optional): where the var is injected \u2014 "build", "runtime", or "both". On create, defaults to "both". On update, omitting it leaves the existing target untouched.',
1496
1506
  "",
1497
1507
  'Returns: { envVar: EnvVar, action: "created" | "updated" }.',
1498
1508
  "",
@@ -1502,7 +1512,8 @@ defineTool({
1502
1512
  service_id: z9.string().describe("Service publicId."),
1503
1513
  key: z9.string().min(1).max(128).describe("Env-var key."),
1504
1514
  value: z9.string().describe("New value."),
1505
- is_secret: z9.boolean().optional().describe("Mark as secret (encrypted, masked on read). Default true.")
1515
+ is_secret: z9.boolean().optional().describe("Mark as secret (encrypted, masked on read). Default true."),
1516
+ target: z9.enum(["build", "runtime", "both"]).optional().describe("Injection target: build, runtime, or both. Default both.")
1506
1517
  },
1507
1518
  handler: async (args2, ctx) => {
1508
1519
  const teamId = await ctx.resolveTeamId();
@@ -1511,6 +1522,7 @@ defineTool({
1511
1522
  if (match) {
1512
1523
  const updatePayload = { value: args2.value };
1513
1524
  if (args2.is_secret !== void 0) updatePayload.isSecret = args2.is_secret;
1525
+ if (args2.target !== void 0) updatePayload.target = args2.target;
1514
1526
  const response2 = await ctx.hoststack.envVars.update(
1515
1527
  teamId,
1516
1528
  args2.service_id,
@@ -1526,7 +1538,8 @@ defineTool({
1526
1538
  const response = await ctx.hoststack.envVars.create(teamId, args2.service_id, {
1527
1539
  key: args2.key,
1528
1540
  value: args2.value,
1529
- isSecret: args2.is_secret ?? true
1541
+ isSecret: args2.is_secret ?? true,
1542
+ ...args2.target !== void 0 ? { target: args2.target } : {}
1530
1543
  });
1531
1544
  const data = {
1532
1545
  envVar: shapeEnvVar(response.envVar),
@@ -1582,7 +1595,7 @@ defineTool({
1582
1595
  "",
1583
1596
  "Inputs:",
1584
1597
  " - service_id: publicId of the service.",
1585
- " - env_vars: array of { key, value, is_secret? }. is_secret defaults to true per row.",
1598
+ ' - env_vars: array of { key, value, is_secret?, target? }. is_secret defaults to FALSE per row (a row is stored in the clear unless you set is_secret:true); target defaults to "both". Note this differs from set_env_var, which defaults a newly-created var to secret.',
1586
1599
  "",
1587
1600
  "Returns: { ok: true }. Re-list with list_env_vars to confirm the new state.",
1588
1601
  "",
@@ -1594,7 +1607,8 @@ defineTool({
1594
1607
  z9.object({
1595
1608
  key: z9.string().min(1).max(128),
1596
1609
  value: z9.string(),
1597
- is_secret: z9.boolean().optional()
1610
+ is_secret: z9.boolean().optional(),
1611
+ target: z9.enum(["build", "runtime", "both"]).optional()
1598
1612
  })
1599
1613
  ).max(500).describe("Array of env-var rows. Hard cap 500.")
1600
1614
  },
@@ -1607,6 +1621,7 @@ defineTool({
1607
1621
  value: v.value
1608
1622
  };
1609
1623
  if (v.is_secret !== void 0) row.isSecret = v.is_secret;
1624
+ if (v.target !== void 0) row.target = v.target;
1610
1625
  return row;
1611
1626
  })
1612
1627
  };
@@ -1628,6 +1643,10 @@ defineTool({
1628
1643
  "",
1629
1644
  "When to use: the user wants to see what envs exist before creating a service in one or promoting a deploy. Every project has at least Production.",
1630
1645
  "",
1646
+ // §2.2: these are project deployment environments, not cloud Dev Boxes — point agents at the
1647
+ // right surface so the two "dev" concepts stay distinct (bidirectional cross-reference).
1648
+ "NOTE \u2014 these are project deployment environments (Production / Staging / Preview), NOT agentic cloud Dev Boxes. To list Dev Boxes use list_dev_environments; to create one use create_dev_environment / create_standalone_dev_environment / spin_up_dev_environment.",
1649
+ "",
1631
1650
  "Inputs:",
1632
1651
  ' - project_id: project publicId (e.g. "prj_abc123").',
1633
1652
  "",
@@ -1654,10 +1673,15 @@ defineTool({
1654
1673
  "",
1655
1674
  "When to use: the user wants to add an env so they can run a sibling service alongside production for testing or staging before release.",
1656
1675
  "",
1676
+ // §2.1/§2.2: steer callers away from the agentic cloud Dev Box tools (which live in
1677
+ // services.ts) — an environment is a deployment target, NOT a Dev Box. Bidirectional with
1678
+ // the cross-references those dev-box tools point back here.
1679
+ 'NOTE \u2014 this is NOT a cloud Dev Box. An environment is a project deployment target (Production / Staging / Preview); type="development" just gives services a -dev hostname suffix. For an agentic cloud Dev Box (a terminal you drive coding agents in) use create_dev_environment / create_standalone_dev_environment / spin_up_dev_environment instead.',
1680
+ "",
1657
1681
  "Inputs:",
1658
1682
  " - project_id: project publicId.",
1659
1683
  " - name: human-readable name (1\u201364 chars). Shown in the env switcher.",
1660
- ' - type: "production" | "staging" | "development" | "preview". Determines the hostname suffix on services in this env (production stays clean; others get -staging / -dev / -preview).',
1684
+ ' - type: "production" | "staging" | "development" | "preview". Determines the hostname suffix on services in this env (production stays clean; others get -staging / -dev / -preview). type="development" creates a PROJECT deployment environment (a -dev hostname suffix), NOT an agentic Dev Box \u2014 for a cloud dev box use create_dev_environment / create_standalone_dev_environment / spin_up_dev_environment.',
1661
1685
  " - is_protected (optional): require admin role for destructive actions in this env. Default false.",
1662
1686
  "",
1663
1687
  "Returns: { environment: Environment }.",
@@ -1667,7 +1691,10 @@ defineTool({
1667
1691
  input: {
1668
1692
  project_id: z10.string().describe("Project publicId."),
1669
1693
  name: z10.string().min(1).max(64).describe("Environment name (1\u201364 chars)."),
1670
- type: z10.enum(["production", "staging", "development", "preview"]).describe("Environment type \u2014 drives the hostname suffix."),
1694
+ type: z10.enum(["production", "staging", "development", "preview"]).describe(
1695
+ // §2.1: "development" here is a project deployment env, NOT a cloud Dev Box.
1696
+ 'Environment type \u2014 drives the hostname suffix. type="development" creates a PROJECT deployment environment (a -dev hostname suffix), NOT an agentic Dev Box \u2014 for a cloud dev box use create_dev_environment / create_standalone_dev_environment / spin_up_dev_environment.'
1697
+ ),
1671
1698
  is_protected: z10.boolean().optional().describe("Require admin role for destructive actions. Default false.")
1672
1699
  },
1673
1700
  handler: async (args2, ctx) => {
@@ -1759,6 +1786,14 @@ defineTool({
1759
1786
  });
1760
1787
 
1761
1788
  // src/tools/meta.ts
1789
+ var DEV_ENV_TOOL_NAMES = [
1790
+ "create_dev_environment",
1791
+ "create_standalone_dev_environment",
1792
+ "spin_up_dev_environment",
1793
+ "list_dev_environments",
1794
+ "resize_dev_environment",
1795
+ "delete_dev_environment"
1796
+ ];
1762
1797
  defineTool({
1763
1798
  name: "get_me",
1764
1799
  category: "meta",
@@ -1783,6 +1818,36 @@ defineTool({
1783
1818
  return respond({ summary: `Authenticated as ${userEmail}${teamLabel}.`, data });
1784
1819
  }
1785
1820
  });
1821
+ defineTool({
1822
+ name: "describe_mcp",
1823
+ category: "meta",
1824
+ description: [
1825
+ "Report this MCP build: its package version, total tool count, and the list of Dev Box tools it exposes. Read-only \u2014 touches no account data.",
1826
+ "",
1827
+ "When to use: at the start of a session to confirm the live MCP build, OR when a Dev Box tool you expect (create_dev_environment, create_standalone_dev_environment, spin_up_dev_environment, list_dev_environments, resize_dev_environment, delete_dev_environment) seems to be missing. A STALE npx/npm cache can silently present an old tool set with no other signal \u2014 call this and check `devEnvToolsPresent`; if it is false, the cached build predates the Dev Box tools and the user should refresh it (e.g. `npx --yes @hoststack.dev/mcp@latest` / clear the npx cache).",
1828
+ "",
1829
+ 'Note on terminology: a "Dev Box" (a.k.a. Cloud Dev Box) is an agentic cloud box you code inside (create_dev_environment & friends). It is NOT a project "environment" (create_environment), which is a deployment target like Production / Staging / Preview. This tool only reports the Dev Box tool set.',
1830
+ "",
1831
+ "Inputs: none.",
1832
+ "",
1833
+ "Returns: { version, toolCount, devEnvTools: string[], devEnvToolsPresent: boolean } \u2014 version is the @hoststack.dev/mcp package version, toolCount is every registered tool, devEnvTools is the expected Dev Box tool names, and devEnvToolsPresent is true only if all of them are actually registered in this build.",
1834
+ "",
1835
+ 'Example: describe_mcp() \u2192 { version: "0.14.0", toolCount: 30, devEnvTools: ["create_dev_environment", \u2026], devEnvToolsPresent: true }'
1836
+ ].join("\n"),
1837
+ input: {},
1838
+ handler: async (_args, _ctx) => {
1839
+ const registered = new Set(listToolDefinitions().map((t) => t.name));
1840
+ const devEnvToolsPresent = DEV_ENV_TOOL_NAMES.every((name) => registered.has(name));
1841
+ const data = {
1842
+ version: MCP_VERSION,
1843
+ toolCount: registered.size,
1844
+ devEnvTools: [...DEV_ENV_TOOL_NAMES],
1845
+ devEnvToolsPresent
1846
+ };
1847
+ const summary = devEnvToolsPresent ? `hoststack MCP v${MCP_VERSION} \u2014 ${data.toolCount} tools, all ${DEV_ENV_TOOL_NAMES.length} Dev Box tools present.` : `hoststack MCP v${MCP_VERSION} \u2014 ${data.toolCount} tools, but Dev Box tools are MISSING (stale build \u2014 refresh the npx/npm cache).`;
1848
+ return respond({ summary, data });
1849
+ }
1850
+ });
1786
1851
 
1787
1852
  // src/tools/notifications.ts
1788
1853
  import { z as z11 } from "zod";
@@ -2107,11 +2172,19 @@ var SERVICE_PLANS = [
2107
2172
  "pico",
2108
2173
  "nano",
2109
2174
  "micro",
2110
- "starter",
2175
+ "small",
2111
2176
  "standard",
2177
+ "large",
2178
+ "xlarge",
2112
2179
  "pro_standard",
2113
2180
  "pro_large"
2114
2181
  ];
2182
+ var DEV_ENV_MIN_SIZE = "standard";
2183
+ function coerceDevEnvSize(value) {
2184
+ const minIdx = SERVICE_PLANS.indexOf(DEV_ENV_MIN_SIZE);
2185
+ const idx = value ? SERVICE_PLANS.indexOf(value) : -1;
2186
+ return idx >= minIdx ? value : DEV_ENV_MIN_SIZE;
2187
+ }
2115
2188
  defineTool({
2116
2189
  name: "list_services",
2117
2190
  category: "services",
@@ -2120,11 +2193,14 @@ defineTool({
2120
2193
  "",
2121
2194
  "When to use: the agent needs to find a service by name, check what is deployed, or pick a target for a follow-up tool (logs, deploys, env vars). This is the canonical way to resolve a publicId from a human-friendly name.",
2122
2195
  "",
2196
+ "Note: agentic Dev Boxes (cloud dev environments) are EXCLUDED by default. Pass dev_environment:true to include them, or use list_dev_environments for the dedicated, annotated dev-box listing.",
2197
+ "",
2123
2198
  "Inputs (all optional):",
2124
2199
  ' - project_id: narrow to one project (numeric id or publicId "prj_\u2026").',
2125
2200
  ' - environment_id: narrow to one environment (numeric id or publicId "env_\u2026").',
2126
2201
  ' - status: "active" | "deploying" | "suspended" | "failed" | "not_deployed".',
2127
2202
  ' - type: "web_service" | "private_service" | "worker" | "cron_job" | "static_site".',
2203
+ " - dev_environment: include Dev Boxes in the results (excluded by default).",
2128
2204
  "",
2129
2205
  "Returns: { items: Service[] } \u2014 each service includes id, publicId, name, type, status, projectId, repoUrl, branch, runtime, createdAt.",
2130
2206
  "",
@@ -2134,11 +2210,15 @@ defineTool({
2134
2210
  project_id: z13.union([z13.number().int().positive(), z13.string()]).optional().describe("Project filter \u2014 numeric id or publicId."),
2135
2211
  environment_id: z13.union([z13.number().int().positive(), z13.string()]).optional().describe("Environment filter \u2014 numeric id or publicId."),
2136
2212
  status: z13.enum(["active", "deploying", "suspended", "failed", "not_deployed"]).optional().describe("Filter by current runtime status."),
2137
- type: z13.enum(["web_service", "private_service", "worker", "cron_job", "static_site"]).optional().describe("Filter by service type.")
2213
+ type: z13.enum(["web_service", "private_service", "worker", "cron_job", "static_site"]).optional().describe("Filter by service type."),
2214
+ dev_environment: z13.boolean().optional().describe(
2215
+ "Include agentic Dev Boxes in the results (excluded by default; see list_dev_environments)."
2216
+ )
2138
2217
  },
2139
2218
  handler: async (args2, ctx) => {
2140
2219
  const teamId = await ctx.resolveTeamId();
2141
2220
  const filters = {};
2221
+ if (args2.dev_environment) filters.devEnvironment = true;
2142
2222
  if (args2.project_id !== void 0) {
2143
2223
  const resolved = await ctx.hoststack.resolveId(args2.project_id, {
2144
2224
  kind: "project",
@@ -2161,7 +2241,8 @@ defineTool({
2161
2241
  args2.project_id !== void 0 ? `project=${args2.project_id}` : null,
2162
2242
  args2.environment_id !== void 0 ? `env=${args2.environment_id}` : null,
2163
2243
  args2.status ? `status=${args2.status}` : null,
2164
- args2.type ? `type=${args2.type}` : null
2244
+ args2.type ? `type=${args2.type}` : null,
2245
+ args2.dev_environment ? "devBoxes=included" : null
2165
2246
  ].filter(Boolean).join(", ");
2166
2247
  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}` : ""}.`;
2167
2248
  return respond({ summary, data });
@@ -2252,14 +2333,16 @@ defineTool({
2252
2333
  name: "create_dev_environment",
2253
2334
  category: "services",
2254
2335
  description: [
2255
- "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.",
2336
+ 'Spin up an AI Dev Box in one call: a cloud "Dev Box" (an agentic container from the dev-env image \u2014 Claude Code + Codex + OpenCode + MCPs preinstalled) inside a project, with a persistent /workspace volume and the MCP API keys set, then fire the first deploy.',
2337
+ "",
2338
+ 'This makes a cloud Dev Box, NOT a project deploy environment. A project "environment" (create_environment \u2192 Production / Staging / Preview) is a deployment target; this is a workspace you code in. If you actually want a -dev deploy target, use create_environment instead.',
2256
2339
  "",
2257
- `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.`,
2340
+ `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.`,
2258
2341
  "",
2259
2342
  "Inputs:",
2260
2343
  ' - project_id: numeric id or publicId ("prj_\u2026") of the target project.',
2261
2344
  ' - name (optional): service name (default "dev-environment").',
2262
- ' - plan (optional): service size (default "standard" \u2014 2 GB, the smallest that fits a coding agent + build).',
2345
+ ' - plan (optional): box size. Defaults to "standard" (2 GB) \u2014 the OOM-safe floor (DEV_ENV_MIN_SIZE); a coding agent + build OOMs below it. A smaller plan is clamped up to "standard".',
2263
2346
  " - disk_gb (optional): /workspace volume size in GB (default 10, 1\u2013100).",
2264
2347
  " - hoststack_api_key (optional): sets HOSTSTACK_API_KEY so the hoststack MCP works inside the container.",
2265
2348
  " - poststack_api_key (optional): sets POSTSTACK_API_KEY so the poststack MCP works inside the container.",
@@ -2273,7 +2356,7 @@ defineTool({
2273
2356
  project_id: z13.union([z13.number().int().positive(), z13.string()]).describe("Target project \u2014 numeric id or publicId."),
2274
2357
  name: z13.string().min(1).max(100).optional().describe('Service name (default "dev-environment").'),
2275
2358
  plan: z13.enum(SERVICE_PLANS).optional().describe(
2276
- 'Service size (default "standard" \u2014 2 GB; smallest that fits a coding agent).'
2359
+ 'Box size (default "standard" \u2014 2 GB, the OOM-safe floor; a smaller plan is clamped up to "standard").'
2277
2360
  ),
2278
2361
  disk_gb: z13.number().int().min(1).max(100).optional().describe("/workspace volume size in GB (default 10)."),
2279
2362
  hoststack_api_key: z13.string().optional().describe("Value for HOSTSTACK_API_KEY (enables the hoststack MCP in-container)."),
@@ -2291,68 +2374,94 @@ defineTool({
2291
2374
  });
2292
2375
  const name = args2.name ?? "dev-environment";
2293
2376
  const sizeGb = args2.disk_gb ?? DEV_ENV_VOLUME.sizeGb;
2377
+ const plan = coerceDevEnvSize(args2.plan);
2294
2378
  const createInput = {
2295
2379
  name,
2296
2380
  type: "private_service",
2297
2381
  projectId,
2298
2382
  dockerImage: DEV_ENV_IMAGE,
2299
- autoDeploy: false
2383
+ autoDeploy: false,
2384
+ plan
2300
2385
  };
2301
- if (args2.plan !== void 0) createInput.plan = args2.plan;
2302
2386
  const created = await ctx.hoststack.services.create(teamId, createInput);
2303
2387
  const service = created.service;
2304
- const envVars = [];
2305
- if (args2.hoststack_api_key)
2306
- envVars.push({
2307
- key: "HOSTSTACK_API_KEY",
2308
- value: args2.hoststack_api_key,
2309
- isSecret: true
2310
- });
2311
- if (args2.poststack_api_key)
2312
- envVars.push({
2313
- key: "POSTSTACK_API_KEY",
2314
- value: args2.poststack_api_key,
2315
- isSecret: true
2316
- });
2317
- if (args2.repo_url) {
2318
- envVars.push({
2319
- key: "HOSTSTACK_DEVENV_REPO_URL",
2320
- value: args2.repo_url,
2321
- isSecret: false
2322
- });
2323
- if (args2.branch)
2388
+ const rollback = async () => {
2389
+ try {
2390
+ await ctx.hoststack.services.tearDownDevEnvironment(teamId, service.id);
2391
+ } catch {
2392
+ try {
2393
+ await ctx.hoststack.services.delete(teamId, service.id);
2394
+ } catch {
2395
+ }
2396
+ }
2397
+ };
2398
+ try {
2399
+ const envVars = [];
2400
+ if (args2.hoststack_api_key)
2324
2401
  envVars.push({
2325
- key: "HOSTSTACK_DEVENV_BRANCH",
2326
- value: args2.branch,
2402
+ key: "HOSTSTACK_API_KEY",
2403
+ value: args2.hoststack_api_key,
2404
+ isSecret: true
2405
+ });
2406
+ if (args2.poststack_api_key)
2407
+ envVars.push({
2408
+ key: "POSTSTACK_API_KEY",
2409
+ value: args2.poststack_api_key,
2410
+ isSecret: true
2411
+ });
2412
+ if (args2.repo_url) {
2413
+ envVars.push({
2414
+ key: "HOSTSTACK_DEVENV_REPO_URL",
2415
+ value: args2.repo_url,
2327
2416
  isSecret: false
2328
2417
  });
2329
- }
2330
- if (envVars.length > 0) {
2331
- await ctx.hoststack.envVars.bulkSet(teamId, service.id, { vars: envVars });
2332
- }
2333
- let volumeAttached = false;
2334
- try {
2335
- await ctx.hoststack.volumes.create(teamId, service.id, {
2336
- name: DEV_ENV_VOLUME.name,
2337
- mountPath: DEV_ENV_VOLUME.mountPath,
2338
- sizeGb
2418
+ if (args2.branch)
2419
+ envVars.push({
2420
+ key: "HOSTSTACK_DEVENV_BRANCH",
2421
+ value: args2.branch,
2422
+ isSecret: false
2423
+ });
2424
+ }
2425
+ if (envVars.length > 0) {
2426
+ await ctx.hoststack.envVars.bulkSet(teamId, service.id, { vars: envVars });
2427
+ }
2428
+ try {
2429
+ await ctx.hoststack.volumes.create(teamId, service.id, {
2430
+ name: DEV_ENV_VOLUME.name,
2431
+ mountPath: DEV_ENV_VOLUME.mountPath,
2432
+ sizeGb
2433
+ });
2434
+ } catch (error) {
2435
+ const reason = error instanceof Error ? error.message : String(error);
2436
+ await rollback();
2437
+ return respondError(
2438
+ `Failed to attach the /workspace volume for Dev Box "${name}" \u2014 aborted and rolled back rather than deploy a box that would lose all work on recreate. Volume error: ${reason}`,
2439
+ { volumeError: reason, rolledBack: true }
2440
+ );
2441
+ }
2442
+ const deploy = await ctx.hoststack.deploys.trigger(teamId, service.id);
2443
+ const deployId = deploy.deploy?.id ?? null;
2444
+ return respond({
2445
+ summary: `Created AI Dev Box "${name}" (${service.publicId}) with a /workspace volume \u2014 deploying. Open it in the dashboard's Dev Boxes section (or the service's Terminal tab) once running.`,
2446
+ data: { service: shapeService(service), volumeAttached: true, deployId }
2339
2447
  });
2340
- volumeAttached = true;
2341
- } catch {
2448
+ } catch (error) {
2449
+ const reason = error instanceof Error ? error.message : String(error);
2450
+ await rollback();
2451
+ return respondError(
2452
+ `Failed to provision Dev Box "${name}" \u2014 rolled back the partially-created box. Error: ${reason}`,
2453
+ { rolledBack: true }
2454
+ );
2342
2455
  }
2343
- const deploy = await ctx.hoststack.deploys.trigger(teamId, service.id);
2344
- const deployId = deploy.deploy?.id ?? null;
2345
- return respond({
2346
- summary: `Created AI dev environment "${name}" (${service.publicId})${volumeAttached ? " with a /workspace volume" : ""} \u2014 deploying. Open it in the dashboard's Development section (or the service's Terminal tab) once running.`,
2347
- data: { service: shapeService(service), volumeAttached, deployId }
2348
- });
2349
2456
  }
2350
2457
  });
2351
2458
  defineTool({
2352
2459
  name: "spin_up_dev_environment",
2353
2460
  category: "services",
2354
2461
  description: [
2355
- "Spin up an agentic dev environment FROM an existing service so you can reproduce a bug, fix it, view it, and ship it. Creates a dev box (Claude/Codex/OpenCode + MCPs) that RUNS a clone of the app: the repo is auto-cloned into /workspace, the service env-vars are copied, and its linked database is cloned (so the box never touches the prod DB). The box gets an unguessable public dev URL and seamless `git push`.",
2462
+ "Spin up an agentic Dev Box FROM an existing service so you can reproduce a bug, fix it, view it, and ship it. Creates a cloud Dev Box (Claude/Codex/OpenCode + MCPs) that RUNS a clone of the app: the repo is auto-cloned into /workspace, the service env-vars are copied, and its linked database is cloned (so the box never touches the prod DB). The box gets an unguessable public dev URL and seamless `git push`.",
2463
+ "",
2464
+ "This makes a cloud Dev Box, NOT a project deploy environment \u2014 for a -dev/-staging deploy target use create_environment instead.",
2356
2465
  "",
2357
2466
  'When to use: "spin up a dev environment for <service>", "I found a bug on <service>, give me a box to fix it". Distinct from create_dev_environment, which makes a BARE box not tied to any app.',
2358
2467
  "",
@@ -2395,7 +2504,9 @@ defineTool({
2395
2504
  name: "delete_dev_environment",
2396
2505
  category: "services",
2397
2506
  description: [
2398
- "Tear down an agentic dev environment in one call: removes the dev box AND cascade-deletes its cloned database, its /workspace volume, and the auto-created `development` environment if it is now empty.",
2507
+ "Tear down an agentic Dev Box in one call: removes the cloud Dev Box AND cascade-deletes its cloned database, its /workspace volume, and the auto-created `development` environment if it is now empty.",
2508
+ "",
2509
+ "This deletes a cloud Dev Box, NOT a project deploy environment \u2014 to remove a -dev/-staging deploy target use delete_environment instead.",
2399
2510
  "",
2400
2511
  'When to use: "delete / tear down the dev environment", once you have shipped the fix and no longer need the box.',
2401
2512
  "",
@@ -2426,15 +2537,17 @@ defineTool({
2426
2537
  name: "resize_dev_environment",
2427
2538
  category: "services",
2428
2539
  description: [
2429
- "Resize a dev box (or any service) to a different size tier \u2014 the supported way to give it more memory/CPU/disk headroom (e.g. when ESLint/tsc OOMs).",
2540
+ "Resize a cloud Dev Box (or any service) to a different size tier \u2014 the supported way to give it more memory/CPU/disk headroom (e.g. when ESLint/tsc OOMs).",
2430
2541
  "",
2431
- "When to use: a dev box OOM-killed (see exitReason/recommendedSize from list_dev_environments), or you just want more headroom. This changes the SIZE TIER \u2014 unlike per-config memory/CPU overrides, which are clamped to the current tier and so cannot grow a box past it.",
2542
+ "This resizes a cloud Dev Box, NOT a project deploy environment.",
2543
+ "",
2544
+ "When to use: a Dev Box OOM-killed (see exitReason/recommendedSize from list_dev_environments), or you just want more headroom. This changes the SIZE TIER \u2014 unlike per-config memory/CPU overrides, which are clamped to the current tier and so cannot grow a box past it.",
2432
2545
  "",
2433
2546
  "How it applies: the new tier's memory + CPU take effect LIVE on the running container (no recreate, no dropped shell sessions); a larger disk takes effect on the next recreate (suspend\u2192resume). Dev boxes are floored to the OOM-safe minimum size server-side.",
2434
2547
  "",
2435
2548
  "Inputs:",
2436
2549
  " - service_id: the box to resize \u2014 numeric id or publicId.",
2437
- ' - size: target tier (e.g. "standard", "large", "xlarge").',
2550
+ ' - size: target tier \u2014 one of the service catalog sizes (e.g. "standard", "large", "xlarge").',
2438
2551
  "",
2439
2552
  "Returns: { service } with the new plan.",
2440
2553
  "",
@@ -2442,7 +2555,9 @@ defineTool({
2442
2555
  ].join("\n"),
2443
2556
  input: {
2444
2557
  service_id: z13.union([z13.number().int().positive(), z13.string()]).describe("The box to resize \u2014 numeric id or publicId."),
2445
- size: z13.string().min(1).describe('Target size tier, e.g. "standard", "large", "xlarge".')
2558
+ size: z13.enum(SERVICE_PLANS).describe(
2559
+ 'Target size tier (service catalog size, e.g. "standard", "large", "xlarge").'
2560
+ )
2446
2561
  },
2447
2562
  handler: async (args2, ctx) => {
2448
2563
  const teamId = await ctx.resolveTeamId();
@@ -2461,7 +2576,9 @@ defineTool({
2461
2576
  name: "list_dev_environments",
2462
2577
  category: "services",
2463
2578
  description: [
2464
- `List the team's dev environments (the dashboard "Development" section), each annotated with the companion services attached to it.`,
2579
+ `List the team's cloud Dev Boxes (the dashboard "Dev Boxes" / Development section), each annotated with the companion services attached to it.`,
2580
+ "",
2581
+ "These are cloud Dev Boxes, NOT project deploy environments \u2014 to list a project's deploy targets (Production / Staging / Preview) use list_environments instead.",
2465
2582
  "",
2466
2583
  `When to use: "show my dev environments", before opening/tearing one down, to find a box's id.`,
2467
2584
  "",
@@ -2488,22 +2605,60 @@ defineTool({
2488
2605
  });
2489
2606
  }
2490
2607
  });
2608
+ defineTool({
2609
+ name: "list_templates",
2610
+ category: "services",
2611
+ description: [
2612
+ "List the canonical Dev Box (AI dev environment) preset so an agent can see, without guessing, what a `create_standalone_dev_environment` / `create_dev_environment` actually provisions: the container image, the persistent /workspace volume (mount + default size), the OOM-safe size floor, and the companion database options.",
2613
+ "",
2614
+ "When to use: before creating a Dev Box, to confirm the image, the default /workspace size, the minimum plan, or which companion engines can be attached \u2014 instead of hand-duplicating those constants.",
2615
+ "",
2616
+ 'Returns: { templates: [{ id, name, image, volume: { name, mountPath, sizeGb }, minPlan, companions }] }. Today there is one preset ("dev-environment").',
2617
+ "",
2618
+ 'Example: list_templates() \u2192 { templates: [{ id: "dev-environment", image: "hoststack/dev-env:latest", volume: { mountPath: "/workspace", sizeGb: 10 }, minPlan: "standard", companions: ["postgres","redis","meilisearch"] }] }'
2619
+ ].join("\n"),
2620
+ input: {},
2621
+ handler: async () => {
2622
+ const templates = [
2623
+ {
2624
+ id: "dev-environment",
2625
+ name: "AI Dev Environment",
2626
+ image: DEV_ENV_IMAGE,
2627
+ volume: {
2628
+ name: DEV_ENV_VOLUME.name,
2629
+ mountPath: DEV_ENV_VOLUME.mountPath,
2630
+ sizeGb: DEV_ENV_VOLUME.sizeGb
2631
+ },
2632
+ /** OOM-safe plan floor — a coding agent + build needs at least this. */
2633
+ minPlan: DEV_ENV_MIN_SIZE,
2634
+ /** Companion engines you can attach (fresh + empty), like local `make db-up`. */
2635
+ companions: ["postgres", "redis", "meilisearch"]
2636
+ }
2637
+ ];
2638
+ return respond({
2639
+ summary: `${templates.length} Dev Box template${templates.length === 1 ? "" : "s"} available (image ${DEV_ENV_IMAGE}, /workspace ${DEV_ENV_VOLUME.sizeGb} GB, floor "${DEV_ENV_MIN_SIZE}").`,
2640
+ data: { templates }
2641
+ });
2642
+ }
2643
+ });
2491
2644
  defineTool({
2492
2645
  name: "create_standalone_dev_environment",
2493
2646
  category: "services",
2494
2647
  description: [
2495
- "Create a STANDALONE dev environment in one call: a cloud box (Claude Code + Codex + OpenCode + MCPs) on a persistent /workspace, from a connected GitHub repo, an arbitrary clone URL, or blank \u2014 with optional companion Postgres / Redis / Meilisearch wired into its env (mirrors a local `make db-up`). The box lives in the team's hidden Development home, NOT under a project.",
2648
+ "Create a STANDALONE Dev Box in one call: a cloud box (Claude Code + Codex + OpenCode + MCPs) on a persistent /workspace, from a connected GitHub repo, an arbitrary clone URL, or blank \u2014 with optional companion Postgres / Redis / Meilisearch wired into its env (mirrors a local `make db-up`). The box lives in the team's hidden Development home, NOT under a project.",
2649
+ "",
2650
+ "This makes a cloud Dev Box, NOT a project deploy environment (create_environment \u2192 Production / Staging / Preview). It is a workspace you code in, not a deployment target.",
2496
2651
  "",
2497
2652
  'When to use: "create a dev environment for <repo>", "spin me up a cloud dev box with a Postgres". Distinct from spin_up_dev_environment (which clones an EXISTING service) and create_dev_environment (a bare box in a chosen project).',
2498
2653
  "",
2499
2654
  "Inputs:",
2500
- " - name: the dev box name.",
2655
+ " - name: the Dev Box name.",
2501
2656
  ' - source_kind: "github_repo" (clone a connected repo \u2014 needs github_repo_id), "url" (clone any http(s) git URL \u2014 needs clone_url), or "blank" (empty box).',
2502
2657
  " - github_repo_id (for github_repo): numeric id of a connected GitHub repo.",
2503
2658
  " - clone_url (for url): an http(s) git clone URL.",
2504
2659
  " - branch (optional): branch to clone.",
2505
2660
  ' - databases (optional): companion services to attach \u2014 any of "postgres", "redis", "meilisearch".',
2506
- ' - plan (optional): box size (default "micro").',
2661
+ ' - plan (optional): box size. Defaults to "standard" (2 GB) \u2014 the OOM-safe floor (DEV_ENV_MIN_SIZE); a smaller plan is floored up to "standard" server-side.',
2507
2662
  " - agent_accounts (optional): bind specific saved agent logins by account id per provider. OMIT to auto-inherit the box owner's default logins (claude/codex/opencode), so `claude` is already authenticated on first boot \u2014 no manual login.",
2508
2663
  "",
2509
2664
  "Returns: { service, devUrl, deployId } \u2014 deploying. Once live: open the Terminal tab, run `claude`, start the dev server on $PORT, view at https://<devUrl>. Tear down with delete_dev_environment.",
@@ -2511,13 +2666,15 @@ defineTool({
2511
2666
  'Example: create_standalone_dev_environment({ name: "app-dev", source_kind: "github_repo", github_repo_id: 42, databases: ["postgres","redis"] })'
2512
2667
  ].join("\n"),
2513
2668
  input: {
2514
- name: z13.string().min(1).max(100).describe("Dev box name."),
2669
+ name: z13.string().min(1).max(100).describe("Dev Box name."),
2515
2670
  source_kind: z13.enum(["github_repo", "url", "blank"]).describe("Where the code comes from."),
2516
2671
  github_repo_id: z13.number().int().positive().optional().describe('Connected GitHub repo id (required when source_kind="github_repo").'),
2517
2672
  clone_url: z13.string().url().optional().describe('http(s) git clone URL (required when source_kind="url").'),
2518
2673
  branch: z13.string().min(1).max(255).optional().describe("Branch to clone."),
2519
2674
  databases: z13.array(z13.enum(["postgres", "redis", "meilisearch"])).optional().describe("Companion services to attach (fresh + empty)."),
2520
- plan: z13.enum(SERVICE_PLANS).optional().describe('Box size (default "micro").'),
2675
+ plan: z13.enum(SERVICE_PLANS).optional().describe(
2676
+ 'Box size (default "standard" \u2014 2 GB; a smaller plan is floored to "standard").'
2677
+ ),
2521
2678
  agent_accounts: z13.array(
2522
2679
  z13.object({
2523
2680
  provider: z13.enum(["claude", "codex", "opencode"]),
@@ -3187,7 +3344,7 @@ defineTool({
3187
3344
 
3188
3345
  // src/server-factory.ts
3189
3346
  var PACKAGE_NAME = "hoststack";
3190
- var PACKAGE_VERSION = "0.9.1";
3347
+ var PACKAGE_VERSION = MCP_VERSION;
3191
3348
  function createMcpServer(options) {
3192
3349
  const baseUrl2 = (options.baseUrl ?? "https://hoststack.dev").replace(/\/$/, "");
3193
3350
  const hoststack = new HostStack({ apiKey: options.apiKey, baseUrl: baseUrl2 });