@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/hoststack-mcp.js
CHANGED
|
@@ -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 = [
|
|
1054
|
-
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
1125
|
-
const response = await ctx.
|
|
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.
|
|
1172
|
+
const { zones } = await ctx.hoststack.dns.listZones(teamId);
|
|
1157
1173
|
for (const zone of zones) {
|
|
1158
|
-
const { records } = await ctx.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
1281
|
-
|
|
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.
|
|
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.
|
|
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(
|
|
1332
|
-
const { zones } = await
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
"
|
|
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
|
-
|
|
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 /
|
|
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):
|
|
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.",
|
|
@@ -2272,7 +2355,9 @@ defineTool({
|
|
|
2272
2355
|
input: {
|
|
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
|
-
plan: z13.enum(SERVICE_PLANS).optional().describe(
|
|
2358
|
+
plan: z13.enum(SERVICE_PLANS).optional().describe(
|
|
2359
|
+
'Box size (default "standard" \u2014 2 GB, the OOM-safe floor; a smaller plan is clamped up to "standard").'
|
|
2360
|
+
),
|
|
2276
2361
|
disk_gb: z13.number().int().min(1).max(100).optional().describe("/workspace volume size in GB (default 10)."),
|
|
2277
2362
|
hoststack_api_key: z13.string().optional().describe("Value for HOSTSTACK_API_KEY (enables the hoststack MCP in-container)."),
|
|
2278
2363
|
poststack_api_key: z13.string().optional().describe("Value for POSTSTACK_API_KEY (enables the poststack MCP in-container)."),
|
|
@@ -2289,68 +2374,94 @@ defineTool({
|
|
|
2289
2374
|
});
|
|
2290
2375
|
const name = args2.name ?? "dev-environment";
|
|
2291
2376
|
const sizeGb = args2.disk_gb ?? DEV_ENV_VOLUME.sizeGb;
|
|
2377
|
+
const plan = coerceDevEnvSize(args2.plan);
|
|
2292
2378
|
const createInput = {
|
|
2293
2379
|
name,
|
|
2294
2380
|
type: "private_service",
|
|
2295
2381
|
projectId,
|
|
2296
2382
|
dockerImage: DEV_ENV_IMAGE,
|
|
2297
|
-
autoDeploy: false
|
|
2383
|
+
autoDeploy: false,
|
|
2384
|
+
plan
|
|
2298
2385
|
};
|
|
2299
|
-
if (args2.plan !== void 0) createInput.plan = args2.plan;
|
|
2300
2386
|
const created = await ctx.hoststack.services.create(teamId, createInput);
|
|
2301
2387
|
const service = created.service;
|
|
2302
|
-
const
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
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)
|
|
2401
|
+
envVars.push({
|
|
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) {
|
|
2322
2413
|
envVars.push({
|
|
2323
|
-
key: "
|
|
2324
|
-
value: args2.
|
|
2414
|
+
key: "HOSTSTACK_DEVENV_REPO_URL",
|
|
2415
|
+
value: args2.repo_url,
|
|
2325
2416
|
isSecret: false
|
|
2326
2417
|
});
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
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 }
|
|
2337
2447
|
});
|
|
2338
|
-
|
|
2339
|
-
|
|
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
|
+
);
|
|
2340
2455
|
}
|
|
2341
|
-
const deploy = await ctx.hoststack.deploys.trigger(teamId, service.id);
|
|
2342
|
-
const deployId = deploy.deploy?.id ?? null;
|
|
2343
|
-
return respond({
|
|
2344
|
-
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.`,
|
|
2345
|
-
data: { service: shapeService(service), volumeAttached, deployId }
|
|
2346
|
-
});
|
|
2347
2456
|
}
|
|
2348
2457
|
});
|
|
2349
2458
|
defineTool({
|
|
2350
2459
|
name: "spin_up_dev_environment",
|
|
2351
2460
|
category: "services",
|
|
2352
2461
|
description: [
|
|
2353
|
-
"Spin up an agentic
|
|
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.",
|
|
2354
2465
|
"",
|
|
2355
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.',
|
|
2356
2467
|
"",
|
|
@@ -2393,7 +2504,9 @@ defineTool({
|
|
|
2393
2504
|
name: "delete_dev_environment",
|
|
2394
2505
|
category: "services",
|
|
2395
2506
|
description: [
|
|
2396
|
-
"Tear down an agentic
|
|
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.",
|
|
2397
2510
|
"",
|
|
2398
2511
|
'When to use: "delete / tear down the dev environment", once you have shipped the fix and no longer need the box.',
|
|
2399
2512
|
"",
|
|
@@ -2420,15 +2533,56 @@ defineTool({
|
|
|
2420
2533
|
});
|
|
2421
2534
|
}
|
|
2422
2535
|
});
|
|
2536
|
+
defineTool({
|
|
2537
|
+
name: "resize_dev_environment",
|
|
2538
|
+
category: "services",
|
|
2539
|
+
description: [
|
|
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).",
|
|
2541
|
+
"",
|
|
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.",
|
|
2545
|
+
"",
|
|
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.",
|
|
2547
|
+
"",
|
|
2548
|
+
"Inputs:",
|
|
2549
|
+
" - service_id: the box to resize \u2014 numeric id or publicId.",
|
|
2550
|
+
' - size: target tier \u2014 one of the service catalog sizes (e.g. "standard", "large", "xlarge").',
|
|
2551
|
+
"",
|
|
2552
|
+
"Returns: { service } with the new plan.",
|
|
2553
|
+
"",
|
|
2554
|
+
'Example: resize_dev_environment({ service_id: "svc_skyskraber_dev", size: "large" }) \u2192 bumps the box to the large tier, applied live.'
|
|
2555
|
+
].join("\n"),
|
|
2556
|
+
input: {
|
|
2557
|
+
service_id: z13.union([z13.number().int().positive(), z13.string()]).describe("The box to resize \u2014 numeric id or publicId."),
|
|
2558
|
+
size: z13.enum(SERVICE_PLANS).describe(
|
|
2559
|
+
'Target size tier (service catalog size, e.g. "standard", "large", "xlarge").'
|
|
2560
|
+
)
|
|
2561
|
+
},
|
|
2562
|
+
handler: async (args2, ctx) => {
|
|
2563
|
+
const teamId = await ctx.resolveTeamId();
|
|
2564
|
+
const serviceId = await ctx.hoststack.resolveId(args2.service_id, {
|
|
2565
|
+
kind: "service",
|
|
2566
|
+
teamId
|
|
2567
|
+
});
|
|
2568
|
+
const { service } = await ctx.hoststack.services.resize(teamId, serviceId, args2.size);
|
|
2569
|
+
return respond({
|
|
2570
|
+
summary: `Resized to ${service.plan} \u2014 memory/CPU applied live; disk grows on next recreate.`,
|
|
2571
|
+
data: { service: shapeService(service) }
|
|
2572
|
+
});
|
|
2573
|
+
}
|
|
2574
|
+
});
|
|
2423
2575
|
defineTool({
|
|
2424
2576
|
name: "list_dev_environments",
|
|
2425
2577
|
category: "services",
|
|
2426
2578
|
description: [
|
|
2427
|
-
`List the team's
|
|
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.",
|
|
2428
2582
|
"",
|
|
2429
2583
|
`When to use: "show my dev environments", before opening/tearing one down, to find a box's id.`,
|
|
2430
2584
|
"",
|
|
2431
|
-
'Returns: { items: [{ ...service, devUrl, databases }] } where `databases` lists the companion engines wired into the box (e.g. ["postgres","redis"]).',
|
|
2585
|
+
'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).',
|
|
2432
2586
|
"",
|
|
2433
2587
|
"Example: list_dev_environments() \u2192 every dev box for the active team."
|
|
2434
2588
|
].join("\n"),
|
|
@@ -2436,47 +2590,99 @@ defineTool({
|
|
|
2436
2590
|
handler: async (_args, ctx) => {
|
|
2437
2591
|
const teamId = await ctx.resolveTeamId();
|
|
2438
2592
|
const { environments } = await ctx.hoststack.services.listDevEnvironments(teamId);
|
|
2593
|
+
const oomCount = environments.filter((e) => e.exitReason === "oom_killed").length;
|
|
2439
2594
|
return respond({
|
|
2440
|
-
summary: `${environments.length} dev environment${environments.length === 1 ? "" : "s"}
|
|
2595
|
+
summary: `${environments.length} dev environment${environments.length === 1 ? "" : "s"}.` + (oomCount > 0 ? ` ${oomCount} recently OOM-killed \u2014 consider resize_dev_environment.` : ""),
|
|
2441
2596
|
data: {
|
|
2442
2597
|
items: environments.map((env) => ({
|
|
2443
2598
|
...shapeService(env),
|
|
2444
2599
|
devUrl: env.devUrl ?? null,
|
|
2445
|
-
databases: env.databases ?? []
|
|
2600
|
+
databases: env.databases ?? [],
|
|
2601
|
+
exitReason: env.exitReason ?? null,
|
|
2602
|
+
recommendedSize: env.recommendedSize ?? null
|
|
2446
2603
|
}))
|
|
2447
2604
|
}
|
|
2448
2605
|
});
|
|
2449
2606
|
}
|
|
2450
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
|
+
});
|
|
2451
2644
|
defineTool({
|
|
2452
2645
|
name: "create_standalone_dev_environment",
|
|
2453
2646
|
category: "services",
|
|
2454
2647
|
description: [
|
|
2455
|
-
"Create a STANDALONE
|
|
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.",
|
|
2456
2651
|
"",
|
|
2457
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).',
|
|
2458
2653
|
"",
|
|
2459
2654
|
"Inputs:",
|
|
2460
|
-
" - name: the
|
|
2655
|
+
" - name: the Dev Box name.",
|
|
2461
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).',
|
|
2462
2657
|
" - github_repo_id (for github_repo): numeric id of a connected GitHub repo.",
|
|
2463
2658
|
" - clone_url (for url): an http(s) git clone URL.",
|
|
2464
2659
|
" - branch (optional): branch to clone.",
|
|
2465
2660
|
' - databases (optional): companion services to attach \u2014 any of "postgres", "redis", "meilisearch".',
|
|
2466
|
-
' - plan (optional): box size (
|
|
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.',
|
|
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.",
|
|
2467
2663
|
"",
|
|
2468
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.",
|
|
2469
2665
|
"",
|
|
2470
2666
|
'Example: create_standalone_dev_environment({ name: "app-dev", source_kind: "github_repo", github_repo_id: 42, databases: ["postgres","redis"] })'
|
|
2471
2667
|
].join("\n"),
|
|
2472
2668
|
input: {
|
|
2473
|
-
name: z13.string().min(1).max(100).describe("Dev
|
|
2669
|
+
name: z13.string().min(1).max(100).describe("Dev Box name."),
|
|
2474
2670
|
source_kind: z13.enum(["github_repo", "url", "blank"]).describe("Where the code comes from."),
|
|
2475
2671
|
github_repo_id: z13.number().int().positive().optional().describe('Connected GitHub repo id (required when source_kind="github_repo").'),
|
|
2476
2672
|
clone_url: z13.string().url().optional().describe('http(s) git clone URL (required when source_kind="url").'),
|
|
2477
2673
|
branch: z13.string().min(1).max(255).optional().describe("Branch to clone."),
|
|
2478
2674
|
databases: z13.array(z13.enum(["postgres", "redis", "meilisearch"])).optional().describe("Companion services to attach (fresh + empty)."),
|
|
2479
|
-
plan: z13.enum(SERVICE_PLANS).optional().describe(
|
|
2675
|
+
plan: z13.enum(SERVICE_PLANS).optional().describe(
|
|
2676
|
+
'Box size (default "standard" \u2014 2 GB; a smaller plan is floored to "standard").'
|
|
2677
|
+
),
|
|
2678
|
+
agent_accounts: z13.array(
|
|
2679
|
+
z13.object({
|
|
2680
|
+
provider: z13.enum(["claude", "codex", "opencode"]),
|
|
2681
|
+
account_id: z13.number().int().positive()
|
|
2682
|
+
})
|
|
2683
|
+
).max(3).optional().describe(
|
|
2684
|
+
"Bind saved agent logins by account id per provider. Omit to inherit the box owner's default logins automatically."
|
|
2685
|
+
)
|
|
2480
2686
|
},
|
|
2481
2687
|
handler: async (args2, ctx) => {
|
|
2482
2688
|
const teamId = await ctx.resolveTeamId();
|
|
@@ -2512,7 +2718,13 @@ defineTool({
|
|
|
2512
2718
|
name: args2.name,
|
|
2513
2719
|
source,
|
|
2514
2720
|
...args2.databases ? { databases: args2.databases } : {},
|
|
2515
|
-
...args2.plan ? { plan: args2.plan } : {}
|
|
2721
|
+
...args2.plan ? { plan: args2.plan } : {},
|
|
2722
|
+
...args2.agent_accounts && args2.agent_accounts.length > 0 ? {
|
|
2723
|
+
agentAccounts: args2.agent_accounts.map((a) => ({
|
|
2724
|
+
provider: a.provider,
|
|
2725
|
+
accountId: a.account_id
|
|
2726
|
+
}))
|
|
2727
|
+
} : {}
|
|
2516
2728
|
};
|
|
2517
2729
|
const result = await ctx.hoststack.services.createDevEnvironment(teamId, input);
|
|
2518
2730
|
return respond({
|
|
@@ -3132,7 +3344,7 @@ defineTool({
|
|
|
3132
3344
|
|
|
3133
3345
|
// src/server-factory.ts
|
|
3134
3346
|
var PACKAGE_NAME = "hoststack";
|
|
3135
|
-
var PACKAGE_VERSION =
|
|
3347
|
+
var PACKAGE_VERSION = MCP_VERSION;
|
|
3136
3348
|
function createMcpServer(options) {
|
|
3137
3349
|
const baseUrl2 = (options.baseUrl ?? "https://hoststack.dev").replace(/\/$/, "");
|
|
3138
3350
|
const hoststack = new HostStack({ apiKey: options.apiKey, baseUrl: baseUrl2 });
|