@hoststack.dev/mcp 0.9.1 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -85,12 +85,12 @@ If `HOSTSTACK_API_KEY` is set in your shell, it gets baked into the snippet; oth
85
85
 
86
86
  ## Tool inventory
87
87
 
88
- 58 tools, grouped by resource:
88
+ 60 tools, grouped by resource:
89
89
 
90
90
  | Category | Read | Write |
91
91
  | ----------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
92
92
  | **projects** | `list_projects`, `get_project` | `create_project`, `update_project` |
93
- | **services** | `list_services`, `get_service`, `get_service_metrics`, `get_service_metrics_history`, `get_service_logs`, `get_service_logs_bulk` | `update_service`, `update_service_config`, `suspend_service`, `resume_service` |
93
+ | **services** | `list_services`, `get_service`, `get_service_metrics`, `get_service_metrics_history`, `get_service_logs`, `get_service_logs_bulk` | `create_service`, `create_dev_environment`, `update_service`, `update_service_config`, `suspend_service`, `resume_service` |
94
94
  | **deploys** | `list_deploys`, `get_deploy`, `get_deploy_logs`, `diagnose_deploy` | `trigger_deploy`, `cancel_deploy` |
95
95
  | **environments** | `list_environments` | `create_environment`, `delete_environment`, `promote_deploy` |
96
96
  | **databases** | `list_databases`, `get_database`, `get_database_cluster`, `query_database` | `update_database`, `upgrade_database_to_ha` (use the dashboard for create/delete/credentials) |
@@ -109,7 +109,7 @@ A few design notes worth knowing as a caller:
109
109
  - **`set_env_var` / `delete_env_var` are key-based.** You don't need an env-var ID; the MCP looks up the existing var by key first, then patches or deletes by ID under the hood.
110
110
  - **`list_env_vars` masks secret values.** Anything stored with `is_secret: true` comes back as `••••••`. The masking happens server-side, so you can't accidentally leak a secret to the agent's context window.
111
111
  - **`get_service_logs` and `get_deploy_logs` are snapshots.** Streaming logs over MCP isn't supported — re-call the tool to get newer entries. Use the dashboard's `/dashboard/services/:id/logs` for live tails.
112
- - **No `delete_project`, `delete_service`, or `create_service`.** Destructive cascades and the wizard flow are dashboard-only — too risky / too complex for v0.x.
112
+ - **No `delete_project` or `delete_service`.** Destructive cascades are dashboard-only — too risky for an agent to call. Creating is supported via `create_service` / `create_dev_environment`.
113
113
  - **Telemetry, hosted only.** When you use `https://hoststack.dev/api/mcp`, we record one row per tool call (tool name, duration, ok/error, SHA-derived hash of input args — never the args themselves) for the analytics page and on-call alerts. Retained 30 days. The local stdio install records nothing.
114
114
 
115
115
  ## Environment variables
@@ -1154,20 +1154,23 @@ defineTool({
1154
1154
  "",
1155
1155
  "Inputs:",
1156
1156
  ' - type: "A" | "AAAA" | "CNAME" | "MX" | "TXT" | "NS" | "SRV" | "CAA" | "ALIAS".',
1157
- ' - name: "@" for the zone apex, or a subdomain label ("www", "api", "_acme-challenge").',
1158
- " - content: the literal record value. For TXT pass the unquoted string \u2014 the PowerDNS layer handles quoting.",
1157
+ ' - name: "@" for the zone apex, or a subdomain label ("www", "api", "_acme-challenge"). Do NOT include the zone suffix \u2014 the backend prepends it. "bounce" on zone "micci.dk" publishes "bounce.micci.dk"; passing "bounce.micci.dk" works (the backend strips the suffix) but is non-idiomatic.',
1158
+ " - content: the literal record value. For TXT pass the unquoted string \u2014 the PowerDNS layer handles quoting. For CNAME / MX / NS / SRV / ALIAS the target may be passed with or without a trailing dot; the adapter FQDN-izes it.",
1159
1159
  " - ttl (optional): 60\u201386400 seconds, default 3600.",
1160
1160
  " - priority (optional, required for MX and SRV): 0\u201365535.",
1161
1161
  "",
1162
1162
  "Returns: { record: Record } \u2014 see get_dns_record for the shape.",
1163
1163
  "",
1164
+ "Notes:",
1165
+ ' - CNAME at the apex is rejected (RFC 1034 forbids CNAME coexisting with NS/SOA). Use type="ALIAS" for apex aliasing.',
1166
+ "",
1164
1167
  'Example: create_dns_record({ domain: "micci.dk", type: "TXT", name: "@", content: "google-site-verification=C0V0scK48g2\u2026", ttl: 300 })'
1165
1168
  ].join("\n"),
1166
1169
  input: {
1167
1170
  zone_id: z7.string().optional().describe("Zone publicId."),
1168
1171
  domain: z7.string().optional().describe("Apex or subdomain to resolve a hosted zone."),
1169
1172
  type: z7.enum(DNS_RECORD_TYPES).describe("DNS record type."),
1170
- name: z7.string().min(1).max(253).describe('"@" for apex, or label (no trailing dot).'),
1173
+ name: z7.string().min(1).max(253).describe('"@" for apex, or a bare label. Do NOT include the zone suffix.'),
1171
1174
  content: z7.string().min(1).max(2e3).describe("Record value; TXT values are unquoted."),
1172
1175
  ttl: z7.number().int().min(60).max(86400).optional().describe("TTL in seconds (default 3600)."),
1173
1176
  priority: z7.number().int().min(0).max(65535).optional().describe("Required for MX / SRV.")
@@ -1221,7 +1224,7 @@ defineTool({
1221
1224
  input: {
1222
1225
  record_id: z7.string().describe("Record publicId."),
1223
1226
  type: z7.enum(DNS_RECORD_TYPES).describe("DNS record type."),
1224
- name: z7.string().min(1).max(253).describe('"@" for apex, or label.'),
1227
+ name: z7.string().min(1).max(253).describe('"@" for apex, or a bare label. Do NOT include the zone suffix.'),
1225
1228
  content: z7.string().min(1).max(2e3).describe("Record value; TXT unquoted."),
1226
1229
  ttl: z7.number().int().min(60).max(86400).optional().describe("TTL in seconds (default 3600)."),
1227
1230
  priority: z7.number().int().min(0).max(65535).optional().describe("Required for MX / SRV.")
@@ -1937,6 +1940,7 @@ defineTool({
1937
1940
 
1938
1941
  // src/tools/projects.ts
1939
1942
  import { z as z12 } from "zod";
1943
+ var REGION_IDS = ["eu-central-1", "eu-central-2", "eu-west-1", "us-east-1"];
1940
1944
  defineTool({
1941
1945
  name: "list_projects",
1942
1946
  category: "projects",
@@ -1969,16 +1973,16 @@ defineTool({
1969
1973
  "Inputs:",
1970
1974
  " - name: human-readable project name (1\u201360 chars).",
1971
1975
  " - description (optional): short blurb shown in the dashboard.",
1972
- ' - region (optional): "fsn1" (Falkenstein) | "nbg1" (Nuremberg) | "hel1" (Helsinki). Default depends on team plan.',
1976
+ ' - region (optional): "eu-central-1" (Falkenstein) | "eu-central-2" (Nuremberg) | "eu-west-1" (Helsinki) | "us-east-1" (Ashburn). Defaults to eu-central-2.',
1973
1977
  "",
1974
1978
  "Returns: { project: Project } \u2014 includes the new id and publicId.",
1975
1979
  "",
1976
- 'Example: create_project({ name: "billing-api", description: "Stripe webhooks", region: "fsn1" }) \u2192 { project: { id: 12, publicId: "prj_\u2026", \u2026 } }'
1980
+ 'Example: create_project({ name: "billing-api", description: "Stripe webhooks", region: "eu-central-1" }) \u2192 { project: { id: 12, publicId: "prj_\u2026", \u2026 } }'
1977
1981
  ].join("\n"),
1978
1982
  input: {
1979
1983
  name: z12.string().min(1).max(60).describe("Project name (1\u201360 chars)."),
1980
1984
  description: z12.string().max(500).optional().describe("Short description (\u2264500 chars)."),
1981
- region: z12.enum(["fsn1", "nbg1", "hel1"]).optional().describe("Hetzner region: fsn1 | nbg1 | hel1.")
1985
+ region: z12.enum(REGION_IDS).optional().describe("Region: eu-central-1 | eu-central-2 | eu-west-1 | us-east-1.")
1982
1986
  },
1983
1987
  handler: async (args2, ctx) => {
1984
1988
  const teamId = await ctx.resolveTeamId();
@@ -2058,6 +2062,24 @@ defineTool({
2058
2062
 
2059
2063
  // src/tools/services.ts
2060
2064
  import { z as z13 } from "zod";
2065
+ var DEV_ENV_IMAGE = "hoststack/dev-env:claude";
2066
+ var DEV_ENV_VOLUME = { name: "workspace", mountPath: "/workspace", sizeGb: 10 };
2067
+ var SERVICE_TYPES = [
2068
+ "web_service",
2069
+ "private_service",
2070
+ "worker",
2071
+ "cron_job",
2072
+ "static_site"
2073
+ ];
2074
+ var SERVICE_PLANS = [
2075
+ "pico",
2076
+ "nano",
2077
+ "micro",
2078
+ "starter",
2079
+ "standard",
2080
+ "pro_standard",
2081
+ "pro_large"
2082
+ ];
2061
2083
  defineTool({
2062
2084
  name: "list_services",
2063
2085
  category: "services",
@@ -2109,10 +2131,171 @@ defineTool({
2109
2131
  args2.status ? `status=${args2.status}` : null,
2110
2132
  args2.type ? `type=${args2.type}` : null
2111
2133
  ].filter(Boolean).join(", ");
2112
- const summary = data.items.length === 0 ? filterDesc ? `No services match the given filters (${filterDesc}).` : "No services yet \u2014 create one with create_service or via the dashboard." : `Found ${data.items.length} service${data.items.length === 1 ? "" : "s"}${filterDesc ? ` matching ${filterDesc}` : ""}.`;
2134
+ 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}` : ""}.`;
2113
2135
  return respond({ summary, data });
2114
2136
  }
2115
2137
  });
2138
+ defineTool({
2139
+ name: "create_service",
2140
+ category: "services",
2141
+ description: [
2142
+ "Create a new service in a project. Source is either a connected git repo (github_repo_id) OR a pre-built docker_image \u2014 never both.",
2143
+ "",
2144
+ "When to use: the user wants to deploy something new. For a one-command AI dev environment specifically, prefer create_dev_environment (it also attaches the /workspace volume and sets the MCP keys).",
2145
+ "",
2146
+ "Inputs:",
2147
+ ' - project_id: numeric id or publicId ("prj_\u2026") of the target project.',
2148
+ " - name: service name (1\u2013100 chars).",
2149
+ ' - type: "web_service" | "private_service" | "worker" | "cron_job" | "static_site".',
2150
+ " - docker_image (optional): pre-built image ref to deploy instead of building from source.",
2151
+ " - github_repo_id (optional): connect a previously-linked GitHub repo by numeric id.",
2152
+ ' - branch (optional): git branch (default "main").',
2153
+ " - install_command / build_command / start_command (optional): build/run shell commands. start_command is required for web/private services without a docker_image.",
2154
+ " - cron_schedule (optional): cron expression \u2014 required for cron_job.",
2155
+ ' - publish_path (optional): static-site output dir (e.g. "dist").',
2156
+ ' - runtime (optional): "node" | "bun" | "python" | \u2026 (auto-detected from a repo when omitted).',
2157
+ ' - plan (optional): service size (default "micro").',
2158
+ " - environment_id (optional): bind to a specific environment; defaults to the project Production env.",
2159
+ " - auto_deploy (optional, default true): trigger the first deploy immediately when a source is present.",
2160
+ "",
2161
+ "Returns: { service: Service, deployId: number | null }.",
2162
+ "",
2163
+ 'Example: create_service({ project_id: "prj_abc", name: "api", type: "web_service", github_repo_id: 42 }) \u2192 { service: { publicId: "svc_\u2026" }, deployId: 1234 }'
2164
+ ].join("\n"),
2165
+ input: {
2166
+ project_id: z13.union([z13.number().int().positive(), z13.string()]).describe("Target project \u2014 numeric id or publicId."),
2167
+ name: z13.string().min(1).max(100).describe("Service name (1\u2013100 chars)."),
2168
+ type: z13.enum(SERVICE_TYPES).describe("Service type."),
2169
+ docker_image: z13.string().max(500).optional().describe("Pre-built image ref. Mutually exclusive with github_repo_id."),
2170
+ github_repo_id: z13.number().int().positive().optional().describe("Linked GitHub repo numeric id. Mutually exclusive with docker_image."),
2171
+ branch: z13.string().max(200).optional().describe('Git branch (default "main").'),
2172
+ install_command: z13.string().max(1e3).optional().describe("Install shell command."),
2173
+ build_command: z13.string().max(1e3).optional().describe("Build shell command."),
2174
+ start_command: z13.string().max(1e3).optional().describe("Start shell command (required for web/private services without an image)."),
2175
+ cron_schedule: z13.string().max(100).optional().describe("Cron expression \u2014 required for cron_job."),
2176
+ publish_path: z13.string().max(500).optional().describe("Static-site output dir."),
2177
+ runtime: z13.string().max(50).optional().describe("Runtime hint (node/bun/python/\u2026)."),
2178
+ plan: z13.enum(SERVICE_PLANS).optional().describe('Service size (default "micro").'),
2179
+ environment_id: z13.union([z13.number().int().positive(), z13.string()]).optional().describe("Bind to a specific environment; defaults to Production."),
2180
+ auto_deploy: z13.boolean().optional().describe("Trigger the first deploy immediately (default true).")
2181
+ },
2182
+ handler: async (args2, ctx) => {
2183
+ const teamId = await ctx.resolveTeamId();
2184
+ const projectId = await ctx.hoststack.resolveId(args2.project_id, {
2185
+ kind: "project",
2186
+ teamId
2187
+ });
2188
+ const input = {
2189
+ name: args2.name,
2190
+ type: args2.type,
2191
+ projectId
2192
+ };
2193
+ if (args2.docker_image !== void 0) input.dockerImage = args2.docker_image;
2194
+ if (args2.github_repo_id !== void 0) input.githubRepoId = args2.github_repo_id;
2195
+ if (args2.branch !== void 0) input.branch = args2.branch;
2196
+ if (args2.install_command !== void 0) input.installCommand = args2.install_command;
2197
+ if (args2.build_command !== void 0) input.buildCommand = args2.build_command;
2198
+ if (args2.start_command !== void 0) input.startCommand = args2.start_command;
2199
+ if (args2.cron_schedule !== void 0) input.cronSchedule = args2.cron_schedule;
2200
+ if (args2.publish_path !== void 0) input.publishPath = args2.publish_path;
2201
+ if (args2.runtime !== void 0) input.runtime = args2.runtime;
2202
+ if (args2.plan !== void 0) input.plan = args2.plan;
2203
+ if (args2.auto_deploy !== void 0) input.autoDeploy = args2.auto_deploy;
2204
+ if (args2.environment_id !== void 0) {
2205
+ input.environmentId = await ctx.hoststack.resolveId(args2.environment_id, {
2206
+ kind: "environment",
2207
+ teamId
2208
+ });
2209
+ }
2210
+ const response = await ctx.hoststack.services.create(teamId, input);
2211
+ const data = {
2212
+ service: shapeService(response.service),
2213
+ deployId: response.deployId ?? null
2214
+ };
2215
+ const publicId = data.service && "publicId" in data.service ? data.service.publicId : "unknown";
2216
+ return respond({ summary: `Created service "${args2.name}" (${publicId}).`, data });
2217
+ }
2218
+ });
2219
+ defineTool({
2220
+ name: "create_dev_environment",
2221
+ category: "services",
2222
+ description: [
2223
+ "Spin up an AI dev environment in one call: a private service from the agentic dev-env image (Claude Code + Codex + OpenCode + MCPs preinstalled), with a persistent /workspace volume and the MCP API keys set, then fire the first deploy.",
2224
+ "",
2225
+ `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.`,
2226
+ "",
2227
+ "Inputs:",
2228
+ ' - project_id: numeric id or publicId ("prj_\u2026") of the target project.',
2229
+ ' - name (optional): service name (default "dev-environment").',
2230
+ ' - plan (optional): service size (default "micro").',
2231
+ " - disk_gb (optional): /workspace volume size in GB (default 10, 1\u2013100).",
2232
+ " - hoststack_api_key (optional): sets HOSTSTACK_API_KEY so the hoststack MCP works inside the container.",
2233
+ " - poststack_api_key (optional): sets POSTSTACK_API_KEY so the poststack MCP works inside the container.",
2234
+ "",
2235
+ "Returns: { service: Service, volumeAttached: boolean, deployId: number | null }. Open the Terminal tab on the service (dashboard or phone) once it is running; log in once with `claude /login` inside the container.",
2236
+ "",
2237
+ 'Example: create_dev_environment({ project_id: "prj_abc", name: "scratch", hoststack_api_key: "hs_live_\u2026" })'
2238
+ ].join("\n"),
2239
+ input: {
2240
+ project_id: z13.union([z13.number().int().positive(), z13.string()]).describe("Target project \u2014 numeric id or publicId."),
2241
+ name: z13.string().min(1).max(100).optional().describe('Service name (default "dev-environment").'),
2242
+ plan: z13.enum(SERVICE_PLANS).optional().describe('Service size (default "micro").'),
2243
+ disk_gb: z13.number().int().min(1).max(100).optional().describe("/workspace volume size in GB (default 10)."),
2244
+ hoststack_api_key: z13.string().optional().describe("Value for HOSTSTACK_API_KEY (enables the hoststack MCP in-container)."),
2245
+ poststack_api_key: z13.string().optional().describe("Value for POSTSTACK_API_KEY (enables the poststack MCP in-container).")
2246
+ },
2247
+ handler: async (args2, ctx) => {
2248
+ const teamId = await ctx.resolveTeamId();
2249
+ const projectId = await ctx.hoststack.resolveId(args2.project_id, {
2250
+ kind: "project",
2251
+ teamId
2252
+ });
2253
+ const name = args2.name ?? "dev-environment";
2254
+ const sizeGb = args2.disk_gb ?? DEV_ENV_VOLUME.sizeGb;
2255
+ const createInput = {
2256
+ name,
2257
+ type: "private_service",
2258
+ projectId,
2259
+ dockerImage: DEV_ENV_IMAGE,
2260
+ autoDeploy: false
2261
+ };
2262
+ if (args2.plan !== void 0) createInput.plan = args2.plan;
2263
+ const created = await ctx.hoststack.services.create(teamId, createInput);
2264
+ const service = created.service;
2265
+ const envVars = [];
2266
+ if (args2.hoststack_api_key)
2267
+ envVars.push({
2268
+ key: "HOSTSTACK_API_KEY",
2269
+ value: args2.hoststack_api_key,
2270
+ isSecret: true
2271
+ });
2272
+ if (args2.poststack_api_key)
2273
+ envVars.push({
2274
+ key: "POSTSTACK_API_KEY",
2275
+ value: args2.poststack_api_key,
2276
+ isSecret: true
2277
+ });
2278
+ if (envVars.length > 0) {
2279
+ await ctx.hoststack.envVars.bulkSet(teamId, service.id, { vars: envVars });
2280
+ }
2281
+ let volumeAttached = false;
2282
+ try {
2283
+ await ctx.hoststack.volumes.create(teamId, service.id, {
2284
+ name: DEV_ENV_VOLUME.name,
2285
+ mountPath: DEV_ENV_VOLUME.mountPath,
2286
+ sizeGb
2287
+ });
2288
+ volumeAttached = true;
2289
+ } catch {
2290
+ }
2291
+ const deploy = await ctx.hoststack.deploys.trigger(teamId, service.id);
2292
+ const deployId = deploy.deploy?.id ?? null;
2293
+ return respond({
2294
+ summary: `Created AI dev environment "${name}" (${service.publicId})${volumeAttached ? " with a /workspace volume" : ""} \u2014 deploying. Open its Terminal tab once running.`,
2295
+ data: { service: shapeService(service), volumeAttached, deployId }
2296
+ });
2297
+ }
2298
+ });
2116
2299
  defineTool({
2117
2300
  name: "get_service",
2118
2301
  category: "services",