@hoststack.dev/mcp 0.13.0 → 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.
- package/dist/hoststack-mcp.js +311 -99
- package/dist/hoststack-mcp.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +308 -99
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { HostStack } from "@hoststack.dev/sdk";
|
|
4
4
|
|
|
5
|
+
// src/version.ts
|
|
6
|
+
var MCP_VERSION = true ? "0.15.0" : "0.0.0-dev";
|
|
7
|
+
var USER_AGENT = `hoststack-mcp/${MCP_VERSION}`;
|
|
8
|
+
|
|
5
9
|
// src/api-client.ts
|
|
6
10
|
var ApiClient = class {
|
|
7
11
|
constructor(apiKey, baseUrl) {
|
|
@@ -11,7 +15,8 @@ var ApiClient = class {
|
|
|
11
15
|
get headers() {
|
|
12
16
|
return {
|
|
13
17
|
Authorization: `Bearer ${this.apiKey}`,
|
|
14
|
-
"Content-Type": "application/json"
|
|
18
|
+
"Content-Type": "application/json",
|
|
19
|
+
"User-Agent": USER_AGENT
|
|
15
20
|
};
|
|
16
21
|
}
|
|
17
22
|
async get(path, params) {
|
|
@@ -1054,10 +1059,20 @@ defineTool({
|
|
|
1054
1059
|
|
|
1055
1060
|
// src/tools/dns-records.ts
|
|
1056
1061
|
import { z as z7 } from "zod";
|
|
1057
|
-
var DNS_RECORD_TYPES = [
|
|
1058
|
-
|
|
1062
|
+
var DNS_RECORD_TYPES = [
|
|
1063
|
+
"A",
|
|
1064
|
+
"AAAA",
|
|
1065
|
+
"CNAME",
|
|
1066
|
+
"MX",
|
|
1067
|
+
"TXT",
|
|
1068
|
+
"NS",
|
|
1069
|
+
"SRV",
|
|
1070
|
+
"CAA",
|
|
1071
|
+
"ALIAS"
|
|
1072
|
+
];
|
|
1073
|
+
async function resolveZonePublicId(hoststack, teamId, input) {
|
|
1059
1074
|
if (input.zone_id) {
|
|
1060
|
-
const { zones: zones2 } = await
|
|
1075
|
+
const { zones: zones2 } = await hoststack.dns.listZones(teamId);
|
|
1061
1076
|
const match = zones2.find((z15) => z15.publicId === input.zone_id);
|
|
1062
1077
|
if (!match) {
|
|
1063
1078
|
throw new Error(`Zone ${input.zone_id} not found on this team.`);
|
|
@@ -1067,7 +1082,7 @@ async function resolveZonePublicId(api, teamId, input) {
|
|
|
1067
1082
|
if (!input.domain) {
|
|
1068
1083
|
throw new Error("Provide either zone_id or domain.");
|
|
1069
1084
|
}
|
|
1070
|
-
const { zones } = await
|
|
1085
|
+
const { zones } = await hoststack.dns.listZones(teamId);
|
|
1071
1086
|
const fqdn = input.domain.toLowerCase().replace(/\.$/, "");
|
|
1072
1087
|
const labels = fqdn.split(".");
|
|
1073
1088
|
for (let i = 0; i < labels.length - 1; i++) {
|
|
@@ -1096,7 +1111,7 @@ defineTool({
|
|
|
1096
1111
|
input: {},
|
|
1097
1112
|
handler: async (_args, ctx) => {
|
|
1098
1113
|
const teamId = await ctx.resolveTeamId();
|
|
1099
|
-
const response = await ctx.
|
|
1114
|
+
const response = await ctx.hoststack.dns.listZones(teamId);
|
|
1100
1115
|
const items = Array.isArray(response.zones) ? response.zones.map(shape) : [];
|
|
1101
1116
|
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"}.`;
|
|
1102
1117
|
return respond({ summary, data: { items } });
|
|
@@ -1125,10 +1140,8 @@ defineTool({
|
|
|
1125
1140
|
const zoneInput = {};
|
|
1126
1141
|
if (args.zone_id !== void 0) zoneInput.zone_id = args.zone_id;
|
|
1127
1142
|
if (args.domain !== void 0) zoneInput.domain = args.domain;
|
|
1128
|
-
const zone = await resolveZonePublicId(ctx.
|
|
1129
|
-
const response = await ctx.
|
|
1130
|
-
`/api/dns-zones/${teamId}/${zone.publicId}/records`
|
|
1131
|
-
);
|
|
1143
|
+
const zone = await resolveZonePublicId(ctx.hoststack, teamId, zoneInput);
|
|
1144
|
+
const response = await ctx.hoststack.dns.listRecords(teamId, zone.publicId);
|
|
1132
1145
|
const items = Array.isArray(response.records) ? response.records.map(shape) : [];
|
|
1133
1146
|
const summary = items.length === 0 ? `Zone ${zone.domainName} has no records yet.` : `Returned ${items.length} record${items.length === 1 ? "" : "s"} for ${zone.domainName}.`;
|
|
1134
1147
|
return respond({
|
|
@@ -1157,11 +1170,9 @@ defineTool({
|
|
|
1157
1170
|
},
|
|
1158
1171
|
handler: async (args, ctx) => {
|
|
1159
1172
|
const teamId = await ctx.resolveTeamId();
|
|
1160
|
-
const { zones } = await ctx.
|
|
1173
|
+
const { zones } = await ctx.hoststack.dns.listZones(teamId);
|
|
1161
1174
|
for (const zone of zones) {
|
|
1162
|
-
const { records } = await ctx.
|
|
1163
|
-
`/api/dns-zones/${teamId}/${zone.publicId}/records`
|
|
1164
|
-
);
|
|
1175
|
+
const { records } = await ctx.hoststack.dns.listRecords(teamId, zone.publicId);
|
|
1165
1176
|
const match = records.find((r) => r.publicId === args.record_id);
|
|
1166
1177
|
if (match) {
|
|
1167
1178
|
return respond({
|
|
@@ -1217,7 +1228,7 @@ defineTool({
|
|
|
1217
1228
|
const zoneInput = {};
|
|
1218
1229
|
if (args.zone_id !== void 0) zoneInput.zone_id = args.zone_id;
|
|
1219
1230
|
if (args.domain !== void 0) zoneInput.domain = args.domain;
|
|
1220
|
-
const zone = await resolveZonePublicId(ctx.
|
|
1231
|
+
const zone = await resolveZonePublicId(ctx.hoststack, teamId, zoneInput);
|
|
1221
1232
|
const body = {
|
|
1222
1233
|
type: args.type,
|
|
1223
1234
|
name: args.name,
|
|
@@ -1225,10 +1236,7 @@ defineTool({
|
|
|
1225
1236
|
};
|
|
1226
1237
|
if (args.ttl !== void 0) body.ttl = args.ttl;
|
|
1227
1238
|
if (args.priority !== void 0) body.priority = args.priority;
|
|
1228
|
-
const response = await ctx.
|
|
1229
|
-
`/api/dns-zones/${teamId}/${zone.publicId}/records`,
|
|
1230
|
-
body
|
|
1231
|
-
);
|
|
1239
|
+
const response = await ctx.hoststack.dns.createRecord(teamId, zone.publicId, body);
|
|
1232
1240
|
return respond({
|
|
1233
1241
|
summary: `Created ${args.type} ${args.name} on ${zone.domainName}.`,
|
|
1234
1242
|
data: {
|
|
@@ -1268,7 +1276,7 @@ defineTool({
|
|
|
1268
1276
|
return respondError(`Priority is required for ${args.type} records (0\u201365535).`);
|
|
1269
1277
|
}
|
|
1270
1278
|
const teamId = await ctx.resolveTeamId();
|
|
1271
|
-
const target = await findRecordZone(ctx.
|
|
1279
|
+
const target = await findRecordZone(ctx.hoststack, teamId, args.record_id);
|
|
1272
1280
|
if (!target) {
|
|
1273
1281
|
return respondError(
|
|
1274
1282
|
`Record ${args.record_id} not found on any zone owned by this team.`
|
|
@@ -1281,8 +1289,10 @@ defineTool({
|
|
|
1281
1289
|
};
|
|
1282
1290
|
if (args.ttl !== void 0) body.ttl = args.ttl;
|
|
1283
1291
|
if (args.priority !== void 0) body.priority = args.priority;
|
|
1284
|
-
const response = await ctx.
|
|
1285
|
-
|
|
1292
|
+
const response = await ctx.hoststack.dns.updateRecord(
|
|
1293
|
+
teamId,
|
|
1294
|
+
target.zone.publicId,
|
|
1295
|
+
args.record_id,
|
|
1286
1296
|
body
|
|
1287
1297
|
);
|
|
1288
1298
|
return respond({
|
|
@@ -1317,27 +1327,23 @@ defineTool({
|
|
|
1317
1327
|
},
|
|
1318
1328
|
handler: async (args, ctx) => {
|
|
1319
1329
|
const teamId = await ctx.resolveTeamId();
|
|
1320
|
-
const target = await findRecordZone(ctx.
|
|
1330
|
+
const target = await findRecordZone(ctx.hoststack, teamId, args.record_id);
|
|
1321
1331
|
if (!target) {
|
|
1322
1332
|
return respondError(
|
|
1323
1333
|
`Record ${args.record_id} not found on any zone owned by this team.`
|
|
1324
1334
|
);
|
|
1325
1335
|
}
|
|
1326
|
-
await ctx.
|
|
1327
|
-
`/api/dns-zones/${teamId}/${target.zone.publicId}/records/${args.record_id}`
|
|
1328
|
-
);
|
|
1336
|
+
await ctx.hoststack.dns.deleteRecord(teamId, target.zone.publicId, args.record_id);
|
|
1329
1337
|
return respond({
|
|
1330
1338
|
summary: `Deleted record ${args.record_id} from ${target.zone.domainName}.`,
|
|
1331
1339
|
data: { ok: true }
|
|
1332
1340
|
});
|
|
1333
1341
|
}
|
|
1334
1342
|
});
|
|
1335
|
-
async function findRecordZone(
|
|
1336
|
-
const { zones } = await
|
|
1343
|
+
async function findRecordZone(hoststack, teamId, recordPublicId) {
|
|
1344
|
+
const { zones } = await hoststack.dns.listZones(teamId);
|
|
1337
1345
|
for (const zone of zones) {
|
|
1338
|
-
const { records } = await
|
|
1339
|
-
`/api/dns-zones/${teamId}/${zone.publicId}/records`
|
|
1340
|
-
);
|
|
1346
|
+
const { records } = await hoststack.dns.listRecords(teamId, zone.publicId);
|
|
1341
1347
|
const match = records.find((r) => r.publicId === recordPublicId);
|
|
1342
1348
|
if (match) return { zone, record: match };
|
|
1343
1349
|
}
|
|
@@ -1497,6 +1503,7 @@ defineTool({
|
|
|
1497
1503
|
' - key: env-var name (e.g. "DATABASE_URL").',
|
|
1498
1504
|
" - value: new value (will be encrypted at rest if is_secret=true).",
|
|
1499
1505
|
" - 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.",
|
|
1506
|
+
' - 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.',
|
|
1500
1507
|
"",
|
|
1501
1508
|
'Returns: { envVar: EnvVar, action: "created" | "updated" }.',
|
|
1502
1509
|
"",
|
|
@@ -1506,7 +1513,8 @@ defineTool({
|
|
|
1506
1513
|
service_id: z9.string().describe("Service publicId."),
|
|
1507
1514
|
key: z9.string().min(1).max(128).describe("Env-var key."),
|
|
1508
1515
|
value: z9.string().describe("New value."),
|
|
1509
|
-
is_secret: z9.boolean().optional().describe("Mark as secret (encrypted, masked on read). Default true.")
|
|
1516
|
+
is_secret: z9.boolean().optional().describe("Mark as secret (encrypted, masked on read). Default true."),
|
|
1517
|
+
target: z9.enum(["build", "runtime", "both"]).optional().describe("Injection target: build, runtime, or both. Default both.")
|
|
1510
1518
|
},
|
|
1511
1519
|
handler: async (args, ctx) => {
|
|
1512
1520
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -1515,6 +1523,7 @@ defineTool({
|
|
|
1515
1523
|
if (match) {
|
|
1516
1524
|
const updatePayload = { value: args.value };
|
|
1517
1525
|
if (args.is_secret !== void 0) updatePayload.isSecret = args.is_secret;
|
|
1526
|
+
if (args.target !== void 0) updatePayload.target = args.target;
|
|
1518
1527
|
const response2 = await ctx.hoststack.envVars.update(
|
|
1519
1528
|
teamId,
|
|
1520
1529
|
args.service_id,
|
|
@@ -1530,7 +1539,8 @@ defineTool({
|
|
|
1530
1539
|
const response = await ctx.hoststack.envVars.create(teamId, args.service_id, {
|
|
1531
1540
|
key: args.key,
|
|
1532
1541
|
value: args.value,
|
|
1533
|
-
isSecret: args.is_secret ?? true
|
|
1542
|
+
isSecret: args.is_secret ?? true,
|
|
1543
|
+
...args.target !== void 0 ? { target: args.target } : {}
|
|
1534
1544
|
});
|
|
1535
1545
|
const data = {
|
|
1536
1546
|
envVar: shapeEnvVar(response.envVar),
|
|
@@ -1586,7 +1596,7 @@ defineTool({
|
|
|
1586
1596
|
"",
|
|
1587
1597
|
"Inputs:",
|
|
1588
1598
|
" - service_id: publicId of the service.",
|
|
1589
|
-
|
|
1599
|
+
' - 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.',
|
|
1590
1600
|
"",
|
|
1591
1601
|
"Returns: { ok: true }. Re-list with list_env_vars to confirm the new state.",
|
|
1592
1602
|
"",
|
|
@@ -1598,7 +1608,8 @@ defineTool({
|
|
|
1598
1608
|
z9.object({
|
|
1599
1609
|
key: z9.string().min(1).max(128),
|
|
1600
1610
|
value: z9.string(),
|
|
1601
|
-
is_secret: z9.boolean().optional()
|
|
1611
|
+
is_secret: z9.boolean().optional(),
|
|
1612
|
+
target: z9.enum(["build", "runtime", "both"]).optional()
|
|
1602
1613
|
})
|
|
1603
1614
|
).max(500).describe("Array of env-var rows. Hard cap 500.")
|
|
1604
1615
|
},
|
|
@@ -1611,6 +1622,7 @@ defineTool({
|
|
|
1611
1622
|
value: v.value
|
|
1612
1623
|
};
|
|
1613
1624
|
if (v.is_secret !== void 0) row.isSecret = v.is_secret;
|
|
1625
|
+
if (v.target !== void 0) row.target = v.target;
|
|
1614
1626
|
return row;
|
|
1615
1627
|
})
|
|
1616
1628
|
};
|
|
@@ -1632,6 +1644,10 @@ defineTool({
|
|
|
1632
1644
|
"",
|
|
1633
1645
|
"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.",
|
|
1634
1646
|
"",
|
|
1647
|
+
// §2.2: these are project deployment environments, not cloud Dev Boxes — point agents at the
|
|
1648
|
+
// right surface so the two "dev" concepts stay distinct (bidirectional cross-reference).
|
|
1649
|
+
"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.",
|
|
1650
|
+
"",
|
|
1635
1651
|
"Inputs:",
|
|
1636
1652
|
' - project_id: project publicId (e.g. "prj_abc123").',
|
|
1637
1653
|
"",
|
|
@@ -1658,10 +1674,15 @@ defineTool({
|
|
|
1658
1674
|
"",
|
|
1659
1675
|
"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.",
|
|
1660
1676
|
"",
|
|
1677
|
+
// §2.1/§2.2: steer callers away from the agentic cloud Dev Box tools (which live in
|
|
1678
|
+
// services.ts) — an environment is a deployment target, NOT a Dev Box. Bidirectional with
|
|
1679
|
+
// the cross-references those dev-box tools point back here.
|
|
1680
|
+
'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.',
|
|
1681
|
+
"",
|
|
1661
1682
|
"Inputs:",
|
|
1662
1683
|
" - project_id: project publicId.",
|
|
1663
1684
|
" - name: human-readable name (1\u201364 chars). Shown in the env switcher.",
|
|
1664
|
-
' - type: "production" | "staging" | "development" | "preview". Determines the hostname suffix on services in this env (production stays clean; others get -staging / -dev / -preview).',
|
|
1685
|
+
' - 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.',
|
|
1665
1686
|
" - is_protected (optional): require admin role for destructive actions in this env. Default false.",
|
|
1666
1687
|
"",
|
|
1667
1688
|
"Returns: { environment: Environment }.",
|
|
@@ -1671,7 +1692,10 @@ defineTool({
|
|
|
1671
1692
|
input: {
|
|
1672
1693
|
project_id: z10.string().describe("Project publicId."),
|
|
1673
1694
|
name: z10.string().min(1).max(64).describe("Environment name (1\u201364 chars)."),
|
|
1674
|
-
type: z10.enum(["production", "staging", "development", "preview"]).describe(
|
|
1695
|
+
type: z10.enum(["production", "staging", "development", "preview"]).describe(
|
|
1696
|
+
// §2.1: "development" here is a project deployment env, NOT a cloud Dev Box.
|
|
1697
|
+
'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.'
|
|
1698
|
+
),
|
|
1675
1699
|
is_protected: z10.boolean().optional().describe("Require admin role for destructive actions. Default false.")
|
|
1676
1700
|
},
|
|
1677
1701
|
handler: async (args, ctx) => {
|
|
@@ -1763,6 +1787,14 @@ defineTool({
|
|
|
1763
1787
|
});
|
|
1764
1788
|
|
|
1765
1789
|
// src/tools/meta.ts
|
|
1790
|
+
var DEV_ENV_TOOL_NAMES = [
|
|
1791
|
+
"create_dev_environment",
|
|
1792
|
+
"create_standalone_dev_environment",
|
|
1793
|
+
"spin_up_dev_environment",
|
|
1794
|
+
"list_dev_environments",
|
|
1795
|
+
"resize_dev_environment",
|
|
1796
|
+
"delete_dev_environment"
|
|
1797
|
+
];
|
|
1766
1798
|
defineTool({
|
|
1767
1799
|
name: "get_me",
|
|
1768
1800
|
category: "meta",
|
|
@@ -1787,6 +1819,36 @@ defineTool({
|
|
|
1787
1819
|
return respond({ summary: `Authenticated as ${userEmail}${teamLabel}.`, data });
|
|
1788
1820
|
}
|
|
1789
1821
|
});
|
|
1822
|
+
defineTool({
|
|
1823
|
+
name: "describe_mcp",
|
|
1824
|
+
category: "meta",
|
|
1825
|
+
description: [
|
|
1826
|
+
"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.",
|
|
1827
|
+
"",
|
|
1828
|
+
"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).",
|
|
1829
|
+
"",
|
|
1830
|
+
'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.',
|
|
1831
|
+
"",
|
|
1832
|
+
"Inputs: none.",
|
|
1833
|
+
"",
|
|
1834
|
+
"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.",
|
|
1835
|
+
"",
|
|
1836
|
+
'Example: describe_mcp() \u2192 { version: "0.14.0", toolCount: 30, devEnvTools: ["create_dev_environment", \u2026], devEnvToolsPresent: true }'
|
|
1837
|
+
].join("\n"),
|
|
1838
|
+
input: {},
|
|
1839
|
+
handler: async (_args, _ctx) => {
|
|
1840
|
+
const registered = new Set(listToolDefinitions().map((t) => t.name));
|
|
1841
|
+
const devEnvToolsPresent = DEV_ENV_TOOL_NAMES.every((name) => registered.has(name));
|
|
1842
|
+
const data = {
|
|
1843
|
+
version: MCP_VERSION,
|
|
1844
|
+
toolCount: registered.size,
|
|
1845
|
+
devEnvTools: [...DEV_ENV_TOOL_NAMES],
|
|
1846
|
+
devEnvToolsPresent
|
|
1847
|
+
};
|
|
1848
|
+
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).`;
|
|
1849
|
+
return respond({ summary, data });
|
|
1850
|
+
}
|
|
1851
|
+
});
|
|
1790
1852
|
|
|
1791
1853
|
// src/tools/notifications.ts
|
|
1792
1854
|
import { z as z11 } from "zod";
|
|
@@ -2111,11 +2173,19 @@ var SERVICE_PLANS = [
|
|
|
2111
2173
|
"pico",
|
|
2112
2174
|
"nano",
|
|
2113
2175
|
"micro",
|
|
2114
|
-
"
|
|
2176
|
+
"small",
|
|
2115
2177
|
"standard",
|
|
2178
|
+
"large",
|
|
2179
|
+
"xlarge",
|
|
2116
2180
|
"pro_standard",
|
|
2117
2181
|
"pro_large"
|
|
2118
2182
|
];
|
|
2183
|
+
var DEV_ENV_MIN_SIZE = "standard";
|
|
2184
|
+
function coerceDevEnvSize(value) {
|
|
2185
|
+
const minIdx = SERVICE_PLANS.indexOf(DEV_ENV_MIN_SIZE);
|
|
2186
|
+
const idx = value ? SERVICE_PLANS.indexOf(value) : -1;
|
|
2187
|
+
return idx >= minIdx ? value : DEV_ENV_MIN_SIZE;
|
|
2188
|
+
}
|
|
2119
2189
|
defineTool({
|
|
2120
2190
|
name: "list_services",
|
|
2121
2191
|
category: "services",
|
|
@@ -2124,11 +2194,14 @@ defineTool({
|
|
|
2124
2194
|
"",
|
|
2125
2195
|
"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.",
|
|
2126
2196
|
"",
|
|
2197
|
+
"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.",
|
|
2198
|
+
"",
|
|
2127
2199
|
"Inputs (all optional):",
|
|
2128
2200
|
' - project_id: narrow to one project (numeric id or publicId "prj_\u2026").',
|
|
2129
2201
|
' - environment_id: narrow to one environment (numeric id or publicId "env_\u2026").',
|
|
2130
2202
|
' - status: "active" | "deploying" | "suspended" | "failed" | "not_deployed".',
|
|
2131
2203
|
' - type: "web_service" | "private_service" | "worker" | "cron_job" | "static_site".',
|
|
2204
|
+
" - dev_environment: include Dev Boxes in the results (excluded by default).",
|
|
2132
2205
|
"",
|
|
2133
2206
|
"Returns: { items: Service[] } \u2014 each service includes id, publicId, name, type, status, projectId, repoUrl, branch, runtime, createdAt.",
|
|
2134
2207
|
"",
|
|
@@ -2138,11 +2211,15 @@ defineTool({
|
|
|
2138
2211
|
project_id: z13.union([z13.number().int().positive(), z13.string()]).optional().describe("Project filter \u2014 numeric id or publicId."),
|
|
2139
2212
|
environment_id: z13.union([z13.number().int().positive(), z13.string()]).optional().describe("Environment filter \u2014 numeric id or publicId."),
|
|
2140
2213
|
status: z13.enum(["active", "deploying", "suspended", "failed", "not_deployed"]).optional().describe("Filter by current runtime status."),
|
|
2141
|
-
type: z13.enum(["web_service", "private_service", "worker", "cron_job", "static_site"]).optional().describe("Filter by service type.")
|
|
2214
|
+
type: z13.enum(["web_service", "private_service", "worker", "cron_job", "static_site"]).optional().describe("Filter by service type."),
|
|
2215
|
+
dev_environment: z13.boolean().optional().describe(
|
|
2216
|
+
"Include agentic Dev Boxes in the results (excluded by default; see list_dev_environments)."
|
|
2217
|
+
)
|
|
2142
2218
|
},
|
|
2143
2219
|
handler: async (args, ctx) => {
|
|
2144
2220
|
const teamId = await ctx.resolveTeamId();
|
|
2145
2221
|
const filters = {};
|
|
2222
|
+
if (args.dev_environment) filters.devEnvironment = true;
|
|
2146
2223
|
if (args.project_id !== void 0) {
|
|
2147
2224
|
const resolved = await ctx.hoststack.resolveId(args.project_id, {
|
|
2148
2225
|
kind: "project",
|
|
@@ -2165,7 +2242,8 @@ defineTool({
|
|
|
2165
2242
|
args.project_id !== void 0 ? `project=${args.project_id}` : null,
|
|
2166
2243
|
args.environment_id !== void 0 ? `env=${args.environment_id}` : null,
|
|
2167
2244
|
args.status ? `status=${args.status}` : null,
|
|
2168
|
-
args.type ? `type=${args.type}` : null
|
|
2245
|
+
args.type ? `type=${args.type}` : null,
|
|
2246
|
+
args.dev_environment ? "devBoxes=included" : null
|
|
2169
2247
|
].filter(Boolean).join(", ");
|
|
2170
2248
|
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}` : ""}.`;
|
|
2171
2249
|
return respond({ summary, data });
|
|
@@ -2256,14 +2334,16 @@ defineTool({
|
|
|
2256
2334
|
name: "create_dev_environment",
|
|
2257
2335
|
category: "services",
|
|
2258
2336
|
description: [
|
|
2259
|
-
|
|
2337
|
+
'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.',
|
|
2260
2338
|
"",
|
|
2261
|
-
|
|
2339
|
+
'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.',
|
|
2340
|
+
"",
|
|
2341
|
+
`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.`,
|
|
2262
2342
|
"",
|
|
2263
2343
|
"Inputs:",
|
|
2264
2344
|
' - project_id: numeric id or publicId ("prj_\u2026") of the target project.',
|
|
2265
2345
|
' - name (optional): service name (default "dev-environment").',
|
|
2266
|
-
' - plan (optional):
|
|
2346
|
+
' - 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".',
|
|
2267
2347
|
" - disk_gb (optional): /workspace volume size in GB (default 10, 1\u2013100).",
|
|
2268
2348
|
" - hoststack_api_key (optional): sets HOSTSTACK_API_KEY so the hoststack MCP works inside the container.",
|
|
2269
2349
|
" - poststack_api_key (optional): sets POSTSTACK_API_KEY so the poststack MCP works inside the container.",
|
|
@@ -2276,7 +2356,9 @@ defineTool({
|
|
|
2276
2356
|
input: {
|
|
2277
2357
|
project_id: z13.union([z13.number().int().positive(), z13.string()]).describe("Target project \u2014 numeric id or publicId."),
|
|
2278
2358
|
name: z13.string().min(1).max(100).optional().describe('Service name (default "dev-environment").'),
|
|
2279
|
-
plan: z13.enum(SERVICE_PLANS).optional().describe(
|
|
2359
|
+
plan: z13.enum(SERVICE_PLANS).optional().describe(
|
|
2360
|
+
'Box size (default "standard" \u2014 2 GB, the OOM-safe floor; a smaller plan is clamped up to "standard").'
|
|
2361
|
+
),
|
|
2280
2362
|
disk_gb: z13.number().int().min(1).max(100).optional().describe("/workspace volume size in GB (default 10)."),
|
|
2281
2363
|
hoststack_api_key: z13.string().optional().describe("Value for HOSTSTACK_API_KEY (enables the hoststack MCP in-container)."),
|
|
2282
2364
|
poststack_api_key: z13.string().optional().describe("Value for POSTSTACK_API_KEY (enables the poststack MCP in-container)."),
|
|
@@ -2293,68 +2375,94 @@ defineTool({
|
|
|
2293
2375
|
});
|
|
2294
2376
|
const name = args.name ?? "dev-environment";
|
|
2295
2377
|
const sizeGb = args.disk_gb ?? DEV_ENV_VOLUME.sizeGb;
|
|
2378
|
+
const plan = coerceDevEnvSize(args.plan);
|
|
2296
2379
|
const createInput = {
|
|
2297
2380
|
name,
|
|
2298
2381
|
type: "private_service",
|
|
2299
2382
|
projectId,
|
|
2300
2383
|
dockerImage: DEV_ENV_IMAGE,
|
|
2301
|
-
autoDeploy: false
|
|
2384
|
+
autoDeploy: false,
|
|
2385
|
+
plan
|
|
2302
2386
|
};
|
|
2303
|
-
if (args.plan !== void 0) createInput.plan = args.plan;
|
|
2304
2387
|
const created = await ctx.hoststack.services.create(teamId, createInput);
|
|
2305
2388
|
const service = created.service;
|
|
2306
|
-
const
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
if (args.branch)
|
|
2389
|
+
const rollback = async () => {
|
|
2390
|
+
try {
|
|
2391
|
+
await ctx.hoststack.services.tearDownDevEnvironment(teamId, service.id);
|
|
2392
|
+
} catch {
|
|
2393
|
+
try {
|
|
2394
|
+
await ctx.hoststack.services.delete(teamId, service.id);
|
|
2395
|
+
} catch {
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
};
|
|
2399
|
+
try {
|
|
2400
|
+
const envVars = [];
|
|
2401
|
+
if (args.hoststack_api_key)
|
|
2402
|
+
envVars.push({
|
|
2403
|
+
key: "HOSTSTACK_API_KEY",
|
|
2404
|
+
value: args.hoststack_api_key,
|
|
2405
|
+
isSecret: true
|
|
2406
|
+
});
|
|
2407
|
+
if (args.poststack_api_key)
|
|
2326
2408
|
envVars.push({
|
|
2327
|
-
key: "
|
|
2328
|
-
value: args.
|
|
2409
|
+
key: "POSTSTACK_API_KEY",
|
|
2410
|
+
value: args.poststack_api_key,
|
|
2411
|
+
isSecret: true
|
|
2412
|
+
});
|
|
2413
|
+
if (args.repo_url) {
|
|
2414
|
+
envVars.push({
|
|
2415
|
+
key: "HOSTSTACK_DEVENV_REPO_URL",
|
|
2416
|
+
value: args.repo_url,
|
|
2329
2417
|
isSecret: false
|
|
2330
2418
|
});
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2419
|
+
if (args.branch)
|
|
2420
|
+
envVars.push({
|
|
2421
|
+
key: "HOSTSTACK_DEVENV_BRANCH",
|
|
2422
|
+
value: args.branch,
|
|
2423
|
+
isSecret: false
|
|
2424
|
+
});
|
|
2425
|
+
}
|
|
2426
|
+
if (envVars.length > 0) {
|
|
2427
|
+
await ctx.hoststack.envVars.bulkSet(teamId, service.id, { vars: envVars });
|
|
2428
|
+
}
|
|
2429
|
+
try {
|
|
2430
|
+
await ctx.hoststack.volumes.create(teamId, service.id, {
|
|
2431
|
+
name: DEV_ENV_VOLUME.name,
|
|
2432
|
+
mountPath: DEV_ENV_VOLUME.mountPath,
|
|
2433
|
+
sizeGb
|
|
2434
|
+
});
|
|
2435
|
+
} catch (error) {
|
|
2436
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2437
|
+
await rollback();
|
|
2438
|
+
return respondError(
|
|
2439
|
+
`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}`,
|
|
2440
|
+
{ volumeError: reason, rolledBack: true }
|
|
2441
|
+
);
|
|
2442
|
+
}
|
|
2443
|
+
const deploy = await ctx.hoststack.deploys.trigger(teamId, service.id);
|
|
2444
|
+
const deployId = deploy.deploy?.id ?? null;
|
|
2445
|
+
return respond({
|
|
2446
|
+
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.`,
|
|
2447
|
+
data: { service: shapeService(service), volumeAttached: true, deployId }
|
|
2341
2448
|
});
|
|
2342
|
-
|
|
2343
|
-
|
|
2449
|
+
} catch (error) {
|
|
2450
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2451
|
+
await rollback();
|
|
2452
|
+
return respondError(
|
|
2453
|
+
`Failed to provision Dev Box "${name}" \u2014 rolled back the partially-created box. Error: ${reason}`,
|
|
2454
|
+
{ rolledBack: true }
|
|
2455
|
+
);
|
|
2344
2456
|
}
|
|
2345
|
-
const deploy = await ctx.hoststack.deploys.trigger(teamId, service.id);
|
|
2346
|
-
const deployId = deploy.deploy?.id ?? null;
|
|
2347
|
-
return respond({
|
|
2348
|
-
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.`,
|
|
2349
|
-
data: { service: shapeService(service), volumeAttached, deployId }
|
|
2350
|
-
});
|
|
2351
2457
|
}
|
|
2352
2458
|
});
|
|
2353
2459
|
defineTool({
|
|
2354
2460
|
name: "spin_up_dev_environment",
|
|
2355
2461
|
category: "services",
|
|
2356
2462
|
description: [
|
|
2357
|
-
"Spin up an agentic
|
|
2463
|
+
"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`.",
|
|
2464
|
+
"",
|
|
2465
|
+
"This makes a cloud Dev Box, NOT a project deploy environment \u2014 for a -dev/-staging deploy target use create_environment instead.",
|
|
2358
2466
|
"",
|
|
2359
2467
|
'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.',
|
|
2360
2468
|
"",
|
|
@@ -2397,7 +2505,9 @@ defineTool({
|
|
|
2397
2505
|
name: "delete_dev_environment",
|
|
2398
2506
|
category: "services",
|
|
2399
2507
|
description: [
|
|
2400
|
-
"Tear down an agentic
|
|
2508
|
+
"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.",
|
|
2509
|
+
"",
|
|
2510
|
+
"This deletes a cloud Dev Box, NOT a project deploy environment \u2014 to remove a -dev/-staging deploy target use delete_environment instead.",
|
|
2401
2511
|
"",
|
|
2402
2512
|
'When to use: "delete / tear down the dev environment", once you have shipped the fix and no longer need the box.',
|
|
2403
2513
|
"",
|
|
@@ -2424,15 +2534,56 @@ defineTool({
|
|
|
2424
2534
|
});
|
|
2425
2535
|
}
|
|
2426
2536
|
});
|
|
2537
|
+
defineTool({
|
|
2538
|
+
name: "resize_dev_environment",
|
|
2539
|
+
category: "services",
|
|
2540
|
+
description: [
|
|
2541
|
+
"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).",
|
|
2542
|
+
"",
|
|
2543
|
+
"This resizes a cloud Dev Box, NOT a project deploy environment.",
|
|
2544
|
+
"",
|
|
2545
|
+
"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.",
|
|
2546
|
+
"",
|
|
2547
|
+
"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.",
|
|
2548
|
+
"",
|
|
2549
|
+
"Inputs:",
|
|
2550
|
+
" - service_id: the box to resize \u2014 numeric id or publicId.",
|
|
2551
|
+
' - size: target tier \u2014 one of the service catalog sizes (e.g. "standard", "large", "xlarge").',
|
|
2552
|
+
"",
|
|
2553
|
+
"Returns: { service } with the new plan.",
|
|
2554
|
+
"",
|
|
2555
|
+
'Example: resize_dev_environment({ service_id: "svc_skyskraber_dev", size: "large" }) \u2192 bumps the box to the large tier, applied live.'
|
|
2556
|
+
].join("\n"),
|
|
2557
|
+
input: {
|
|
2558
|
+
service_id: z13.union([z13.number().int().positive(), z13.string()]).describe("The box to resize \u2014 numeric id or publicId."),
|
|
2559
|
+
size: z13.enum(SERVICE_PLANS).describe(
|
|
2560
|
+
'Target size tier (service catalog size, e.g. "standard", "large", "xlarge").'
|
|
2561
|
+
)
|
|
2562
|
+
},
|
|
2563
|
+
handler: async (args, ctx) => {
|
|
2564
|
+
const teamId = await ctx.resolveTeamId();
|
|
2565
|
+
const serviceId = await ctx.hoststack.resolveId(args.service_id, {
|
|
2566
|
+
kind: "service",
|
|
2567
|
+
teamId
|
|
2568
|
+
});
|
|
2569
|
+
const { service } = await ctx.hoststack.services.resize(teamId, serviceId, args.size);
|
|
2570
|
+
return respond({
|
|
2571
|
+
summary: `Resized to ${service.plan} \u2014 memory/CPU applied live; disk grows on next recreate.`,
|
|
2572
|
+
data: { service: shapeService(service) }
|
|
2573
|
+
});
|
|
2574
|
+
}
|
|
2575
|
+
});
|
|
2427
2576
|
defineTool({
|
|
2428
2577
|
name: "list_dev_environments",
|
|
2429
2578
|
category: "services",
|
|
2430
2579
|
description: [
|
|
2431
|
-
`List the team's
|
|
2580
|
+
`List the team's cloud Dev Boxes (the dashboard "Dev Boxes" / Development section), each annotated with the companion services attached to it.`,
|
|
2581
|
+
"",
|
|
2582
|
+
"These are cloud Dev Boxes, NOT project deploy environments \u2014 to list a project's deploy targets (Production / Staging / Preview) use list_environments instead.",
|
|
2432
2583
|
"",
|
|
2433
2584
|
`When to use: "show my dev environments", before opening/tearing one down, to find a box's id.`,
|
|
2434
2585
|
"",
|
|
2435
|
-
'Returns: { items: [{ ...service, devUrl, databases }] } where `databases` lists the companion engines wired into the box (e.g. ["postgres","redis"]).',
|
|
2586
|
+
'Returns: { items: [{ ...service, devUrl, databases, exitReason, recommendedSize }] } where `databases` lists the companion engines wired into the box (e.g. ["postgres","redis"]). `exitReason` is "oom_killed" / "crashed" / null for the box\'s last container exit; when it is "oom_killed", `recommendedSize` is the next tier up to rescale to (use resize_dev_environment).',
|
|
2436
2587
|
"",
|
|
2437
2588
|
"Example: list_dev_environments() \u2192 every dev box for the active team."
|
|
2438
2589
|
].join("\n"),
|
|
@@ -2440,47 +2591,99 @@ defineTool({
|
|
|
2440
2591
|
handler: async (_args, ctx) => {
|
|
2441
2592
|
const teamId = await ctx.resolveTeamId();
|
|
2442
2593
|
const { environments } = await ctx.hoststack.services.listDevEnvironments(teamId);
|
|
2594
|
+
const oomCount = environments.filter((e) => e.exitReason === "oom_killed").length;
|
|
2443
2595
|
return respond({
|
|
2444
|
-
summary: `${environments.length} dev environment${environments.length === 1 ? "" : "s"}
|
|
2596
|
+
summary: `${environments.length} dev environment${environments.length === 1 ? "" : "s"}.` + (oomCount > 0 ? ` ${oomCount} recently OOM-killed \u2014 consider resize_dev_environment.` : ""),
|
|
2445
2597
|
data: {
|
|
2446
2598
|
items: environments.map((env) => ({
|
|
2447
2599
|
...shapeService(env),
|
|
2448
2600
|
devUrl: env.devUrl ?? null,
|
|
2449
|
-
databases: env.databases ?? []
|
|
2601
|
+
databases: env.databases ?? [],
|
|
2602
|
+
exitReason: env.exitReason ?? null,
|
|
2603
|
+
recommendedSize: env.recommendedSize ?? null
|
|
2450
2604
|
}))
|
|
2451
2605
|
}
|
|
2452
2606
|
});
|
|
2453
2607
|
}
|
|
2454
2608
|
});
|
|
2609
|
+
defineTool({
|
|
2610
|
+
name: "list_templates",
|
|
2611
|
+
category: "services",
|
|
2612
|
+
description: [
|
|
2613
|
+
"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.",
|
|
2614
|
+
"",
|
|
2615
|
+
"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.",
|
|
2616
|
+
"",
|
|
2617
|
+
'Returns: { templates: [{ id, name, image, volume: { name, mountPath, sizeGb }, minPlan, companions }] }. Today there is one preset ("dev-environment").',
|
|
2618
|
+
"",
|
|
2619
|
+
'Example: list_templates() \u2192 { templates: [{ id: "dev-environment", image: "hoststack/dev-env:latest", volume: { mountPath: "/workspace", sizeGb: 10 }, minPlan: "standard", companions: ["postgres","redis","meilisearch"] }] }'
|
|
2620
|
+
].join("\n"),
|
|
2621
|
+
input: {},
|
|
2622
|
+
handler: async () => {
|
|
2623
|
+
const templates = [
|
|
2624
|
+
{
|
|
2625
|
+
id: "dev-environment",
|
|
2626
|
+
name: "AI Dev Environment",
|
|
2627
|
+
image: DEV_ENV_IMAGE,
|
|
2628
|
+
volume: {
|
|
2629
|
+
name: DEV_ENV_VOLUME.name,
|
|
2630
|
+
mountPath: DEV_ENV_VOLUME.mountPath,
|
|
2631
|
+
sizeGb: DEV_ENV_VOLUME.sizeGb
|
|
2632
|
+
},
|
|
2633
|
+
/** OOM-safe plan floor — a coding agent + build needs at least this. */
|
|
2634
|
+
minPlan: DEV_ENV_MIN_SIZE,
|
|
2635
|
+
/** Companion engines you can attach (fresh + empty), like local `make db-up`. */
|
|
2636
|
+
companions: ["postgres", "redis", "meilisearch"]
|
|
2637
|
+
}
|
|
2638
|
+
];
|
|
2639
|
+
return respond({
|
|
2640
|
+
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}").`,
|
|
2641
|
+
data: { templates }
|
|
2642
|
+
});
|
|
2643
|
+
}
|
|
2644
|
+
});
|
|
2455
2645
|
defineTool({
|
|
2456
2646
|
name: "create_standalone_dev_environment",
|
|
2457
2647
|
category: "services",
|
|
2458
2648
|
description: [
|
|
2459
|
-
"Create a STANDALONE
|
|
2649
|
+
"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.",
|
|
2650
|
+
"",
|
|
2651
|
+
"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.",
|
|
2460
2652
|
"",
|
|
2461
2653
|
'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).',
|
|
2462
2654
|
"",
|
|
2463
2655
|
"Inputs:",
|
|
2464
|
-
" - name: the
|
|
2656
|
+
" - name: the Dev Box name.",
|
|
2465
2657
|
' - 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).',
|
|
2466
2658
|
" - github_repo_id (for github_repo): numeric id of a connected GitHub repo.",
|
|
2467
2659
|
" - clone_url (for url): an http(s) git clone URL.",
|
|
2468
2660
|
" - branch (optional): branch to clone.",
|
|
2469
2661
|
' - databases (optional): companion services to attach \u2014 any of "postgres", "redis", "meilisearch".',
|
|
2470
|
-
' - plan (optional): box size (
|
|
2662
|
+
' - 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.',
|
|
2663
|
+
" - 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.",
|
|
2471
2664
|
"",
|
|
2472
2665
|
"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.",
|
|
2473
2666
|
"",
|
|
2474
2667
|
'Example: create_standalone_dev_environment({ name: "app-dev", source_kind: "github_repo", github_repo_id: 42, databases: ["postgres","redis"] })'
|
|
2475
2668
|
].join("\n"),
|
|
2476
2669
|
input: {
|
|
2477
|
-
name: z13.string().min(1).max(100).describe("Dev
|
|
2670
|
+
name: z13.string().min(1).max(100).describe("Dev Box name."),
|
|
2478
2671
|
source_kind: z13.enum(["github_repo", "url", "blank"]).describe("Where the code comes from."),
|
|
2479
2672
|
github_repo_id: z13.number().int().positive().optional().describe('Connected GitHub repo id (required when source_kind="github_repo").'),
|
|
2480
2673
|
clone_url: z13.string().url().optional().describe('http(s) git clone URL (required when source_kind="url").'),
|
|
2481
2674
|
branch: z13.string().min(1).max(255).optional().describe("Branch to clone."),
|
|
2482
2675
|
databases: z13.array(z13.enum(["postgres", "redis", "meilisearch"])).optional().describe("Companion services to attach (fresh + empty)."),
|
|
2483
|
-
plan: z13.enum(SERVICE_PLANS).optional().describe(
|
|
2676
|
+
plan: z13.enum(SERVICE_PLANS).optional().describe(
|
|
2677
|
+
'Box size (default "standard" \u2014 2 GB; a smaller plan is floored to "standard").'
|
|
2678
|
+
),
|
|
2679
|
+
agent_accounts: z13.array(
|
|
2680
|
+
z13.object({
|
|
2681
|
+
provider: z13.enum(["claude", "codex", "opencode"]),
|
|
2682
|
+
account_id: z13.number().int().positive()
|
|
2683
|
+
})
|
|
2684
|
+
).max(3).optional().describe(
|
|
2685
|
+
"Bind saved agent logins by account id per provider. Omit to inherit the box owner's default logins automatically."
|
|
2686
|
+
)
|
|
2484
2687
|
},
|
|
2485
2688
|
handler: async (args, ctx) => {
|
|
2486
2689
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -2516,7 +2719,13 @@ defineTool({
|
|
|
2516
2719
|
name: args.name,
|
|
2517
2720
|
source,
|
|
2518
2721
|
...args.databases ? { databases: args.databases } : {},
|
|
2519
|
-
...args.plan ? { plan: args.plan } : {}
|
|
2722
|
+
...args.plan ? { plan: args.plan } : {},
|
|
2723
|
+
...args.agent_accounts && args.agent_accounts.length > 0 ? {
|
|
2724
|
+
agentAccounts: args.agent_accounts.map((a) => ({
|
|
2725
|
+
provider: a.provider,
|
|
2726
|
+
accountId: a.account_id
|
|
2727
|
+
}))
|
|
2728
|
+
} : {}
|
|
2520
2729
|
};
|
|
2521
2730
|
const result = await ctx.hoststack.services.createDevEnvironment(teamId, input);
|
|
2522
2731
|
return respond({
|
|
@@ -3136,7 +3345,7 @@ defineTool({
|
|
|
3136
3345
|
|
|
3137
3346
|
// src/server-factory.ts
|
|
3138
3347
|
var PACKAGE_NAME = "hoststack";
|
|
3139
|
-
var PACKAGE_VERSION =
|
|
3348
|
+
var PACKAGE_VERSION = MCP_VERSION;
|
|
3140
3349
|
function createMcpServer(options) {
|
|
3141
3350
|
const baseUrl = (options.baseUrl ?? "https://hoststack.dev").replace(/\/$/, "");
|
|
3142
3351
|
const hoststack = new HostStack({ apiKey: options.apiKey, baseUrl });
|