helipod-mcp 0.5.13 → 0.5.16
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 +2 -2
- package/dist/http.js +92 -25
- package/dist/index.js +92 -25
- package/dist/stdio.js +92 -25
- package/package.json +1 -1
- package/skills/helipod/SKILL.md +7 -4
- package/skills/helipod-deploy/SKILL.md +5 -1
- package/skills/helipod-domains/SKILL.md +1 -1
- package/skills/helipod-email/SKILL.md +8 -2
- package/skills/helipod-launch/SKILL.md +32 -0
- package/skills/helipod-reseller/SKILL.md +36 -0
- package/skills/helipod-vps-deploy/SKILL.md +39 -0
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`):
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
@@ -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:
|
|
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: {
|
|
36891
|
-
|
|
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.
|
|
38561
|
+
var VERSION = "0.5.16";
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
@@ -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:
|
|
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: {
|
|
36036
|
-
|
|
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.
|
|
37706
|
+
var VERSION = "0.5.16";
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
@@ -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:
|
|
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: {
|
|
36143
|
-
|
|
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.
|
|
37813
|
+
var VERSION = "0.5.16";
|
|
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.
|
|
4
|
+
"version": "0.5.16",
|
|
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": {
|
package/skills/helipod/SKILL.md
CHANGED
|
@@ -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
|
|
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
|
|
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`)
|
|
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.
|