helipod-mcp 0.5.13 → 0.5.17

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
@@ -2,8 +2,8 @@
2
2
 
3
3
  Use [Helipod](https://helipod.io) from AI coding tools: deploy apps and databases, run VPS servers, buy domains and edit DNS, and send email, by asking in plain language.
4
4
 
5
- - **MCP server** (`helipod-mcp`): 238 tools over the Container, Database, VPS, Domain, Email, Account and AI APIs. Works in Claude Code, Codex, Cursor, VS Code, Windsurf, Claude Desktop and any MCP client.
6
- - **Skills** (`skills/`): step-by-step playbooks so the agent deploys, prices and confirms the way Helipod expects (price first, confirm before deleting, read build logs on failure).
5
+ - **MCP server** (`helipod-mcp`): 263 tools over the Container, Database, VPS, Domain, Email, Account and AI APIs. Works in Claude Code, Codex, Cursor, VS Code, Windsurf, Claude Desktop and any MCP client.
6
+ - **Skills** (`skills/`): step-by-step playbooks so the agent deploys, prices and confirms the way Helipod expects (price first, confirm before deleting, read build logs on failure): containers, VPS and apps on a VPS, databases, domains, email, the AI gateway, end-to-end launches (app + database + domain + email) and reselling.
7
7
 
8
8
  Step-by-step guides for each tool (Claude Code, Codex, Cursor, VS Code, Gemini CLI, opencode, Windsurf, Claude Desktop, Cline and others): **https://docs.helipod.io/docs/helipod/ai-agents**
9
9
 
package/dist/http.js CHANGED
@@ -35623,6 +35623,14 @@ class HelipodClient {
35623
35623
  const headers = !this.cfg.emailApiKey && opts.project ? { "x-helipod-project": opts.project } : undefined;
35624
35624
  return this.request(this.emailApiUrl, key, method, path, { ...opts, headers });
35625
35625
  }
35626
+ emailWithAccountKey(method, path, opts = {}) {
35627
+ const key = this.cfg.apiKey;
35628
+ if (!key) {
35629
+ throw new HelipodError(401, "NO_API_KEY", "Managing email keys needs HELIPOD_API_KEY (a Helipod API key with the Email API enabled); an email key cannot do it.");
35630
+ }
35631
+ const headers = opts.project ? { "x-helipod-project": opts.project } : undefined;
35632
+ return this.request(this.emailApiUrl, key, method, path, { ...opts, headers });
35633
+ }
35626
35634
  async image(link, maxBytes) {
35627
35635
  let url2;
35628
35636
  try {
@@ -35753,7 +35761,7 @@ var containerTools = [
35753
35761
  kind: "read",
35754
35762
  description: "Projects this API key can reach, each with its environments (production, staging, ...). Start here to find an environmentId.",
35755
35763
  input: {},
35756
- run: (_a3, api2) => api2.api("GET", `${C}/projects`)
35764
+ run: (_a3, api2) => api2.api("GET", `/projects`)
35757
35765
  }),
35758
35766
  defineTool({
35759
35767
  name: "get_project",
@@ -35761,7 +35769,7 @@ var containerTools = [
35761
35769
  kind: "read",
35762
35770
  description: "One project with its environments (ids and names). Use it to find an environmentId when you already know the project.",
35763
35771
  input: { projectId: id("Project") },
35764
- run: (a, api2) => api2.api("GET", `${C}/projects/${a.projectId}`)
35772
+ run: (a, api2) => api2.api("GET", `/projects/${a.projectId}`)
35765
35773
  }),
35766
35774
  defineTool({
35767
35775
  name: "create_project",
@@ -35769,7 +35777,7 @@ var containerTools = [
35769
35777
  kind: "write",
35770
35778
  description: "Create a project (free) with its default production environment. Needs a whole-account key (a key limited to one project gets 403). Pass an idempotencyKey when retrying.",
35771
35779
  input: { name: exports_external.string().min(3).max(100), description: exports_external.string().max(500).optional(), idempotencyKey: idem },
35772
- run: (a, api2) => api2.api("POST", `${C}/projects`, { body: { name: a.name, description: a.description }, idempotencyKey: a.idempotencyKey })
35780
+ run: (a, api2) => api2.api("POST", `/projects`, { body: { name: a.name, description: a.description }, idempotencyKey: a.idempotencyKey })
35773
35781
  }),
35774
35782
  defineTool({
35775
35783
  name: "update_project",
@@ -35778,7 +35786,7 @@ var containerTools = [
35778
35786
  idempotent: true,
35779
35787
  description: "Change a project's name and/or description.",
35780
35788
  input: { projectId: id("Project"), name: exports_external.string().min(3).max(100).optional(), description: exports_external.string().max(500).optional() },
35781
- run: (a, api2) => api2.api("PATCH", `${C}/projects/${a.projectId}`, { body: { name: a.name, description: a.description } })
35789
+ run: (a, api2) => api2.api("PATCH", `/projects/${a.projectId}`, { body: { name: a.name, description: a.description } })
35782
35790
  }),
35783
35791
  defineTool({
35784
35792
  name: "delete_project",
@@ -35786,7 +35794,7 @@ var containerTools = [
35786
35794
  kind: "destructive",
35787
35795
  description: "Delete an EMPTY project and its environments and shared variables. Irreversible. Refused (409 PROJECT_NOT_EMPTY) while it still has services, databases, VPS or other resources: those must be deleted first. Needs a whole-account key. Only after the user confirms; confirm must be the project's exact name.",
35788
35796
  input: { projectId: id("Project"), confirm: exports_external.string().max(255).describe("The project name, exactly") },
35789
- run: (a, api2) => api2.api("DELETE", `${C}/projects/${a.projectId}`, { body: { confirm: a.confirm } })
35797
+ run: (a, api2) => api2.api("DELETE", `/projects/${a.projectId}`, { body: { confirm: a.confirm } })
35790
35798
  }),
35791
35799
  defineTool({
35792
35800
  name: "create_environment",
@@ -35794,7 +35802,7 @@ var containerTools = [
35794
35802
  kind: "write",
35795
35803
  description: "Add an environment (e.g. staging) to a project. Free; services in it are billed as usual once deployed.",
35796
35804
  input: { projectId: id("Project"), name: exports_external.string().regex(/^[A-Za-z0-9][A-Za-z0-9 _.-]{0,49}$/).describe("1-50 characters: letters, digits, space _ . -") },
35797
- run: (a, api2) => api2.api("POST", `${C}/projects/${a.projectId}/environments`, { body: { name: a.name } })
35805
+ run: (a, api2) => api2.api("POST", `/projects/${a.projectId}/environments`, { body: { name: a.name } })
35798
35806
  }),
35799
35807
  defineTool({
35800
35808
  name: "rename_environment",
@@ -35803,7 +35811,7 @@ var containerTools = [
35803
35811
  idempotent: true,
35804
35812
  description: "Rename an environment (names are unique within the project).",
35805
35813
  input: { environmentId: id("Environment"), name: exports_external.string().regex(/^[A-Za-z0-9][A-Za-z0-9 _.-]{0,49}$/) },
35806
- run: (a, api2) => api2.api("PATCH", `${C}/environments/${a.environmentId}`, { body: { name: a.name } })
35814
+ run: (a, api2) => api2.api("PATCH", `/environments/${a.environmentId}`, { body: { name: a.name } })
35807
35815
  }),
35808
35816
  defineTool({
35809
35817
  name: "delete_environment",
@@ -35811,7 +35819,7 @@ var containerTools = [
35811
35819
  kind: "destructive",
35812
35820
  description: "Delete an EMPTY environment (409 ENVIRONMENT_NOT_EMPTY while it has services: delete them first). Irreversible. Only after the user confirms; confirm must be the environment's exact name.",
35813
35821
  input: { environmentId: id("Environment"), confirm: exports_external.string().max(255).describe("The environment name, exactly") },
35814
- run: (a, api2) => api2.api("DELETE", `${C}/environments/${a.environmentId}`, { body: { confirm: a.confirm } })
35822
+ run: (a, api2) => api2.api("DELETE", `/environments/${a.environmentId}`, { body: { confirm: a.confirm } })
35815
35823
  }),
35816
35824
  defineTool({
35817
35825
  name: "list_project_variables",
@@ -35846,7 +35854,7 @@ var containerTools = [
35846
35854
  name: "list_services",
35847
35855
  title: "List services",
35848
35856
  kind: "read",
35849
- description: "Services (containers) in an environment: status, source, resources, domains, latest deployment and daily cost.",
35857
+ description: "Services (containers) in an environment: status, source, resources, domains, latest deployment and daily cost. Status awaiting_payment = a deploy was refused because the wallet could not pay: top up or buy a bundle (get_bundle), then deploy_service again; nothing is lost.",
35850
35858
  input: { environmentId: id("Environment") },
35851
35859
  run: (a, api2) => api2.api("GET", `${C}/environments/${a.environmentId}/services`)
35852
35860
  }),
@@ -35943,7 +35951,7 @@ var containerTools = [
35943
35951
  title: "Update / rename service",
35944
35952
  kind: "write",
35945
35953
  idempotent: true,
35946
- description: "Rename a service or change its start command, root directory, health check or startup dependencies. Settings apply on the next deploy (needsRedeploy). Renaming breaks references like ${{oldName.KEY}} in other services' variables: the response lists them in warnings.",
35954
+ description: "Rename a service or change its start command, root directory, health check or startup dependencies. Settings apply on the next deploy (needsRedeploy). Renaming rewrites references like ${{oldName.KEY}} in the environment's service variables to the new name (applied on their next deploy); project shared variables are not rewritten and are listed in warnings.",
35947
35955
  input: {
35948
35956
  serviceId: id("Service"),
35949
35957
  name: exports_external.string().regex(/^[A-Za-z][A-Za-z0-9_-]{0,39}$/).optional().describe("New service name (letters, digits, _ -; starts with a letter; unique in the environment)"),
@@ -36786,6 +36794,19 @@ var record3 = exports_external.object({
36786
36794
  content: exports_external.string().describe('TXT without surrounding quotes (they are added by the DNS server); MX as "10 mail.example.com"'),
36787
36795
  ttl: exports_external.number().int().optional()
36788
36796
  });
36797
+ var owner = exports_external.object({
36798
+ name: exports_external.string().min(3).max(255),
36799
+ email: exports_external.string().max(255).optional().describe("Gets the registry's verification email; defaults to the account email"),
36800
+ organization: exports_external.string().max(255).optional(),
36801
+ phone: exports_external.string().min(9).max(20).describe("e.g. +6281234567890"),
36802
+ addressLine1: exports_external.string().min(3).max(500),
36803
+ addressLine2: exports_external.string().max(500).optional(),
36804
+ city: exports_external.string().min(3).max(100),
36805
+ state: exports_external.string().min(1).max(100),
36806
+ countryCode: exports_external.string().regex(/^[A-Za-z]{2}$/).describe("ISO 3166-1 alpha-2, e.g. ID"),
36807
+ postalCode: exports_external.string().min(1).max(20),
36808
+ fax: exports_external.string().max(50).optional()
36809
+ });
36789
36810
  var domainTools = [
36790
36811
  defineTool({
36791
36812
  name: "search_domain",
@@ -36865,19 +36886,7 @@ var domainTools = [
36865
36886
  name: exports_external.string().describe("Full domain, e.g. tokosaya.id"),
36866
36887
  nameservers: exports_external.array(exports_external.string()).min(2).max(5).optional(),
36867
36888
  whoisProtection: exports_external.boolean().optional(),
36868
- registrant: exports_external.object({
36869
- name: exports_external.string().min(3).max(255),
36870
- email: exports_external.string().max(255).optional().describe("Gets the registry's verification email; defaults to the account email"),
36871
- organization: exports_external.string().max(255).optional(),
36872
- phone: exports_external.string().min(9).max(20).describe("e.g. +6281234567890"),
36873
- addressLine1: exports_external.string().min(3).max(500),
36874
- addressLine2: exports_external.string().max(500).optional(),
36875
- city: exports_external.string().min(3).max(100),
36876
- state: exports_external.string().min(1).max(100),
36877
- countryCode: exports_external.string().regex(/^[A-Za-z]{2}$/).describe("ISO 3166-1 alpha-2, e.g. ID"),
36878
- postalCode: exports_external.string().min(1).max(20),
36879
- fax: exports_external.string().max(50).optional()
36880
- }).optional().describe("Owner of THIS domain instead of the account's owner details (e.g. a reseller's customer). .id domains only: others answer 400 REGISTRANT_NOT_SUPPORTED. Use only details the user gave."),
36889
+ registrant: owner.optional().describe("Owner of THIS domain instead of the account's owner details (e.g. a reseller's customer). .id domains only: others answer 400 REGISTRANT_NOT_SUPPORTED. Use only details the user gave."),
36881
36890
  dryRun: dryRun3
36882
36891
  },
36883
36892
  run: (a, api2) => api2.api("POST", "/domains/register", { body: { name: a.name, period: 1, nameservers: a.nameservers, whoisProtection: a.whoisProtection, registrant: a.registrant }, query: { dryRun: a.dryRun ? "true" : undefined } })
@@ -36886,9 +36895,23 @@ var domainTools = [
36886
36895
  name: "transfer_domain",
36887
36896
  title: "Transfer domain in",
36888
36897
  kind: "write",
36889
- description: `Transfer a domain from another registrar (adds 1 year). COSTS MONEY from the wallet. Needs the domain's auth code. ${DRY2}`,
36890
- input: { name: exports_external.string(), authCode: exports_external.string(), nameservers: exports_external.array(exports_external.string()).min(2).max(5).optional(), dryRun: dryRun3 },
36891
- run: (a, api2) => api2.api("POST", "/domains/transfer", { body: { name: a.name, authCode: a.authCode, period: 1, nameservers: a.nameservers }, query: { dryRun: a.dryRun ? "true" : undefined } })
36898
+ description: `Transfer a domain from another registrar (adds 1 year). COSTS MONEY from the wallet. Needs the domain's auth code. ${DRY2} With registrant (.id only) the domain gets that owner when the transfer completes (ownerPending until then); without it, the account's owner details.`,
36899
+ input: {
36900
+ name: exports_external.string(),
36901
+ authCode: exports_external.string(),
36902
+ nameservers: exports_external.array(exports_external.string()).min(2).max(5).optional(),
36903
+ registrant: owner.optional().describe("Owner of THIS domain once the transfer completes (e.g. a reseller's customer). .id domains only: others answer 400 REGISTRANT_NOT_SUPPORTED. Use only details the user gave."),
36904
+ dryRun: dryRun3
36905
+ },
36906
+ run: (a, api2) => api2.api("POST", "/domains/transfer", { body: { name: a.name, authCode: a.authCode, period: 1, nameservers: a.nameservers, registrant: a.registrant }, query: { dryRun: a.dryRun ? "true" : undefined } })
36907
+ }),
36908
+ defineTool({
36909
+ name: "set_domain_owner",
36910
+ title: "Set domain owner",
36911
+ kind: "write",
36912
+ description: "Set a new owner (registrant) for one .id domain of the account, e.g. a reseller's customer or a domain transferred earlier. Free. An active domain gets it now (ownerPending false); a transfer in progress gets it when it completes, and a change the registry does not take yet is retried automatically (ownerPending true, HTTP 202). Other domains keep their other contacts. Use only details the user gave.",
36913
+ input: { domainId, owner },
36914
+ run: (a, api2) => api2.api("PATCH", `/domains/${a.domainId}/registrant`, { body: a.owner })
36892
36915
  }),
36893
36916
  defineTool({
36894
36917
  name: "renew_domain",
@@ -37167,6 +37190,24 @@ var emailTools = [
37167
37190
  idempotencyKey: purchaseKey(a.dryRun, a.idempotencyKey)
37168
37191
  })
37169
37192
  }),
37193
+ defineTool({
37194
+ name: "set_email_auto_renew",
37195
+ title: "Set email auto-renew",
37196
+ kind: "write",
37197
+ idempotent: true,
37198
+ description: "Turn the active email plan's auto-renew on or off (sets the value, safe to repeat). On: the plan renews from the wallet deposit when it ends; off: it ends at its expiry date. get_email_plan and email_status show it as autoRenew. A free plan renews at Rp 0.",
37199
+ input: { projectId, autoRenew: exports_external.boolean() },
37200
+ run: (a, api2) => api2.email("POST", "/plan/auto-renew", { project: a.projectId, body: { enabled: a.autoRenew } })
37201
+ }),
37202
+ defineTool({
37203
+ name: "set_email_payg",
37204
+ title: "Set email pay-as-you-go",
37205
+ kind: "write",
37206
+ idempotent: true,
37207
+ description: "Turn pay-as-you-go on or off for the active paid email plan (sets the value, safe to repeat; off by default). Off: once the monthly quota is used, sends stop with 402 PAYG_DISABLED and nothing is charged. On: sends continue past the quota and blocks of 100 emails are bought from the wallet deposit at the plan's per-email price (unused ones refunded if the plan is not renewed). email_status shows it as paygEnabled. No effect on the free plan.",
37208
+ input: { projectId, paygEnabled: exports_external.boolean() },
37209
+ run: (a, api2) => api2.email("POST", "/plan/payg", { project: a.projectId, body: { enabled: a.paygEnabled } })
37210
+ }),
37170
37211
  defineTool({
37171
37212
  name: "send_email",
37172
37213
  title: "Send email",
@@ -37240,6 +37281,30 @@ var emailTools = [
37240
37281
  input: { projectId, domainId: exports_external.string() },
37241
37282
  run: (a, api2) => api2.email("POST", `/domains/${a.domainId}/verify`, { project: a.projectId })
37242
37283
  }),
37284
+ defineTool({
37285
+ name: "list_email_keys",
37286
+ title: "List email keys",
37287
+ kind: "read",
37288
+ description: "Email API keys of a project (name, prefix, last used, expiry; never the key itself). Needs a Helipod API key with the Email API: an email key or a sign-in token answers 403 KEY_NOT_ALLOWED.",
37289
+ input: { projectId },
37290
+ run: (a, api2) => api2.emailWithAccountKey("GET", "/keys", { project: a.projectId })
37291
+ }),
37292
+ defineTool({
37293
+ name: "create_email_key",
37294
+ title: "Create email key",
37295
+ kind: "write",
37296
+ description: "Make an email API key for a project, e.g. one per customer of a reseller. The key works on the Email API and as the SMTP password (smtp.helipod.io, username apikey) for that project only, and cannot make keys itself. The full key is in the answer ONCE: give it to the user to store; it cannot be shown again. Needs a Helipod API key with the Email API (an email key or a sign-in token answers 403 KEY_NOT_ALLOWED). Per project: 5 active keys on Free/Starter, 10 on Pro, 50 on Business.",
37297
+ input: { projectId, name: exports_external.string().min(1).max(100).describe("To tell keys apart, e.g. the customer's name"), expiresInDays: exports_external.number().int().min(1).max(3650).optional() },
37298
+ run: (a, api2) => api2.emailWithAccountKey("POST", "/keys", { project: a.projectId, body: { name: a.name, expiresInDays: a.expiresInDays } })
37299
+ }),
37300
+ defineTool({
37301
+ name: "revoke_email_key",
37302
+ title: "Revoke email key",
37303
+ kind: "destructive",
37304
+ description: "Revoke an email API key of the project: it stops working at once, also for open SMTP sessions. Apps using it can no longer send. Needs a Helipod API key with the Email API.",
37305
+ input: { projectId, keyId: uid("Email key") },
37306
+ run: (a, api2) => api2.emailWithAccountKey("DELETE", `/keys/${a.keyId}`, { project: a.projectId })
37307
+ }),
37243
37308
  defineTool({
37244
37309
  name: "list_email_templates",
37245
37310
  title: "List email templates",
@@ -38493,7 +38558,7 @@ var generalTools = [
38493
38558
  ];
38494
38559
 
38495
38560
  // src/server.ts
38496
- var VERSION = "0.5.13";
38561
+ var VERSION = "0.5.17";
38497
38562
  var INSTRUCTIONS = `Helipod is a cloud platform (Indonesia): containers, managed Postgres databases, VPS, domains, email and an AI gateway, billed from a prepaid wallet in Rupiah.
38498
38563
  - Start with list_projects to find projects and environment ids. Account tools (get_wallet, invoices, statements, notifications, get_project_activity, support tickets) cover the account; AI tools (list_ai_models, get_ai_overview, get_ai_usage, AI keys, top-ups, generate_ai_image) cover the AI gateway.
38499
38564
  - Anything that costs money (create/deploy/scale services, resize volumes, upgrade a database plan, create/renew/upgrade VPS, VPS backups, register/renew/transfer domains, email plans and inbox add-ons, AI top-ups and auto top-up, generating images): call it with dryRun=true first (or the price lookup when it has no dry run), tell the user the cost in IDR and the wallet balance after, and wait for an explicit yes before the real call. Never treat silence or an earlier general OK as a yes.
@@ -38502,6 +38567,8 @@ var INSTRUCTIONS = `Helipod is a cloud platform (Indonesia): containers, managed
38502
38567
  - Deploying an app, step by step: list_projects (pick the project and its production environmentId) -> get_bundle (is it covered?) -> if it needs Postgres: create_database, then get_database_connection and store DATABASE_URL with set_variables (secrets) after the service exists, or pass it in create_service variables -> create_service with dryRun=true (repo or image, port, variables) -> tell the user the cost (included in the bundle, or walletDailyCost per day) and wait for their OK -> create_service for real -> poll get_deployment until it settles, read get_deployment_logs if it failed -> give the user the *.helipod.app URL from get_service. Ask the user for secrets (API keys) only when the app needs them; never invent values.
38503
38568
  - Destructive tools (delete, terminate, reinstall, rollback, restore a backup or an import, copy a backup onto another VPS, replace DNS, revoke a key, remove an SSH key or port forward, send a broadcast, execute_database_sql): confirm with the user and name what will be lost. Before execute_database_sql, run classify_database_sql.
38504
38569
  - Email: when a project has no email yet (email_status shows activated false) or its plan ended, start it with buy_email_plan (dryRun first; the FREE plan costs nothing, still confirm). That also sets email up for the project; then add and verify a sending domain.
38570
+ - Email keys: create_email_key gives an app or a customer its own key (also its SMTP password: smtp.helipod.io, username apikey); the full key is shown once, so hand it over and never print it again. Domain on Helipod: add_domain_email_records puts the sending domain's DNS records in place in one call.
38571
+ - Domains for someone else (resellers): register_domain and transfer_domain take registrant (.id only; a transfer gets the owner when it completes), set_domain_owner changes it later. Use only details the user gave.
38505
38572
  - Before replacing all DNS records (set_dns_records), read list_dns_records and keep every record the user still needs. Before sending or scheduling a broadcast, tell the user how many contacts it reaches. Before deleting or replacing code, files or variables, read the current value. After renaming a service, fix the variable references listed in warnings.
38506
38573
  - Databases: get_database_connection returns a password; put it in a secret variable (DATABASE_URL) instead of code or chat.
38507
38574
  - Migrate (free): create_vps_import returns a one-time command for the USER to run as root on their old server (never run it yourself); before the first restore_vps_import, show fetchedIp and ask the user to confirm it is their server.
package/dist/index.js CHANGED
@@ -34768,6 +34768,14 @@ class HelipodClient {
34768
34768
  const headers = !this.cfg.emailApiKey && opts.project ? { "x-helipod-project": opts.project } : undefined;
34769
34769
  return this.request(this.emailApiUrl, key, method, path, { ...opts, headers });
34770
34770
  }
34771
+ emailWithAccountKey(method, path, opts = {}) {
34772
+ const key = this.cfg.apiKey;
34773
+ if (!key) {
34774
+ throw new HelipodError(401, "NO_API_KEY", "Managing email keys needs HELIPOD_API_KEY (a Helipod API key with the Email API enabled); an email key cannot do it.");
34775
+ }
34776
+ const headers = opts.project ? { "x-helipod-project": opts.project } : undefined;
34777
+ return this.request(this.emailApiUrl, key, method, path, { ...opts, headers });
34778
+ }
34771
34779
  async image(link, maxBytes) {
34772
34780
  let url2;
34773
34781
  try {
@@ -34898,7 +34906,7 @@ var containerTools = [
34898
34906
  kind: "read",
34899
34907
  description: "Projects this API key can reach, each with its environments (production, staging, ...). Start here to find an environmentId.",
34900
34908
  input: {},
34901
- run: (_a3, api2) => api2.api("GET", `${C}/projects`)
34909
+ run: (_a3, api2) => api2.api("GET", `/projects`)
34902
34910
  }),
34903
34911
  defineTool({
34904
34912
  name: "get_project",
@@ -34906,7 +34914,7 @@ var containerTools = [
34906
34914
  kind: "read",
34907
34915
  description: "One project with its environments (ids and names). Use it to find an environmentId when you already know the project.",
34908
34916
  input: { projectId: id("Project") },
34909
- run: (a, api2) => api2.api("GET", `${C}/projects/${a.projectId}`)
34917
+ run: (a, api2) => api2.api("GET", `/projects/${a.projectId}`)
34910
34918
  }),
34911
34919
  defineTool({
34912
34920
  name: "create_project",
@@ -34914,7 +34922,7 @@ var containerTools = [
34914
34922
  kind: "write",
34915
34923
  description: "Create a project (free) with its default production environment. Needs a whole-account key (a key limited to one project gets 403). Pass an idempotencyKey when retrying.",
34916
34924
  input: { name: exports_external.string().min(3).max(100), description: exports_external.string().max(500).optional(), idempotencyKey: idem },
34917
- run: (a, api2) => api2.api("POST", `${C}/projects`, { body: { name: a.name, description: a.description }, idempotencyKey: a.idempotencyKey })
34925
+ run: (a, api2) => api2.api("POST", `/projects`, { body: { name: a.name, description: a.description }, idempotencyKey: a.idempotencyKey })
34918
34926
  }),
34919
34927
  defineTool({
34920
34928
  name: "update_project",
@@ -34923,7 +34931,7 @@ var containerTools = [
34923
34931
  idempotent: true,
34924
34932
  description: "Change a project's name and/or description.",
34925
34933
  input: { projectId: id("Project"), name: exports_external.string().min(3).max(100).optional(), description: exports_external.string().max(500).optional() },
34926
- run: (a, api2) => api2.api("PATCH", `${C}/projects/${a.projectId}`, { body: { name: a.name, description: a.description } })
34934
+ run: (a, api2) => api2.api("PATCH", `/projects/${a.projectId}`, { body: { name: a.name, description: a.description } })
34927
34935
  }),
34928
34936
  defineTool({
34929
34937
  name: "delete_project",
@@ -34931,7 +34939,7 @@ var containerTools = [
34931
34939
  kind: "destructive",
34932
34940
  description: "Delete an EMPTY project and its environments and shared variables. Irreversible. Refused (409 PROJECT_NOT_EMPTY) while it still has services, databases, VPS or other resources: those must be deleted first. Needs a whole-account key. Only after the user confirms; confirm must be the project's exact name.",
34933
34941
  input: { projectId: id("Project"), confirm: exports_external.string().max(255).describe("The project name, exactly") },
34934
- run: (a, api2) => api2.api("DELETE", `${C}/projects/${a.projectId}`, { body: { confirm: a.confirm } })
34942
+ run: (a, api2) => api2.api("DELETE", `/projects/${a.projectId}`, { body: { confirm: a.confirm } })
34935
34943
  }),
34936
34944
  defineTool({
34937
34945
  name: "create_environment",
@@ -34939,7 +34947,7 @@ var containerTools = [
34939
34947
  kind: "write",
34940
34948
  description: "Add an environment (e.g. staging) to a project. Free; services in it are billed as usual once deployed.",
34941
34949
  input: { projectId: id("Project"), name: exports_external.string().regex(/^[A-Za-z0-9][A-Za-z0-9 _.-]{0,49}$/).describe("1-50 characters: letters, digits, space _ . -") },
34942
- run: (a, api2) => api2.api("POST", `${C}/projects/${a.projectId}/environments`, { body: { name: a.name } })
34950
+ run: (a, api2) => api2.api("POST", `/projects/${a.projectId}/environments`, { body: { name: a.name } })
34943
34951
  }),
34944
34952
  defineTool({
34945
34953
  name: "rename_environment",
@@ -34948,7 +34956,7 @@ var containerTools = [
34948
34956
  idempotent: true,
34949
34957
  description: "Rename an environment (names are unique within the project).",
34950
34958
  input: { environmentId: id("Environment"), name: exports_external.string().regex(/^[A-Za-z0-9][A-Za-z0-9 _.-]{0,49}$/) },
34951
- run: (a, api2) => api2.api("PATCH", `${C}/environments/${a.environmentId}`, { body: { name: a.name } })
34959
+ run: (a, api2) => api2.api("PATCH", `/environments/${a.environmentId}`, { body: { name: a.name } })
34952
34960
  }),
34953
34961
  defineTool({
34954
34962
  name: "delete_environment",
@@ -34956,7 +34964,7 @@ var containerTools = [
34956
34964
  kind: "destructive",
34957
34965
  description: "Delete an EMPTY environment (409 ENVIRONMENT_NOT_EMPTY while it has services: delete them first). Irreversible. Only after the user confirms; confirm must be the environment's exact name.",
34958
34966
  input: { environmentId: id("Environment"), confirm: exports_external.string().max(255).describe("The environment name, exactly") },
34959
- run: (a, api2) => api2.api("DELETE", `${C}/environments/${a.environmentId}`, { body: { confirm: a.confirm } })
34967
+ run: (a, api2) => api2.api("DELETE", `/environments/${a.environmentId}`, { body: { confirm: a.confirm } })
34960
34968
  }),
34961
34969
  defineTool({
34962
34970
  name: "list_project_variables",
@@ -34991,7 +34999,7 @@ var containerTools = [
34991
34999
  name: "list_services",
34992
35000
  title: "List services",
34993
35001
  kind: "read",
34994
- description: "Services (containers) in an environment: status, source, resources, domains, latest deployment and daily cost.",
35002
+ description: "Services (containers) in an environment: status, source, resources, domains, latest deployment and daily cost. Status awaiting_payment = a deploy was refused because the wallet could not pay: top up or buy a bundle (get_bundle), then deploy_service again; nothing is lost.",
34995
35003
  input: { environmentId: id("Environment") },
34996
35004
  run: (a, api2) => api2.api("GET", `${C}/environments/${a.environmentId}/services`)
34997
35005
  }),
@@ -35088,7 +35096,7 @@ var containerTools = [
35088
35096
  title: "Update / rename service",
35089
35097
  kind: "write",
35090
35098
  idempotent: true,
35091
- description: "Rename a service or change its start command, root directory, health check or startup dependencies. Settings apply on the next deploy (needsRedeploy). Renaming breaks references like ${{oldName.KEY}} in other services' variables: the response lists them in warnings.",
35099
+ description: "Rename a service or change its start command, root directory, health check or startup dependencies. Settings apply on the next deploy (needsRedeploy). Renaming rewrites references like ${{oldName.KEY}} in the environment's service variables to the new name (applied on their next deploy); project shared variables are not rewritten and are listed in warnings.",
35092
35100
  input: {
35093
35101
  serviceId: id("Service"),
35094
35102
  name: exports_external.string().regex(/^[A-Za-z][A-Za-z0-9_-]{0,39}$/).optional().describe("New service name (letters, digits, _ -; starts with a letter; unique in the environment)"),
@@ -35931,6 +35939,19 @@ var record3 = exports_external.object({
35931
35939
  content: exports_external.string().describe('TXT without surrounding quotes (they are added by the DNS server); MX as "10 mail.example.com"'),
35932
35940
  ttl: exports_external.number().int().optional()
35933
35941
  });
35942
+ var owner = exports_external.object({
35943
+ name: exports_external.string().min(3).max(255),
35944
+ email: exports_external.string().max(255).optional().describe("Gets the registry's verification email; defaults to the account email"),
35945
+ organization: exports_external.string().max(255).optional(),
35946
+ phone: exports_external.string().min(9).max(20).describe("e.g. +6281234567890"),
35947
+ addressLine1: exports_external.string().min(3).max(500),
35948
+ addressLine2: exports_external.string().max(500).optional(),
35949
+ city: exports_external.string().min(3).max(100),
35950
+ state: exports_external.string().min(1).max(100),
35951
+ countryCode: exports_external.string().regex(/^[A-Za-z]{2}$/).describe("ISO 3166-1 alpha-2, e.g. ID"),
35952
+ postalCode: exports_external.string().min(1).max(20),
35953
+ fax: exports_external.string().max(50).optional()
35954
+ });
35934
35955
  var domainTools = [
35935
35956
  defineTool({
35936
35957
  name: "search_domain",
@@ -36010,19 +36031,7 @@ var domainTools = [
36010
36031
  name: exports_external.string().describe("Full domain, e.g. tokosaya.id"),
36011
36032
  nameservers: exports_external.array(exports_external.string()).min(2).max(5).optional(),
36012
36033
  whoisProtection: exports_external.boolean().optional(),
36013
- registrant: exports_external.object({
36014
- name: exports_external.string().min(3).max(255),
36015
- email: exports_external.string().max(255).optional().describe("Gets the registry's verification email; defaults to the account email"),
36016
- organization: exports_external.string().max(255).optional(),
36017
- phone: exports_external.string().min(9).max(20).describe("e.g. +6281234567890"),
36018
- addressLine1: exports_external.string().min(3).max(500),
36019
- addressLine2: exports_external.string().max(500).optional(),
36020
- city: exports_external.string().min(3).max(100),
36021
- state: exports_external.string().min(1).max(100),
36022
- countryCode: exports_external.string().regex(/^[A-Za-z]{2}$/).describe("ISO 3166-1 alpha-2, e.g. ID"),
36023
- postalCode: exports_external.string().min(1).max(20),
36024
- fax: exports_external.string().max(50).optional()
36025
- }).optional().describe("Owner of THIS domain instead of the account's owner details (e.g. a reseller's customer). .id domains only: others answer 400 REGISTRANT_NOT_SUPPORTED. Use only details the user gave."),
36034
+ registrant: owner.optional().describe("Owner of THIS domain instead of the account's owner details (e.g. a reseller's customer). .id domains only: others answer 400 REGISTRANT_NOT_SUPPORTED. Use only details the user gave."),
36026
36035
  dryRun: dryRun3
36027
36036
  },
36028
36037
  run: (a, api2) => api2.api("POST", "/domains/register", { body: { name: a.name, period: 1, nameservers: a.nameservers, whoisProtection: a.whoisProtection, registrant: a.registrant }, query: { dryRun: a.dryRun ? "true" : undefined } })
@@ -36031,9 +36040,23 @@ var domainTools = [
36031
36040
  name: "transfer_domain",
36032
36041
  title: "Transfer domain in",
36033
36042
  kind: "write",
36034
- description: `Transfer a domain from another registrar (adds 1 year). COSTS MONEY from the wallet. Needs the domain's auth code. ${DRY2}`,
36035
- input: { name: exports_external.string(), authCode: exports_external.string(), nameservers: exports_external.array(exports_external.string()).min(2).max(5).optional(), dryRun: dryRun3 },
36036
- run: (a, api2) => api2.api("POST", "/domains/transfer", { body: { name: a.name, authCode: a.authCode, period: 1, nameservers: a.nameservers }, query: { dryRun: a.dryRun ? "true" : undefined } })
36043
+ description: `Transfer a domain from another registrar (adds 1 year). COSTS MONEY from the wallet. Needs the domain's auth code. ${DRY2} With registrant (.id only) the domain gets that owner when the transfer completes (ownerPending until then); without it, the account's owner details.`,
36044
+ input: {
36045
+ name: exports_external.string(),
36046
+ authCode: exports_external.string(),
36047
+ nameservers: exports_external.array(exports_external.string()).min(2).max(5).optional(),
36048
+ registrant: owner.optional().describe("Owner of THIS domain once the transfer completes (e.g. a reseller's customer). .id domains only: others answer 400 REGISTRANT_NOT_SUPPORTED. Use only details the user gave."),
36049
+ dryRun: dryRun3
36050
+ },
36051
+ run: (a, api2) => api2.api("POST", "/domains/transfer", { body: { name: a.name, authCode: a.authCode, period: 1, nameservers: a.nameservers, registrant: a.registrant }, query: { dryRun: a.dryRun ? "true" : undefined } })
36052
+ }),
36053
+ defineTool({
36054
+ name: "set_domain_owner",
36055
+ title: "Set domain owner",
36056
+ kind: "write",
36057
+ description: "Set a new owner (registrant) for one .id domain of the account, e.g. a reseller's customer or a domain transferred earlier. Free. An active domain gets it now (ownerPending false); a transfer in progress gets it when it completes, and a change the registry does not take yet is retried automatically (ownerPending true, HTTP 202). Other domains keep their other contacts. Use only details the user gave.",
36058
+ input: { domainId, owner },
36059
+ run: (a, api2) => api2.api("PATCH", `/domains/${a.domainId}/registrant`, { body: a.owner })
36037
36060
  }),
36038
36061
  defineTool({
36039
36062
  name: "renew_domain",
@@ -36312,6 +36335,24 @@ var emailTools = [
36312
36335
  idempotencyKey: purchaseKey(a.dryRun, a.idempotencyKey)
36313
36336
  })
36314
36337
  }),
36338
+ defineTool({
36339
+ name: "set_email_auto_renew",
36340
+ title: "Set email auto-renew",
36341
+ kind: "write",
36342
+ idempotent: true,
36343
+ description: "Turn the active email plan's auto-renew on or off (sets the value, safe to repeat). On: the plan renews from the wallet deposit when it ends; off: it ends at its expiry date. get_email_plan and email_status show it as autoRenew. A free plan renews at Rp 0.",
36344
+ input: { projectId, autoRenew: exports_external.boolean() },
36345
+ run: (a, api2) => api2.email("POST", "/plan/auto-renew", { project: a.projectId, body: { enabled: a.autoRenew } })
36346
+ }),
36347
+ defineTool({
36348
+ name: "set_email_payg",
36349
+ title: "Set email pay-as-you-go",
36350
+ kind: "write",
36351
+ idempotent: true,
36352
+ description: "Turn pay-as-you-go on or off for the active paid email plan (sets the value, safe to repeat; off by default). Off: once the monthly quota is used, sends stop with 402 PAYG_DISABLED and nothing is charged. On: sends continue past the quota and blocks of 100 emails are bought from the wallet deposit at the plan's per-email price (unused ones refunded if the plan is not renewed). email_status shows it as paygEnabled. No effect on the free plan.",
36353
+ input: { projectId, paygEnabled: exports_external.boolean() },
36354
+ run: (a, api2) => api2.email("POST", "/plan/payg", { project: a.projectId, body: { enabled: a.paygEnabled } })
36355
+ }),
36315
36356
  defineTool({
36316
36357
  name: "send_email",
36317
36358
  title: "Send email",
@@ -36385,6 +36426,30 @@ var emailTools = [
36385
36426
  input: { projectId, domainId: exports_external.string() },
36386
36427
  run: (a, api2) => api2.email("POST", `/domains/${a.domainId}/verify`, { project: a.projectId })
36387
36428
  }),
36429
+ defineTool({
36430
+ name: "list_email_keys",
36431
+ title: "List email keys",
36432
+ kind: "read",
36433
+ description: "Email API keys of a project (name, prefix, last used, expiry; never the key itself). Needs a Helipod API key with the Email API: an email key or a sign-in token answers 403 KEY_NOT_ALLOWED.",
36434
+ input: { projectId },
36435
+ run: (a, api2) => api2.emailWithAccountKey("GET", "/keys", { project: a.projectId })
36436
+ }),
36437
+ defineTool({
36438
+ name: "create_email_key",
36439
+ title: "Create email key",
36440
+ kind: "write",
36441
+ description: "Make an email API key for a project, e.g. one per customer of a reseller. The key works on the Email API and as the SMTP password (smtp.helipod.io, username apikey) for that project only, and cannot make keys itself. The full key is in the answer ONCE: give it to the user to store; it cannot be shown again. Needs a Helipod API key with the Email API (an email key or a sign-in token answers 403 KEY_NOT_ALLOWED). Per project: 5 active keys on Free/Starter, 10 on Pro, 50 on Business.",
36442
+ input: { projectId, name: exports_external.string().min(1).max(100).describe("To tell keys apart, e.g. the customer's name"), expiresInDays: exports_external.number().int().min(1).max(3650).optional() },
36443
+ run: (a, api2) => api2.emailWithAccountKey("POST", "/keys", { project: a.projectId, body: { name: a.name, expiresInDays: a.expiresInDays } })
36444
+ }),
36445
+ defineTool({
36446
+ name: "revoke_email_key",
36447
+ title: "Revoke email key",
36448
+ kind: "destructive",
36449
+ description: "Revoke an email API key of the project: it stops working at once, also for open SMTP sessions. Apps using it can no longer send. Needs a Helipod API key with the Email API.",
36450
+ input: { projectId, keyId: uid("Email key") },
36451
+ run: (a, api2) => api2.emailWithAccountKey("DELETE", `/keys/${a.keyId}`, { project: a.projectId })
36452
+ }),
36388
36453
  defineTool({
36389
36454
  name: "list_email_templates",
36390
36455
  title: "List email templates",
@@ -37638,7 +37703,7 @@ var generalTools = [
37638
37703
  ];
37639
37704
 
37640
37705
  // src/server.ts
37641
- var VERSION = "0.5.13";
37706
+ var VERSION = "0.5.17";
37642
37707
  var INSTRUCTIONS = `Helipod is a cloud platform (Indonesia): containers, managed Postgres databases, VPS, domains, email and an AI gateway, billed from a prepaid wallet in Rupiah.
37643
37708
  - Start with list_projects to find projects and environment ids. Account tools (get_wallet, invoices, statements, notifications, get_project_activity, support tickets) cover the account; AI tools (list_ai_models, get_ai_overview, get_ai_usage, AI keys, top-ups, generate_ai_image) cover the AI gateway.
37644
37709
  - Anything that costs money (create/deploy/scale services, resize volumes, upgrade a database plan, create/renew/upgrade VPS, VPS backups, register/renew/transfer domains, email plans and inbox add-ons, AI top-ups and auto top-up, generating images): call it with dryRun=true first (or the price lookup when it has no dry run), tell the user the cost in IDR and the wallet balance after, and wait for an explicit yes before the real call. Never treat silence or an earlier general OK as a yes.
@@ -37647,6 +37712,8 @@ var INSTRUCTIONS = `Helipod is a cloud platform (Indonesia): containers, managed
37647
37712
  - Deploying an app, step by step: list_projects (pick the project and its production environmentId) -> get_bundle (is it covered?) -> if it needs Postgres: create_database, then get_database_connection and store DATABASE_URL with set_variables (secrets) after the service exists, or pass it in create_service variables -> create_service with dryRun=true (repo or image, port, variables) -> tell the user the cost (included in the bundle, or walletDailyCost per day) and wait for their OK -> create_service for real -> poll get_deployment until it settles, read get_deployment_logs if it failed -> give the user the *.helipod.app URL from get_service. Ask the user for secrets (API keys) only when the app needs them; never invent values.
37648
37713
  - Destructive tools (delete, terminate, reinstall, rollback, restore a backup or an import, copy a backup onto another VPS, replace DNS, revoke a key, remove an SSH key or port forward, send a broadcast, execute_database_sql): confirm with the user and name what will be lost. Before execute_database_sql, run classify_database_sql.
37649
37714
  - Email: when a project has no email yet (email_status shows activated false) or its plan ended, start it with buy_email_plan (dryRun first; the FREE plan costs nothing, still confirm). That also sets email up for the project; then add and verify a sending domain.
37715
+ - Email keys: create_email_key gives an app or a customer its own key (also its SMTP password: smtp.helipod.io, username apikey); the full key is shown once, so hand it over and never print it again. Domain on Helipod: add_domain_email_records puts the sending domain's DNS records in place in one call.
37716
+ - Domains for someone else (resellers): register_domain and transfer_domain take registrant (.id only; a transfer gets the owner when it completes), set_domain_owner changes it later. Use only details the user gave.
37650
37717
  - Before replacing all DNS records (set_dns_records), read list_dns_records and keep every record the user still needs. Before sending or scheduling a broadcast, tell the user how many contacts it reaches. Before deleting or replacing code, files or variables, read the current value. After renaming a service, fix the variable references listed in warnings.
37651
37718
  - Databases: get_database_connection returns a password; put it in a secret variable (DATABASE_URL) instead of code or chat.
37652
37719
  - Migrate (free): create_vps_import returns a one-time command for the USER to run as root on their old server (never run it yourself); before the first restore_vps_import, show fetchedIp and ask the user to confirm it is their server.
package/dist/stdio.js CHANGED
@@ -34875,6 +34875,14 @@ class HelipodClient {
34875
34875
  const headers = !this.cfg.emailApiKey && opts.project ? { "x-helipod-project": opts.project } : undefined;
34876
34876
  return this.request(this.emailApiUrl, key, method, path, { ...opts, headers });
34877
34877
  }
34878
+ emailWithAccountKey(method, path, opts = {}) {
34879
+ const key = this.cfg.apiKey;
34880
+ if (!key) {
34881
+ throw new HelipodError(401, "NO_API_KEY", "Managing email keys needs HELIPOD_API_KEY (a Helipod API key with the Email API enabled); an email key cannot do it.");
34882
+ }
34883
+ const headers = opts.project ? { "x-helipod-project": opts.project } : undefined;
34884
+ return this.request(this.emailApiUrl, key, method, path, { ...opts, headers });
34885
+ }
34878
34886
  async image(link, maxBytes) {
34879
34887
  let url2;
34880
34888
  try {
@@ -35005,7 +35013,7 @@ var containerTools = [
35005
35013
  kind: "read",
35006
35014
  description: "Projects this API key can reach, each with its environments (production, staging, ...). Start here to find an environmentId.",
35007
35015
  input: {},
35008
- run: (_a3, api2) => api2.api("GET", `${C}/projects`)
35016
+ run: (_a3, api2) => api2.api("GET", `/projects`)
35009
35017
  }),
35010
35018
  defineTool({
35011
35019
  name: "get_project",
@@ -35013,7 +35021,7 @@ var containerTools = [
35013
35021
  kind: "read",
35014
35022
  description: "One project with its environments (ids and names). Use it to find an environmentId when you already know the project.",
35015
35023
  input: { projectId: id("Project") },
35016
- run: (a, api2) => api2.api("GET", `${C}/projects/${a.projectId}`)
35024
+ run: (a, api2) => api2.api("GET", `/projects/${a.projectId}`)
35017
35025
  }),
35018
35026
  defineTool({
35019
35027
  name: "create_project",
@@ -35021,7 +35029,7 @@ var containerTools = [
35021
35029
  kind: "write",
35022
35030
  description: "Create a project (free) with its default production environment. Needs a whole-account key (a key limited to one project gets 403). Pass an idempotencyKey when retrying.",
35023
35031
  input: { name: exports_external.string().min(3).max(100), description: exports_external.string().max(500).optional(), idempotencyKey: idem },
35024
- run: (a, api2) => api2.api("POST", `${C}/projects`, { body: { name: a.name, description: a.description }, idempotencyKey: a.idempotencyKey })
35032
+ run: (a, api2) => api2.api("POST", `/projects`, { body: { name: a.name, description: a.description }, idempotencyKey: a.idempotencyKey })
35025
35033
  }),
35026
35034
  defineTool({
35027
35035
  name: "update_project",
@@ -35030,7 +35038,7 @@ var containerTools = [
35030
35038
  idempotent: true,
35031
35039
  description: "Change a project's name and/or description.",
35032
35040
  input: { projectId: id("Project"), name: exports_external.string().min(3).max(100).optional(), description: exports_external.string().max(500).optional() },
35033
- run: (a, api2) => api2.api("PATCH", `${C}/projects/${a.projectId}`, { body: { name: a.name, description: a.description } })
35041
+ run: (a, api2) => api2.api("PATCH", `/projects/${a.projectId}`, { body: { name: a.name, description: a.description } })
35034
35042
  }),
35035
35043
  defineTool({
35036
35044
  name: "delete_project",
@@ -35038,7 +35046,7 @@ var containerTools = [
35038
35046
  kind: "destructive",
35039
35047
  description: "Delete an EMPTY project and its environments and shared variables. Irreversible. Refused (409 PROJECT_NOT_EMPTY) while it still has services, databases, VPS or other resources: those must be deleted first. Needs a whole-account key. Only after the user confirms; confirm must be the project's exact name.",
35040
35048
  input: { projectId: id("Project"), confirm: exports_external.string().max(255).describe("The project name, exactly") },
35041
- run: (a, api2) => api2.api("DELETE", `${C}/projects/${a.projectId}`, { body: { confirm: a.confirm } })
35049
+ run: (a, api2) => api2.api("DELETE", `/projects/${a.projectId}`, { body: { confirm: a.confirm } })
35042
35050
  }),
35043
35051
  defineTool({
35044
35052
  name: "create_environment",
@@ -35046,7 +35054,7 @@ var containerTools = [
35046
35054
  kind: "write",
35047
35055
  description: "Add an environment (e.g. staging) to a project. Free; services in it are billed as usual once deployed.",
35048
35056
  input: { projectId: id("Project"), name: exports_external.string().regex(/^[A-Za-z0-9][A-Za-z0-9 _.-]{0,49}$/).describe("1-50 characters: letters, digits, space _ . -") },
35049
- run: (a, api2) => api2.api("POST", `${C}/projects/${a.projectId}/environments`, { body: { name: a.name } })
35057
+ run: (a, api2) => api2.api("POST", `/projects/${a.projectId}/environments`, { body: { name: a.name } })
35050
35058
  }),
35051
35059
  defineTool({
35052
35060
  name: "rename_environment",
@@ -35055,7 +35063,7 @@ var containerTools = [
35055
35063
  idempotent: true,
35056
35064
  description: "Rename an environment (names are unique within the project).",
35057
35065
  input: { environmentId: id("Environment"), name: exports_external.string().regex(/^[A-Za-z0-9][A-Za-z0-9 _.-]{0,49}$/) },
35058
- run: (a, api2) => api2.api("PATCH", `${C}/environments/${a.environmentId}`, { body: { name: a.name } })
35066
+ run: (a, api2) => api2.api("PATCH", `/environments/${a.environmentId}`, { body: { name: a.name } })
35059
35067
  }),
35060
35068
  defineTool({
35061
35069
  name: "delete_environment",
@@ -35063,7 +35071,7 @@ var containerTools = [
35063
35071
  kind: "destructive",
35064
35072
  description: "Delete an EMPTY environment (409 ENVIRONMENT_NOT_EMPTY while it has services: delete them first). Irreversible. Only after the user confirms; confirm must be the environment's exact name.",
35065
35073
  input: { environmentId: id("Environment"), confirm: exports_external.string().max(255).describe("The environment name, exactly") },
35066
- run: (a, api2) => api2.api("DELETE", `${C}/environments/${a.environmentId}`, { body: { confirm: a.confirm } })
35074
+ run: (a, api2) => api2.api("DELETE", `/environments/${a.environmentId}`, { body: { confirm: a.confirm } })
35067
35075
  }),
35068
35076
  defineTool({
35069
35077
  name: "list_project_variables",
@@ -35098,7 +35106,7 @@ var containerTools = [
35098
35106
  name: "list_services",
35099
35107
  title: "List services",
35100
35108
  kind: "read",
35101
- description: "Services (containers) in an environment: status, source, resources, domains, latest deployment and daily cost.",
35109
+ description: "Services (containers) in an environment: status, source, resources, domains, latest deployment and daily cost. Status awaiting_payment = a deploy was refused because the wallet could not pay: top up or buy a bundle (get_bundle), then deploy_service again; nothing is lost.",
35102
35110
  input: { environmentId: id("Environment") },
35103
35111
  run: (a, api2) => api2.api("GET", `${C}/environments/${a.environmentId}/services`)
35104
35112
  }),
@@ -35195,7 +35203,7 @@ var containerTools = [
35195
35203
  title: "Update / rename service",
35196
35204
  kind: "write",
35197
35205
  idempotent: true,
35198
- description: "Rename a service or change its start command, root directory, health check or startup dependencies. Settings apply on the next deploy (needsRedeploy). Renaming breaks references like ${{oldName.KEY}} in other services' variables: the response lists them in warnings.",
35206
+ description: "Rename a service or change its start command, root directory, health check or startup dependencies. Settings apply on the next deploy (needsRedeploy). Renaming rewrites references like ${{oldName.KEY}} in the environment's service variables to the new name (applied on their next deploy); project shared variables are not rewritten and are listed in warnings.",
35199
35207
  input: {
35200
35208
  serviceId: id("Service"),
35201
35209
  name: exports_external.string().regex(/^[A-Za-z][A-Za-z0-9_-]{0,39}$/).optional().describe("New service name (letters, digits, _ -; starts with a letter; unique in the environment)"),
@@ -36038,6 +36046,19 @@ var record3 = exports_external.object({
36038
36046
  content: exports_external.string().describe('TXT without surrounding quotes (they are added by the DNS server); MX as "10 mail.example.com"'),
36039
36047
  ttl: exports_external.number().int().optional()
36040
36048
  });
36049
+ var owner = exports_external.object({
36050
+ name: exports_external.string().min(3).max(255),
36051
+ email: exports_external.string().max(255).optional().describe("Gets the registry's verification email; defaults to the account email"),
36052
+ organization: exports_external.string().max(255).optional(),
36053
+ phone: exports_external.string().min(9).max(20).describe("e.g. +6281234567890"),
36054
+ addressLine1: exports_external.string().min(3).max(500),
36055
+ addressLine2: exports_external.string().max(500).optional(),
36056
+ city: exports_external.string().min(3).max(100),
36057
+ state: exports_external.string().min(1).max(100),
36058
+ countryCode: exports_external.string().regex(/^[A-Za-z]{2}$/).describe("ISO 3166-1 alpha-2, e.g. ID"),
36059
+ postalCode: exports_external.string().min(1).max(20),
36060
+ fax: exports_external.string().max(50).optional()
36061
+ });
36041
36062
  var domainTools = [
36042
36063
  defineTool({
36043
36064
  name: "search_domain",
@@ -36117,19 +36138,7 @@ var domainTools = [
36117
36138
  name: exports_external.string().describe("Full domain, e.g. tokosaya.id"),
36118
36139
  nameservers: exports_external.array(exports_external.string()).min(2).max(5).optional(),
36119
36140
  whoisProtection: exports_external.boolean().optional(),
36120
- registrant: exports_external.object({
36121
- name: exports_external.string().min(3).max(255),
36122
- email: exports_external.string().max(255).optional().describe("Gets the registry's verification email; defaults to the account email"),
36123
- organization: exports_external.string().max(255).optional(),
36124
- phone: exports_external.string().min(9).max(20).describe("e.g. +6281234567890"),
36125
- addressLine1: exports_external.string().min(3).max(500),
36126
- addressLine2: exports_external.string().max(500).optional(),
36127
- city: exports_external.string().min(3).max(100),
36128
- state: exports_external.string().min(1).max(100),
36129
- countryCode: exports_external.string().regex(/^[A-Za-z]{2}$/).describe("ISO 3166-1 alpha-2, e.g. ID"),
36130
- postalCode: exports_external.string().min(1).max(20),
36131
- fax: exports_external.string().max(50).optional()
36132
- }).optional().describe("Owner of THIS domain instead of the account's owner details (e.g. a reseller's customer). .id domains only: others answer 400 REGISTRANT_NOT_SUPPORTED. Use only details the user gave."),
36141
+ registrant: owner.optional().describe("Owner of THIS domain instead of the account's owner details (e.g. a reseller's customer). .id domains only: others answer 400 REGISTRANT_NOT_SUPPORTED. Use only details the user gave."),
36133
36142
  dryRun: dryRun3
36134
36143
  },
36135
36144
  run: (a, api2) => api2.api("POST", "/domains/register", { body: { name: a.name, period: 1, nameservers: a.nameservers, whoisProtection: a.whoisProtection, registrant: a.registrant }, query: { dryRun: a.dryRun ? "true" : undefined } })
@@ -36138,9 +36147,23 @@ var domainTools = [
36138
36147
  name: "transfer_domain",
36139
36148
  title: "Transfer domain in",
36140
36149
  kind: "write",
36141
- description: `Transfer a domain from another registrar (adds 1 year). COSTS MONEY from the wallet. Needs the domain's auth code. ${DRY2}`,
36142
- input: { name: exports_external.string(), authCode: exports_external.string(), nameservers: exports_external.array(exports_external.string()).min(2).max(5).optional(), dryRun: dryRun3 },
36143
- run: (a, api2) => api2.api("POST", "/domains/transfer", { body: { name: a.name, authCode: a.authCode, period: 1, nameservers: a.nameservers }, query: { dryRun: a.dryRun ? "true" : undefined } })
36150
+ description: `Transfer a domain from another registrar (adds 1 year). COSTS MONEY from the wallet. Needs the domain's auth code. ${DRY2} With registrant (.id only) the domain gets that owner when the transfer completes (ownerPending until then); without it, the account's owner details.`,
36151
+ input: {
36152
+ name: exports_external.string(),
36153
+ authCode: exports_external.string(),
36154
+ nameservers: exports_external.array(exports_external.string()).min(2).max(5).optional(),
36155
+ registrant: owner.optional().describe("Owner of THIS domain once the transfer completes (e.g. a reseller's customer). .id domains only: others answer 400 REGISTRANT_NOT_SUPPORTED. Use only details the user gave."),
36156
+ dryRun: dryRun3
36157
+ },
36158
+ run: (a, api2) => api2.api("POST", "/domains/transfer", { body: { name: a.name, authCode: a.authCode, period: 1, nameservers: a.nameservers, registrant: a.registrant }, query: { dryRun: a.dryRun ? "true" : undefined } })
36159
+ }),
36160
+ defineTool({
36161
+ name: "set_domain_owner",
36162
+ title: "Set domain owner",
36163
+ kind: "write",
36164
+ description: "Set a new owner (registrant) for one .id domain of the account, e.g. a reseller's customer or a domain transferred earlier. Free. An active domain gets it now (ownerPending false); a transfer in progress gets it when it completes, and a change the registry does not take yet is retried automatically (ownerPending true, HTTP 202). Other domains keep their other contacts. Use only details the user gave.",
36165
+ input: { domainId, owner },
36166
+ run: (a, api2) => api2.api("PATCH", `/domains/${a.domainId}/registrant`, { body: a.owner })
36144
36167
  }),
36145
36168
  defineTool({
36146
36169
  name: "renew_domain",
@@ -36419,6 +36442,24 @@ var emailTools = [
36419
36442
  idempotencyKey: purchaseKey(a.dryRun, a.idempotencyKey)
36420
36443
  })
36421
36444
  }),
36445
+ defineTool({
36446
+ name: "set_email_auto_renew",
36447
+ title: "Set email auto-renew",
36448
+ kind: "write",
36449
+ idempotent: true,
36450
+ description: "Turn the active email plan's auto-renew on or off (sets the value, safe to repeat). On: the plan renews from the wallet deposit when it ends; off: it ends at its expiry date. get_email_plan and email_status show it as autoRenew. A free plan renews at Rp 0.",
36451
+ input: { projectId, autoRenew: exports_external.boolean() },
36452
+ run: (a, api2) => api2.email("POST", "/plan/auto-renew", { project: a.projectId, body: { enabled: a.autoRenew } })
36453
+ }),
36454
+ defineTool({
36455
+ name: "set_email_payg",
36456
+ title: "Set email pay-as-you-go",
36457
+ kind: "write",
36458
+ idempotent: true,
36459
+ description: "Turn pay-as-you-go on or off for the active paid email plan (sets the value, safe to repeat; off by default). Off: once the monthly quota is used, sends stop with 402 PAYG_DISABLED and nothing is charged. On: sends continue past the quota and blocks of 100 emails are bought from the wallet deposit at the plan's per-email price (unused ones refunded if the plan is not renewed). email_status shows it as paygEnabled. No effect on the free plan.",
36460
+ input: { projectId, paygEnabled: exports_external.boolean() },
36461
+ run: (a, api2) => api2.email("POST", "/plan/payg", { project: a.projectId, body: { enabled: a.paygEnabled } })
36462
+ }),
36422
36463
  defineTool({
36423
36464
  name: "send_email",
36424
36465
  title: "Send email",
@@ -36492,6 +36533,30 @@ var emailTools = [
36492
36533
  input: { projectId, domainId: exports_external.string() },
36493
36534
  run: (a, api2) => api2.email("POST", `/domains/${a.domainId}/verify`, { project: a.projectId })
36494
36535
  }),
36536
+ defineTool({
36537
+ name: "list_email_keys",
36538
+ title: "List email keys",
36539
+ kind: "read",
36540
+ description: "Email API keys of a project (name, prefix, last used, expiry; never the key itself). Needs a Helipod API key with the Email API: an email key or a sign-in token answers 403 KEY_NOT_ALLOWED.",
36541
+ input: { projectId },
36542
+ run: (a, api2) => api2.emailWithAccountKey("GET", "/keys", { project: a.projectId })
36543
+ }),
36544
+ defineTool({
36545
+ name: "create_email_key",
36546
+ title: "Create email key",
36547
+ kind: "write",
36548
+ description: "Make an email API key for a project, e.g. one per customer of a reseller. The key works on the Email API and as the SMTP password (smtp.helipod.io, username apikey) for that project only, and cannot make keys itself. The full key is in the answer ONCE: give it to the user to store; it cannot be shown again. Needs a Helipod API key with the Email API (an email key or a sign-in token answers 403 KEY_NOT_ALLOWED). Per project: 5 active keys on Free/Starter, 10 on Pro, 50 on Business.",
36549
+ input: { projectId, name: exports_external.string().min(1).max(100).describe("To tell keys apart, e.g. the customer's name"), expiresInDays: exports_external.number().int().min(1).max(3650).optional() },
36550
+ run: (a, api2) => api2.emailWithAccountKey("POST", "/keys", { project: a.projectId, body: { name: a.name, expiresInDays: a.expiresInDays } })
36551
+ }),
36552
+ defineTool({
36553
+ name: "revoke_email_key",
36554
+ title: "Revoke email key",
36555
+ kind: "destructive",
36556
+ description: "Revoke an email API key of the project: it stops working at once, also for open SMTP sessions. Apps using it can no longer send. Needs a Helipod API key with the Email API.",
36557
+ input: { projectId, keyId: uid("Email key") },
36558
+ run: (a, api2) => api2.emailWithAccountKey("DELETE", `/keys/${a.keyId}`, { project: a.projectId })
36559
+ }),
36495
36560
  defineTool({
36496
36561
  name: "list_email_templates",
36497
36562
  title: "List email templates",
@@ -37745,7 +37810,7 @@ var generalTools = [
37745
37810
  ];
37746
37811
 
37747
37812
  // src/server.ts
37748
- var VERSION = "0.5.13";
37813
+ var VERSION = "0.5.17";
37749
37814
  var INSTRUCTIONS = `Helipod is a cloud platform (Indonesia): containers, managed Postgres databases, VPS, domains, email and an AI gateway, billed from a prepaid wallet in Rupiah.
37750
37815
  - Start with list_projects to find projects and environment ids. Account tools (get_wallet, invoices, statements, notifications, get_project_activity, support tickets) cover the account; AI tools (list_ai_models, get_ai_overview, get_ai_usage, AI keys, top-ups, generate_ai_image) cover the AI gateway.
37751
37816
  - Anything that costs money (create/deploy/scale services, resize volumes, upgrade a database plan, create/renew/upgrade VPS, VPS backups, register/renew/transfer domains, email plans and inbox add-ons, AI top-ups and auto top-up, generating images): call it with dryRun=true first (or the price lookup when it has no dry run), tell the user the cost in IDR and the wallet balance after, and wait for an explicit yes before the real call. Never treat silence or an earlier general OK as a yes.
@@ -37754,6 +37819,8 @@ var INSTRUCTIONS = `Helipod is a cloud platform (Indonesia): containers, managed
37754
37819
  - Deploying an app, step by step: list_projects (pick the project and its production environmentId) -> get_bundle (is it covered?) -> if it needs Postgres: create_database, then get_database_connection and store DATABASE_URL with set_variables (secrets) after the service exists, or pass it in create_service variables -> create_service with dryRun=true (repo or image, port, variables) -> tell the user the cost (included in the bundle, or walletDailyCost per day) and wait for their OK -> create_service for real -> poll get_deployment until it settles, read get_deployment_logs if it failed -> give the user the *.helipod.app URL from get_service. Ask the user for secrets (API keys) only when the app needs them; never invent values.
37755
37820
  - Destructive tools (delete, terminate, reinstall, rollback, restore a backup or an import, copy a backup onto another VPS, replace DNS, revoke a key, remove an SSH key or port forward, send a broadcast, execute_database_sql): confirm with the user and name what will be lost. Before execute_database_sql, run classify_database_sql.
37756
37821
  - Email: when a project has no email yet (email_status shows activated false) or its plan ended, start it with buy_email_plan (dryRun first; the FREE plan costs nothing, still confirm). That also sets email up for the project; then add and verify a sending domain.
37822
+ - Email keys: create_email_key gives an app or a customer its own key (also its SMTP password: smtp.helipod.io, username apikey); the full key is shown once, so hand it over and never print it again. Domain on Helipod: add_domain_email_records puts the sending domain's DNS records in place in one call.
37823
+ - Domains for someone else (resellers): register_domain and transfer_domain take registrant (.id only; a transfer gets the owner when it completes), set_domain_owner changes it later. Use only details the user gave.
37757
37824
  - Before replacing all DNS records (set_dns_records), read list_dns_records and keep every record the user still needs. Before sending or scheduling a broadcast, tell the user how many contacts it reaches. Before deleting or replacing code, files or variables, read the current value. After renaming a service, fix the variable references listed in warnings.
37758
37825
  - Databases: get_database_connection returns a password; put it in a secret variable (DATABASE_URL) instead of code or chat.
37759
37826
  - Migrate (free): create_vps_import returns a one-time command for the USER to run as root on their old server (never run it yourself); before the first restore_vps_import, show fetchedIp and ask the user to confirm it is their server.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "helipod-mcp",
3
3
  "mcpName": "io.helipod/helipod",
4
- "version": "0.5.13",
4
+ "version": "0.5.17",
5
5
  "description": "MCP server for Helipod: containers, databases, VPS, domains and email from Claude Code, Codex, Cursor and other AI tools.",
6
6
  "type": "module",
7
7
  "bin": {
@@ -11,7 +11,7 @@ Helipod is a cloud platform in Indonesia: containers (deploy apps from Git, imag
11
11
 
12
12
  - **MCP tools** (preferred): if tools like `list_projects`, `create_service`, `list_vps_plans` are available, use them.
13
13
  - **Without MCP**: call the HTTP API with `Authorization: Bearer $HELIPOD_API_KEY`.
14
- - Container, Database, VPS, Domain APIs: `https://api.helipod.io/api` (`/containers/...`, `/databases/...`, `/vps/...`, `/domains/...`)
14
+ - Projects, Container, Database, VPS, Domain APIs: `https://api.helipod.io/api` (`/projects` works with a key of any product; `/containers/...`, `/databases/...`, `/vps/...`, `/domains/...`)
15
15
  - Email API: `https://mail.helipod.io/api/email` with `$HELIPOD_API_KEY` when the key has the Email API enabled (a whole-account key adds `X-Helipod-Project: <projectId>`), or a separate email key `$HELIPOD_EMAIL_API_KEY`
16
16
  - AI gateway: `https://ai.helipod.io/v1` with a `hel_ai_...` key (OpenAI and Anthropic compatible)
17
17
  - Keys are created in the dashboard: Project → Settings → API keys. A key is limited to the products and project chosen there; `403 SCOPE_NOT_ALLOWED` means the key lacks that product, `404` on an id means it is outside the key's reach.
@@ -29,11 +29,14 @@ Helipod is a cloud platform in Indonesia: containers (deploy apps from Git, imag
29
29
 
30
30
  | Task | Skill |
31
31
  |---|---|
32
- | Deploy an app, env vars, domains, scaling, logs | `helipod-deploy` |
32
+ | Deploy an app on containers, env vars, domains, scaling, logs | `helipod-deploy` |
33
+ | Run an app on a VPS (WordPress, n8n, panels, your own) | `helipod-vps-deploy` |
34
+ | Go live end to end: app + database + domain + email, or WordPress + domain + mail | `helipod-launch` |
35
+ | Sell Helipod to your own customers (project, email key, domain owner per customer) | `helipod-reseller` |
33
36
  | Managed Postgres: create, connect, branches, SQL, plans | `helipod-databases` |
34
- | Servers with root access | `helipod-vps` |
37
+ | Servers with root access: order, power, firewall, snapshots, migrate | `helipod-vps` |
35
38
  | Buy a domain, DNS, point a domain at an app | `helipod-domains` |
36
- | Send email from an app | `helipod-email` |
39
+ | Send or receive email, email keys and SMTP | `helipod-email` |
37
40
  | Use AI models in an app through Helipod | `helipod-ai-gateway` |
38
41
 
39
42
  Docs: https://docs.helipod.io
@@ -23,7 +23,7 @@ Follow the `helipod` skill rules (price first, confirm destructive actions, neve
23
23
  | Needs Postgres | The managed database: `create_database` (FREE plan, nothing to pay), then `get_database_connection` → `DATABASE_URL` as a secret variable. Not a Postgres container |
24
24
  | A known app or other database (MySQL, Redis, WordPress, n8n, ...) | `list_templates` → `deploy_template` (`dryRun: true` first) |
25
25
 
26
- Private Git repos are connected in the dashboard (GitHub/GitLab integration), not through the API.
26
+ Private Git repos: `get_git_connections` shows whether GitHub / GitLab is connected, `list_git_repos` lists what it can reach; pass a repo `url` to `create_service` (such services redeploy on every push). Not connected yet: the call answers 409 with `details.connectUrl`; give the user that link to connect in their browser, then retry. Never try to connect it yourself.
27
27
 
28
28
  Before deploying a repo, check it quickly: the port the app listens on (prefer `process.env.PORT`), the start command, a health endpoint. For Next.js Helipack adds `output: "standalone"` itself. To override detection, add `helipack.json` at the app root:
29
29
 
@@ -55,6 +55,10 @@ Before deploying a repo, check it quickly: the port the app listens on (prefer `
55
55
  - Custom domain: `add_service_domain` with `kind: "custom"`, then tell the user the DNS record from the response (`CNAME` to `cname.helipod.app`, or the `A` record for a root domain). If the domain is on Helipod, the `helipod-domains` skill can create the record.
56
56
  - Persistent files: `add_volume` (mount path), then redeploy. Without a volume, files are lost on every deploy.
57
57
 
58
+ ## Domain and email for the app
59
+
60
+ A custom domain and working email together: `helipod-launch` (recipe A) has the whole sequence, including one-call DNS when the domain is on Helipod.
61
+
58
62
  ## Several services at once
59
63
 
60
64
  `create_stack` takes a template spec (services with `source`, `variables`, `networking`, `volumes`, `dependsOn`) and deploys them in dependency order. Always `dryRun: true` first. Spec reference: https://docs.helipod.io/docs/container-api/templates-and-stacks
@@ -15,7 +15,7 @@ Follow the `helipod` skill rules. Domains belong to the account: the key must be
15
15
  4. Tell the user the total and get a clear yes, then `register_domain` (1 year; `whoisProtection: true` if they want privacy).
16
16
  5. Premium names are not sold online (`PREMIUM_NOT_SUPPORTED`).
17
17
 
18
- Transfer in: `transfer_domain` with the auth code from the old registrar (1 year added, priced like a transfer). Renew: `renew_domain`, or let it renew itself: `set_domain_auto_renew` (on = renewed from the wallet before expiry; off = it expires unless renewed by hand). WHOIS privacy later: `enable_whois_privacy` with `dryRun: true` first.
18
+ Transfer in: `transfer_domain` with the auth code from the old registrar (1 year added, priced like a transfer). For a .id domain owned by someone else, pass `registrant`: it becomes the owner when the transfer completes (`ownerPending` until then; without it, the account's owner details). Change the owner of a .id domain already on the account: `set_domain_owner` (free). Renew: `renew_domain`, or let it renew itself: `set_domain_auto_renew` (on = renewed from the wallet before expiry; off = it expires unless renewed by hand). WHOIS privacy later: `enable_whois_privacy` with `dryRun: true` first.
19
19
 
20
20
  ## DNS
21
21
 
@@ -9,10 +9,16 @@ Uses the Helipod API key `HELIPOD_API_KEY` with **Email API** enabled (Settings
9
9
 
10
10
  ## Set up
11
11
 
12
- 1. `email_status`: the project needs an email plan (the Free plan is fine for testing).
13
- 2. `add_email_domain` with the domain to send from (often a subdomain like `mail.example.com`), then give the user the DNS records from the response exactly as listed (copy them verbatim). If the domain is on Helipod, use the `helipod-domains` skill to add them.
12
+ 1. `email_status`: the project needs an email plan (the Free plan is fine for testing). A paid plan renews from the wallet when it ends; `set_email_auto_renew` turns that on or off. Over the monthly quota, sends stop (`402 PAYG_DISABLED`) unless pay-as-you-go is on (`set_email_payg`, off by default; it buys blocks of 100 emails from the wallet).
13
+ 2. `add_email_domain` with the domain to send from (often a subdomain like `mail.example.com`). Domain on Helipod: `list_domain_email_records` then `add_domain_email_records` puts every record in place in one call. Elsewhere: give the user the DNS records from the response exactly as listed (copy them verbatim).
14
14
  3. `verify_email_domain` until it is verified.
15
15
 
16
+ ## Email keys and SMTP
17
+
18
+ - An app or a customer gets its own key: `create_email_key` (name it after the app or customer). The full key is in the answer once; store it as a secret. It works for that project only, and cannot make keys.
19
+ - `list_email_keys`, `revoke_email_key` (stops it at once, also open SMTP sessions). Managing keys needs the Helipod API key (`HELIPOD_API_KEY`); an email key or an AI-tool sign-in answers `403 KEY_NOT_ALLOWED`.
20
+ - SMTP for apps that only speak SMTP (WordPress, Laravel, PHPMailer): host `smtp.helipod.io`, port 587 (STARTTLS) or 465 (SSL), username `apikey`, password = an email key (not the Helipod API key). From = an address on a verified domain.
21
+
16
22
  ## Send from code
17
23
 
18
24
  Never put the key in client-side code. Store it as a secret (on Helipod: `set_variables` with `secrets`).
@@ -0,0 +1,32 @@
1
+ ---
2
+ name: helipod-launch
3
+ description: Launch a complete setup on Helipod in one go - an app with its Postgres database, its own domain with HTTPS and email that works (sending domain, SMTP), or a WordPress / n8n site on a VPS with a domain and mail. Use when the user wants to go live end to end ("deploy my app with a database and my domain", "set up WordPress with email") rather than one product.
4
+ ---
5
+
6
+ # Launch end to end on Helipod
7
+
8
+ Follow the `helipod` skill rules. Ask for everything that costs money up front (domain, VPS, paid plans), price each with a dry run, and get one clear yes for the total before buying. Prefer free parts: the FREE database and email plans, a `*.helipod.app` URL until the domain is ready.
9
+
10
+ ## Recipe A: web app + database + domain + email (containers)
11
+
12
+ 1. `list_projects` → the project and its production `environmentId` (`create_project` for a new one).
13
+ 2. Database: `create_database` (FREE plan). After the service exists, `get_database_connection` → `set_variables` with `DATABASE_URL` in `secrets`.
14
+ 3. App: follow `helipod-deploy` (dry run, create, poll `get_deployment`, logs on failure). Expose the app's port so it gets a `*.helipod.app` URL.
15
+ 4. Domain: `search_domain` → `domain_prices` / `register_domain` with `dryRun: true` → yes → register. Then `add_service_domain` with `kind: "custom"` and create the DNS record it returns with `update_dns_record` (the domain is on Helipod, so it is one call). `verify_service_domain` until HTTPS is active.
16
+ 5. Email: `buy_email_plan` (FREE, `dryRun` first) → `add_email_domain` with a subdomain such as `mail.example.com` → `add_domain_email_records` puts every DNS record in place in one call (domain on Helipod) → `verify_email_domain`. The app sends with `HELIPOD_API_KEY` (Email API enabled) or an email key from `create_email_key` stored as a secret.
17
+ 6. Check: open the URL, send one test email with `send_email`, read `list_emails` for delivery.
18
+
19
+ ## Recipe B: WordPress (or n8n) on a VPS + domain + email
20
+
21
+ 1. Follow `helipod-vps-deploy` with the `wordpress-…` (or `n8n-…`) image.
22
+ 2. Domain: register or use one on Helipod, `update_dns_record` `A` → the server IP; HTTPS as in `helipod-vps-deploy`.
23
+ 3. Email: Recipe A step 5, then `create_email_key` and put it in the site's SMTP settings (WP Mail SMTP: host `smtp.helipod.io`, port 587, username `apikey`, password = the key, from = an address on the verified domain).
24
+
25
+ ## Recipe C: a domain the user already has elsewhere
26
+
27
+ - Keep it there: give the user the exact records to add (the CNAME / A from `add_service_domain`, the email records from `add_email_domain`), then `verify_service_domain` / `verify_email_domain`.
28
+ - Move it to Helipod: `transfer_domain` with the auth code (`dryRun` first). It stays `pending_transfer` for a few days; DNS keeps working at the old provider meanwhile.
29
+
30
+ ## Hand over
31
+
32
+ Tell the user, in one short list: the URL(s), where the database lives, the sending domain and the key name (never the key), what is billed and when (VPS period, domain renewal date, bundle or `walletDailyCost`), and that `auto-renew` is on for the domain and the VPS (or how to turn it off).
@@ -0,0 +1,36 @@
1
+ ---
2
+ name: helipod-reseller
3
+ description: Resell Helipod under your own brand (white label) - one project per customer, email keys and SMTP passwords per customer, domains registered or transferred in the customer's name, VPS and services billed to your wallet. Use when the user sells hosting, domains or email to their own customers through the Helipod API or MCP.
4
+ ---
5
+
6
+ # Reselling Helipod
7
+
8
+ Follow the `helipod` skill rules. The reseller's wallet pays for everything; set your own prices to customers outside Helipod. Use a whole-account Helipod API key with the products you resell (Email API, Domain API, VPS API, Container API, Database API). Projects need no product: any key can list them, a whole-account key creates them.
9
+
10
+ ## Onboard a customer
11
+
12
+ 1. `create_project` (name = the customer, `idempotencyKey` = your customer id) → keep the `projectId` with the customer in your own system.
13
+ 2. Everything for that customer goes in that project: services, a database, a VPS, email. Pass `projectId` to the email tools (a whole-account key needs it).
14
+
15
+ ## Email per customer
16
+
17
+ 1. `buy_email_plan` for the customer's project (FREE first, `dryRun`), `add_email_domain` with the customer's domain, the DNS records (`add_domain_email_records` when the domain is on Helipod, else give them to the customer), `verify_email_domain`.
18
+ 2. `create_email_key` with the customer's name → the key is shown ONCE: hand it to the customer (or store it encrypted on your side). It works for that project only, on the Email API and as the SMTP password (`smtp.helipod.io`, port 587 STARTTLS or 465, username `apikey`). It cannot make other keys.
19
+ 3. `list_email_keys`, `revoke_email_key` when a customer leaves or a key leaks. Allowance per project: 5 keys (Free/Starter), 10 (Pro), 50 (Business).
20
+ 4. Managing keys needs the Helipod API key itself: an email key or an AI-tool sign-in answers `403 KEY_NOT_ALLOWED`.
21
+
22
+ ## Domains in the customer's name
23
+
24
+ - New domain: `register_domain` with `registrant` = the customer's details (only details the customer gave you). `.id` family only (`.id`, `.my.id`, `.web.id`, `.biz.id`, `.ai.id`); other extensions use the reseller account's owner details (`400 REGISTRANT_NOT_SUPPORTED` if you send one).
25
+ - Transfer in: `transfer_domain` with the auth code and `registrant`: the owner is set when the transfer completes (`ownerPending: true` until then; it is checked again automatically).
26
+ - A domain already on the account: `set_domain_owner`. Free.
27
+ - Renewal is charged to your wallet: `set_domain_auto_renew` per domain to match what the customer paid for. The same for each customer project's email plan: `set_email_auto_renew` (off when the customer stops paying; `get_email_plan` / `email_status` show `autoRenew`), and `set_vps_auto_renew` per server. Sends over an email plan's quota are charged only with `set_email_payg` on (off by default): leave it off unless the customer pays for overage.
28
+
29
+ ## Servers and apps per customer
30
+
31
+ - VPS: `helipod-vps-deploy` inside the customer's project. Containers: `helipod-deploy` in the project's environment.
32
+ - Watch money: `get_wallet` before buying, `list_wallet_transactions` / `get_statement` to bill your customers; top up with `create_deposit` (give the payment link to the account owner, never pay it yourself).
33
+
34
+ ## Offboard
35
+
36
+ Revoke the customer's email keys, then delete or hand over in this order: services and VPS (confirm, data is lost), database, email plan, domains (transfer out at the registry), and finally `delete_project` (only empty projects; `confirm` = its exact name).
@@ -0,0 +1,39 @@
1
+ ---
2
+ name: helipod-vps-deploy
3
+ description: Run an application on a Helipod VPS - pick a ready app image (WordPress, n8n, EasyPanel, aaPanel, Dify, ...) or a plain OS, open the right ports, add an SSH key, point a domain at it and serve HTTPS. Use when the user wants their app on a server with root access instead of containers, or asks to host WordPress / n8n / a panel on a VPS.
4
+ ---
5
+
6
+ # Deploy on a Helipod VPS
7
+
8
+ Follow the `helipod` skill rules (price first, confirm destructive actions, never print passwords). Containers are usually simpler and cheaper for one web app (`helipod-deploy`); choose a VPS when the user needs root, a panel, long-running software they manage themselves, or asks for it.
9
+
10
+ ## 1. Pick the server
11
+
12
+ 1. `list_vps_plans` → plans and their `osTemplates`. App plans list ready images: `wordpress-…`, `woocommerce-…`, `n8n-…`, `easypanel-…`, `aapanel-…`, `1panel-…`, `dify-…`, `lamp-…`, `cloudreve-…`, `srs-…`, AI agents (`openclaw-…`, `opencode-…`, `hermes-…`). An app image is ready to use; a plain OS (`ubuntu-24.04`, `debian-…`) is for the user's own setup.
13
+ 2. Match the app's minimum RAM (WordPress 1 GB, n8n and Dify 2 GB, DeepSeek 4 GB): an image the plan cannot run is not listed.
14
+ 3. `list_projects` → the project. `create_vps` with `dryRun: true` → tell the user the price and the wallet after it, and wait for a yes.
15
+ 4. `create_vps` for real with an `idempotencyKey` and a strong generated password (do not print it back unless asked). Poll `get_vps` every 10 s until `running`.
16
+
17
+ ## 2. Reach it
18
+
19
+ - SSH key instead of the password: ask for the user's PUBLIC key (`~/.ssh/id_ed25519.pub`), `add_vps_ssh_key`. Never ask for or accept a private key.
20
+ - `get_vps` → `providerCode` and `access`:
21
+ - `providerCode` 2 or 3 (own public IP): ports are opened with `list_firewall_rules` / `add_firewall_rules`. App images open their own ports at create; check before adding.
22
+ - `providerCode` 1 (shared address): the server is reached through port forwards. `list_vps_ports` shows the allowed `portRange`; `add_vps_port` with `protocol: "tcp"` for a raw port, or `protocol: "https"` (port 443) with a `domain` and the app's `internalPort` to route a domain to the app (point the domain's DNS at the server's IP first).
23
+ - Login details: `get_vps_credentials` (Linux `ssh root@<host> -p <port>`; Windows RDP).
24
+
25
+ ## 3. Domain and HTTPS
26
+
27
+ 1. A domain on Helipod: `list_my_domains` → `update_dns_record` with an `A` record (`@` and/or `www`) to the server's IP. Elsewhere: tell the user the exact `A` record to add at their DNS provider.
28
+ 2. App images with a panel (EasyPanel, aaPanel, 1Panel) issue certificates from the panel once DNS points at the server. On a plain OS, suggest Caddy (automatic HTTPS): `apt install caddy`, then a Caddyfile `example.com { reverse_proxy localhost:3000 }`.
29
+ 3. Shared-address servers (`providerCode` 1): `add_vps_port` with `protocol: "https"`, the `domain` and the app's `internalPort` serves HTTPS for it.
30
+
31
+ ## 4. First login of an app image
32
+
33
+ The dashboard's VPS page has an **Application** tab with the app's URL, the ports it needs and how to get its admin login; send the user there. Typical first steps: WordPress and n8n open on `http://<ip>` (n8n on port 5678) and ask to create the admin user; EasyPanel opens on port 3000 and asks to create the admin user; aaPanel and 1Panel print their login with a command shown in that tab.
34
+
35
+ ## 5. Keep it safe
36
+
37
+ - `create_snapshot` before risky changes; `run_vps_backup` / `activate_vps_backup` (paid, price first) for daily backups.
38
+ - Never remove the SSH rule (port 22) or the user's only SSH key without asking.
39
+ - Email from the app: SMTP with an email key (`helipod-email`), not a mail server on the VPS.