helipod-mcp 0.5.5 → 0.5.7
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 +1 -1
- package/dist/http.js +104 -8
- package/dist/index.js +104 -8
- package/dist/stdio.js +104 -8
- package/package.json +1 -1
- package/skills/helipod-ai-gateway/SKILL.md +13 -1
- package/skills/helipod-vps/SKILL.md +13 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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`):
|
|
5
|
+
- **MCP server** (`helipod-mcp`): 229 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
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).
|
|
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**
|
package/dist/http.js
CHANGED
|
@@ -36219,6 +36219,7 @@ var password = exports_external.string().min(12).max(30).describe("12-30 charact
|
|
|
36219
36219
|
var period = exports_external.enum(["monthly", "3month", "6month", "yearly"]);
|
|
36220
36220
|
var pointId = exports_external.string().describe("Restore point id (from list_vps_restore_points)");
|
|
36221
36221
|
var backupPath = exports_external.string().max(1024).describe("Absolute path inside the backup, e.g. /etc/nginx or /var/www/index.html");
|
|
36222
|
+
var migrationId = exports_external.string().describe("Migration id (from list_vps_migrations)");
|
|
36222
36223
|
var vpsTools = [
|
|
36223
36224
|
defineTool({
|
|
36224
36225
|
name: "list_vps_plans",
|
|
@@ -36598,6 +36599,84 @@ var vpsTools = [
|
|
|
36598
36599
|
description: "Status of a prepared backup download; when ready, a short-lived url for the user to open (it expires at expiresAt). The link is for the user to download in a browser.",
|
|
36599
36600
|
input: { vpsId, downloadId: exports_external.string() },
|
|
36600
36601
|
run: (a, api2) => api2.api("GET", `/vps/${a.vpsId}/backup/downloads/${a.downloadId}`)
|
|
36602
|
+
}),
|
|
36603
|
+
defineTool({
|
|
36604
|
+
name: "list_vps_migrations",
|
|
36605
|
+
title: "List VPS migrations",
|
|
36606
|
+
kind: "read",
|
|
36607
|
+
description: "Migrations onto this VPS, newest first: imports from servers outside Helipod (kind=import) and copies from another VPS's restore point (kind=copy). Each has status (waiting, uploading, inspecting, uploaded, awaiting_command, restoring, restored, failed, expired, cancelled), the uploaded server's OS and size, who ran the command (fetchedIp), progress and pause state of a running job, and the result.",
|
|
36608
|
+
input: { vpsId },
|
|
36609
|
+
run: (a, api2) => api2.api("GET", `/vps/${a.vpsId}/migrations`)
|
|
36610
|
+
}),
|
|
36611
|
+
defineTool({
|
|
36612
|
+
name: "get_vps_migration",
|
|
36613
|
+
title: "Get a VPS migration",
|
|
36614
|
+
kind: "read",
|
|
36615
|
+
description: "One migration onto this VPS (see list_vps_migrations). While an upload or restore runs, result.progress has percent (of data read), bytesDone/bytesTotal, sentBytes and sendRate; result.stored the bytes confirmed in storage; result.control the pause state (want vs the job's reported state). Poll it every 15-30 s while it runs.",
|
|
36616
|
+
input: { vpsId, migrationId },
|
|
36617
|
+
run: (a, api2) => api2.api("GET", `/vps/${a.vpsId}/migrations/${a.migrationId}`)
|
|
36618
|
+
}),
|
|
36619
|
+
defineTool({
|
|
36620
|
+
name: "create_vps_import",
|
|
36621
|
+
title: "Start a server import (Migrate to Helipod)",
|
|
36622
|
+
kind: "write",
|
|
36623
|
+
description: "Start moving a server from OUTSIDE Helipod (any Linux server with root) onto this running VPS. Free; one import per VPS, at most 3 per account, kept 7 days, the upload is capped at this VPS's disk. Returns a one-time command (curl ... | sudo bash) that works ONCE and expires in 1 hour: give it to the user to run as root on the OLD server; never run it yourself and do not store it. Pass sourceIp (the old server's public IP, e.g. from curl -4 ifconfig.me there) so the command only works from that server; recommended. Then follow it with get_vps_migration: waiting → uploading → inspecting → uploaded.",
|
|
36624
|
+
input: { vpsId, sourceIp: exports_external.string().max(64).optional().describe("The old server's public IP: the command then only works from it (recommended)") },
|
|
36625
|
+
run: ({ vpsId: id2, sourceIp }, api2) => api2.api("POST", `/vps/${id2}/migrations/import`, { body: { sourceIp: sourceIp ?? null } })
|
|
36626
|
+
}),
|
|
36627
|
+
defineTool({
|
|
36628
|
+
name: "new_vps_migration_command",
|
|
36629
|
+
title: "Get a new migration command",
|
|
36630
|
+
kind: "write",
|
|
36631
|
+
description: "A fresh one-time command for an import (status waiting, uploading or failed before an upload finished) or for a restore that waits for its command (awaiting_command). The previous command stops working, and a job still running with it is told to stop. Shown once: give it to the user to run as root on the right server.",
|
|
36632
|
+
input: { vpsId, migrationId },
|
|
36633
|
+
run: (a, api2) => api2.api("POST", `/vps/${a.vpsId}/migrations/${a.migrationId}/command`)
|
|
36634
|
+
}),
|
|
36635
|
+
defineTool({
|
|
36636
|
+
name: "restore_vps_import",
|
|
36637
|
+
title: "Restore an import onto the VPS",
|
|
36638
|
+
kind: "destructive",
|
|
36639
|
+
description: "Restore an uploaded import (status uploaded, restored, or failed after an upload) onto its VPS. mode=files restores 1-50 paths (e.g. /home, /var/www, /etc/nginx, /var/backups/helipod-db for database dumps): target=folder (default) into /root/restore-<date>, target=inplace OVERWRITES files at the same paths. mode=full turns the whole VPS into a copy of the old server: EVERYTHING on the VPS is replaced, only allowed when the VPS runs the same OS as the upload (sourceOs) and is of the full-VM kind. The first restore needs confirmSource=true: before setting it, show the user fetchedIp (and the uploaded server's hostname) from get_vps_migration and ask whether that is their old server; if not, delete the import instead. Runs in the background: follow it with get_vps_migration.",
|
|
36640
|
+
input: {
|
|
36641
|
+
vpsId,
|
|
36642
|
+
migrationId,
|
|
36643
|
+
mode: exports_external.enum(["files", "full"]),
|
|
36644
|
+
paths: exports_external.array(backupPath).min(1).max(50).optional().describe("mode=files: files or folders to restore"),
|
|
36645
|
+
target: exports_external.enum(["folder", "inplace"]).optional().describe("mode=files: folder (default, safe) or inplace (overwrite)"),
|
|
36646
|
+
confirmSource: exports_external.boolean().optional().describe("true only after the user confirmed fetchedIp is their old server (needed once)")
|
|
36647
|
+
},
|
|
36648
|
+
run: (a, api2) => api2.api("POST", `/vps/${a.vpsId}/migrations/${a.migrationId}/restore`, { body: { mode: a.mode, paths: a.paths, target: a.target, confirmSource: a.confirmSource } })
|
|
36649
|
+
}),
|
|
36650
|
+
defineTool({
|
|
36651
|
+
name: "control_vps_migration",
|
|
36652
|
+
title: "Pause, resume or stop a migration job",
|
|
36653
|
+
kind: "write",
|
|
36654
|
+
description: "Pause, resume or stop an upload or restore that runs from a one-time command on a server (it must have checked in: result.control is set). The job follows within about 5 seconds; result.control.state shows when it did. Stop is final: the job ends as failed (stopped); a new command (new_vps_migration_command) starts the upload again and reuses what was already uploaded where possible. The user can do the same on that server with sudo helipod-migrate pause | resume | stop.",
|
|
36655
|
+
input: { vpsId, migrationId, action: exports_external.enum(["pause", "resume", "stop"]) },
|
|
36656
|
+
run: (a, api2) => api2.api("POST", `/vps/${a.vpsId}/migrations/${a.migrationId}/control`, { body: { action: a.action } })
|
|
36657
|
+
}),
|
|
36658
|
+
defineTool({
|
|
36659
|
+
name: "delete_vps_migration",
|
|
36660
|
+
title: "Delete a VPS migration",
|
|
36661
|
+
kind: "destructive",
|
|
36662
|
+
description: "Delete a migration; an import's uploaded files are deleted from storage and cannot be restored any more. A running upload is told to stop. Not while a restore or the upload check runs.",
|
|
36663
|
+
input: { vpsId, migrationId },
|
|
36664
|
+
run: (a, api2) => api2.api("DELETE", `/vps/${a.vpsId}/migrations/${a.migrationId}`)
|
|
36665
|
+
}),
|
|
36666
|
+
defineTool({
|
|
36667
|
+
name: "copy_vps_backup",
|
|
36668
|
+
title: "Copy a restore point to another VPS",
|
|
36669
|
+
kind: "destructive",
|
|
36670
|
+
description: "Copy a restore point of one VPS (vpsId, pointId from list_vps_restore_points) onto ANOTHER VPS of the same account (targetVpsId; to bring the same VPS back use restore_vps_backup). Free. mode=files copies 1-50 paths onto any running VPS: target=folder (default) into /root/restore-<date>, target=inplace OVERWRITES files at the same paths. mode=full turns the target into a full copy: everything on it is replaced; only between two VPS of the same kind running the same OS and version. Runs in the background: follow it with list_vps_migrations on the TARGET VPS. If the result has a command, the target's management agent was unreachable: give the user that one-time command to run as root on the target.",
|
|
36671
|
+
input: {
|
|
36672
|
+
vpsId,
|
|
36673
|
+
pointId,
|
|
36674
|
+
targetVpsId: exports_external.string().describe("The VPS to copy onto (id from list_vps), not vpsId"),
|
|
36675
|
+
mode: exports_external.enum(["files", "full"]),
|
|
36676
|
+
paths: exports_external.array(backupPath).min(1).max(50).optional().describe("mode=files: files or folders to copy"),
|
|
36677
|
+
target: exports_external.enum(["folder", "inplace"]).optional().describe("mode=files: folder (default, safe) or inplace (overwrite)")
|
|
36678
|
+
},
|
|
36679
|
+
run: (a, api2) => api2.api("POST", `/vps/${a.vpsId}/backup/points/${a.pointId}/copy`, { body: { targetVpsId: a.targetVpsId, mode: a.mode, paths: a.paths, target: a.target } })
|
|
36601
36680
|
})
|
|
36602
36681
|
];
|
|
36603
36682
|
|
|
@@ -36607,9 +36686,9 @@ var hostId = exports_external.number().int().positive().describe("Child nameserv
|
|
|
36607
36686
|
var dryRun2 = exports_external.boolean().optional().describe("true = return the exact price (IDR, VAT included) and the wallet balance after it; nothing is charged or registered");
|
|
36608
36687
|
var DRY = "dryRun=true returns the price and walletBalanceAfter without charging or changing anything.";
|
|
36609
36688
|
var record3 = exports_external.object({
|
|
36610
|
-
name: exports_external.string().describe('"@" for the domain itself, or
|
|
36689
|
+
name: exports_external.string().describe('"@" for the domain itself, or the part before the domain, e.g. "www" or "_dmarc.mail". A full name like "www.example.com" is accepted and stored as "www"'),
|
|
36611
36690
|
type: exports_external.enum(["A", "AAAA", "MX", "MXE", "CNAME", "SPF", "TXT"]),
|
|
36612
|
-
content: exports_external.string(),
|
|
36691
|
+
content: exports_external.string().describe('TXT without surrounding quotes (they are added by the DNS server); MX as "10 mail.example.com"'),
|
|
36613
36692
|
ttl: exports_external.number().int().optional()
|
|
36614
36693
|
});
|
|
36615
36694
|
var domainTools = [
|
|
@@ -36710,7 +36789,7 @@ var domainTools = [
|
|
|
36710
36789
|
name: "list_dns_records",
|
|
36711
36790
|
title: "List DNS records",
|
|
36712
36791
|
kind: "read",
|
|
36713
|
-
description:
|
|
36792
|
+
description: 'DNS records of a registered domain. prefix is the name relative to the domain ("@" for the domain itself); TXT content is shown with the quotes the DNS server adds. A domain with no records is not served at all (its nameservers answer REFUSED), so add at least one record for it to resolve.',
|
|
36714
36793
|
input: { domainId },
|
|
36715
36794
|
run: (a, api2) => api2.api("GET", `/domains/${a.domainId}/dns`)
|
|
36716
36795
|
}),
|
|
@@ -36718,7 +36797,7 @@ var domainTools = [
|
|
|
36718
36797
|
name: "set_dns_records",
|
|
36719
36798
|
title: "Replace DNS records",
|
|
36720
36799
|
kind: "destructive",
|
|
36721
|
-
description: "Replace ALL DNS records of the domain with this list. Records not included are deleted
|
|
36800
|
+
description: "Replace ALL DNS records of the domain with this list. Records not included are deleted: read list_dns_records first and send every record that should stay (prefix as name, content as listed; quotes around TXT are removed for you).",
|
|
36722
36801
|
input: { domainId, records: exports_external.array(record3).min(1) },
|
|
36723
36802
|
run: (a, api2) => api2.api("POST", `/domains/${a.domainId}/dns`, { body: { records: a.records } })
|
|
36724
36803
|
}),
|
|
@@ -36772,6 +36851,22 @@ var domainTools = [
|
|
|
36772
36851
|
input: { domainId, ...record3.shape },
|
|
36773
36852
|
run: (a, api2) => api2.api("PUT", `/domains/${a.domainId}/dns`, { body: { name: a.name, type: a.type, content: a.content, ttl: a.ttl } })
|
|
36774
36853
|
}),
|
|
36854
|
+
defineTool({
|
|
36855
|
+
name: "list_domain_email_records",
|
|
36856
|
+
title: "Email records for a domain",
|
|
36857
|
+
kind: "read",
|
|
36858
|
+
description: "Helipod Email sending domains inside this registered domain (e.g. mail.example.com), each DNS record they need and whether the domain has it: present, missing, or conflict (with what to change).",
|
|
36859
|
+
input: { domainId },
|
|
36860
|
+
run: (a, api2) => api2.api("GET", `/domains/${a.domainId}/email-records`)
|
|
36861
|
+
}),
|
|
36862
|
+
defineTool({
|
|
36863
|
+
name: "add_domain_email_records",
|
|
36864
|
+
title: "Add email records to a domain",
|
|
36865
|
+
kind: "write",
|
|
36866
|
+
description: "Add the DNS records a Helipod Email sending domain still needs to this registered domain (Helipod DNS only), then start its verification. Records that would clash (a CNAME on a name in use, a second SPF) are not changed and are listed in conflicts. emailDomainId comes from list_domain_email_records.",
|
|
36867
|
+
input: { domainId, emailDomainId: exports_external.string().uuid() },
|
|
36868
|
+
run: (a, api2) => api2.api("POST", `/domains/${a.domainId}/email-records`, { body: { emailDomainId: a.emailDomainId } })
|
|
36869
|
+
}),
|
|
36775
36870
|
defineTool({
|
|
36776
36871
|
name: "list_child_nameservers",
|
|
36777
36872
|
title: "List child nameservers",
|
|
@@ -36997,7 +37092,7 @@ var emailTools = [
|
|
|
36997
37092
|
name: "verify_email_domain",
|
|
36998
37093
|
title: "Verify sending domain",
|
|
36999
37094
|
kind: "write",
|
|
37000
|
-
description: "Check a sending domain's DNS records again.",
|
|
37095
|
+
description: "Check a sending domain's DNS records again now. Pending domains are also re-checked automatically every 30 minutes for 3 days.",
|
|
37001
37096
|
input: { projectId, domainId: exports_external.string() },
|
|
37002
37097
|
run: (a, api2) => api2.email("POST", `/domains/${a.domainId}/verify`, { project: a.projectId })
|
|
37003
37098
|
}),
|
|
@@ -37045,7 +37140,7 @@ var emailTools = [
|
|
|
37045
37140
|
name: "get_email_domain",
|
|
37046
37141
|
title: "Get sending domain",
|
|
37047
37142
|
kind: "read",
|
|
37048
|
-
description: "One sending domain: verification status and the DNS records to add.",
|
|
37143
|
+
description: "One sending domain: verification status and the DNS records to add. ownershipVerified is true once the ownership TXT is confirmed; status turns verified only when DKIM passes too. When the domain is registered at Helipod, add_domain_email_records adds the records in one call.",
|
|
37049
37144
|
input: { projectId, domainId: exports_external.string() },
|
|
37050
37145
|
run: (a, api2) => api2.email("GET", `/domains/${a.domainId}`, { project: a.projectId })
|
|
37051
37146
|
}),
|
|
@@ -37981,14 +38076,15 @@ var generalTools = [
|
|
|
37981
38076
|
];
|
|
37982
38077
|
|
|
37983
38078
|
// src/server.ts
|
|
37984
|
-
var VERSION = "0.5.
|
|
38079
|
+
var VERSION = "0.5.7";
|
|
37985
38080
|
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.
|
|
37986
38081
|
- 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) cover the AI gateway.
|
|
37987
38082
|
- 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): 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.
|
|
37988
|
-
- Destructive tools (delete, terminate, reinstall, rollback, restore a backup, 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.
|
|
38083
|
+
- 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.
|
|
37989
38084
|
- 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.
|
|
37990
38085
|
- 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.
|
|
37991
38086
|
- Databases: get_database_connection returns a password; put it in a secret variable (DATABASE_URL) instead of code or chat.
|
|
38087
|
+
- 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.
|
|
37992
38088
|
- Deploys and VPS actions run in the background: poll get_deployment / get_vps until they settle; on a failed deploy read get_deployment_logs.
|
|
37993
38089
|
- Never print secrets (passwords, secret variable values, secret files) unless the user asks for them. Domain owner details are personal data: use only what the user gave.
|
|
37994
38090
|
- Errors come back as { status, code, error }: 401/403 means the key lacks access (keys choose products and projects in Settings → API keys; wallet, invoices, tickets and AI money need a whole-account key: with a sign-in, reconnect and choose Whole account), 402 means plan limit or low balance.
|
package/dist/index.js
CHANGED
|
@@ -35364,6 +35364,7 @@ var password = exports_external.string().min(12).max(30).describe("12-30 charact
|
|
|
35364
35364
|
var period = exports_external.enum(["monthly", "3month", "6month", "yearly"]);
|
|
35365
35365
|
var pointId = exports_external.string().describe("Restore point id (from list_vps_restore_points)");
|
|
35366
35366
|
var backupPath = exports_external.string().max(1024).describe("Absolute path inside the backup, e.g. /etc/nginx or /var/www/index.html");
|
|
35367
|
+
var migrationId = exports_external.string().describe("Migration id (from list_vps_migrations)");
|
|
35367
35368
|
var vpsTools = [
|
|
35368
35369
|
defineTool({
|
|
35369
35370
|
name: "list_vps_plans",
|
|
@@ -35743,6 +35744,84 @@ var vpsTools = [
|
|
|
35743
35744
|
description: "Status of a prepared backup download; when ready, a short-lived url for the user to open (it expires at expiresAt). The link is for the user to download in a browser.",
|
|
35744
35745
|
input: { vpsId, downloadId: exports_external.string() },
|
|
35745
35746
|
run: (a, api2) => api2.api("GET", `/vps/${a.vpsId}/backup/downloads/${a.downloadId}`)
|
|
35747
|
+
}),
|
|
35748
|
+
defineTool({
|
|
35749
|
+
name: "list_vps_migrations",
|
|
35750
|
+
title: "List VPS migrations",
|
|
35751
|
+
kind: "read",
|
|
35752
|
+
description: "Migrations onto this VPS, newest first: imports from servers outside Helipod (kind=import) and copies from another VPS's restore point (kind=copy). Each has status (waiting, uploading, inspecting, uploaded, awaiting_command, restoring, restored, failed, expired, cancelled), the uploaded server's OS and size, who ran the command (fetchedIp), progress and pause state of a running job, and the result.",
|
|
35753
|
+
input: { vpsId },
|
|
35754
|
+
run: (a, api2) => api2.api("GET", `/vps/${a.vpsId}/migrations`)
|
|
35755
|
+
}),
|
|
35756
|
+
defineTool({
|
|
35757
|
+
name: "get_vps_migration",
|
|
35758
|
+
title: "Get a VPS migration",
|
|
35759
|
+
kind: "read",
|
|
35760
|
+
description: "One migration onto this VPS (see list_vps_migrations). While an upload or restore runs, result.progress has percent (of data read), bytesDone/bytesTotal, sentBytes and sendRate; result.stored the bytes confirmed in storage; result.control the pause state (want vs the job's reported state). Poll it every 15-30 s while it runs.",
|
|
35761
|
+
input: { vpsId, migrationId },
|
|
35762
|
+
run: (a, api2) => api2.api("GET", `/vps/${a.vpsId}/migrations/${a.migrationId}`)
|
|
35763
|
+
}),
|
|
35764
|
+
defineTool({
|
|
35765
|
+
name: "create_vps_import",
|
|
35766
|
+
title: "Start a server import (Migrate to Helipod)",
|
|
35767
|
+
kind: "write",
|
|
35768
|
+
description: "Start moving a server from OUTSIDE Helipod (any Linux server with root) onto this running VPS. Free; one import per VPS, at most 3 per account, kept 7 days, the upload is capped at this VPS's disk. Returns a one-time command (curl ... | sudo bash) that works ONCE and expires in 1 hour: give it to the user to run as root on the OLD server; never run it yourself and do not store it. Pass sourceIp (the old server's public IP, e.g. from curl -4 ifconfig.me there) so the command only works from that server; recommended. Then follow it with get_vps_migration: waiting → uploading → inspecting → uploaded.",
|
|
35769
|
+
input: { vpsId, sourceIp: exports_external.string().max(64).optional().describe("The old server's public IP: the command then only works from it (recommended)") },
|
|
35770
|
+
run: ({ vpsId: id2, sourceIp }, api2) => api2.api("POST", `/vps/${id2}/migrations/import`, { body: { sourceIp: sourceIp ?? null } })
|
|
35771
|
+
}),
|
|
35772
|
+
defineTool({
|
|
35773
|
+
name: "new_vps_migration_command",
|
|
35774
|
+
title: "Get a new migration command",
|
|
35775
|
+
kind: "write",
|
|
35776
|
+
description: "A fresh one-time command for an import (status waiting, uploading or failed before an upload finished) or for a restore that waits for its command (awaiting_command). The previous command stops working, and a job still running with it is told to stop. Shown once: give it to the user to run as root on the right server.",
|
|
35777
|
+
input: { vpsId, migrationId },
|
|
35778
|
+
run: (a, api2) => api2.api("POST", `/vps/${a.vpsId}/migrations/${a.migrationId}/command`)
|
|
35779
|
+
}),
|
|
35780
|
+
defineTool({
|
|
35781
|
+
name: "restore_vps_import",
|
|
35782
|
+
title: "Restore an import onto the VPS",
|
|
35783
|
+
kind: "destructive",
|
|
35784
|
+
description: "Restore an uploaded import (status uploaded, restored, or failed after an upload) onto its VPS. mode=files restores 1-50 paths (e.g. /home, /var/www, /etc/nginx, /var/backups/helipod-db for database dumps): target=folder (default) into /root/restore-<date>, target=inplace OVERWRITES files at the same paths. mode=full turns the whole VPS into a copy of the old server: EVERYTHING on the VPS is replaced, only allowed when the VPS runs the same OS as the upload (sourceOs) and is of the full-VM kind. The first restore needs confirmSource=true: before setting it, show the user fetchedIp (and the uploaded server's hostname) from get_vps_migration and ask whether that is their old server; if not, delete the import instead. Runs in the background: follow it with get_vps_migration.",
|
|
35785
|
+
input: {
|
|
35786
|
+
vpsId,
|
|
35787
|
+
migrationId,
|
|
35788
|
+
mode: exports_external.enum(["files", "full"]),
|
|
35789
|
+
paths: exports_external.array(backupPath).min(1).max(50).optional().describe("mode=files: files or folders to restore"),
|
|
35790
|
+
target: exports_external.enum(["folder", "inplace"]).optional().describe("mode=files: folder (default, safe) or inplace (overwrite)"),
|
|
35791
|
+
confirmSource: exports_external.boolean().optional().describe("true only after the user confirmed fetchedIp is their old server (needed once)")
|
|
35792
|
+
},
|
|
35793
|
+
run: (a, api2) => api2.api("POST", `/vps/${a.vpsId}/migrations/${a.migrationId}/restore`, { body: { mode: a.mode, paths: a.paths, target: a.target, confirmSource: a.confirmSource } })
|
|
35794
|
+
}),
|
|
35795
|
+
defineTool({
|
|
35796
|
+
name: "control_vps_migration",
|
|
35797
|
+
title: "Pause, resume or stop a migration job",
|
|
35798
|
+
kind: "write",
|
|
35799
|
+
description: "Pause, resume or stop an upload or restore that runs from a one-time command on a server (it must have checked in: result.control is set). The job follows within about 5 seconds; result.control.state shows when it did. Stop is final: the job ends as failed (stopped); a new command (new_vps_migration_command) starts the upload again and reuses what was already uploaded where possible. The user can do the same on that server with sudo helipod-migrate pause | resume | stop.",
|
|
35800
|
+
input: { vpsId, migrationId, action: exports_external.enum(["pause", "resume", "stop"]) },
|
|
35801
|
+
run: (a, api2) => api2.api("POST", `/vps/${a.vpsId}/migrations/${a.migrationId}/control`, { body: { action: a.action } })
|
|
35802
|
+
}),
|
|
35803
|
+
defineTool({
|
|
35804
|
+
name: "delete_vps_migration",
|
|
35805
|
+
title: "Delete a VPS migration",
|
|
35806
|
+
kind: "destructive",
|
|
35807
|
+
description: "Delete a migration; an import's uploaded files are deleted from storage and cannot be restored any more. A running upload is told to stop. Not while a restore or the upload check runs.",
|
|
35808
|
+
input: { vpsId, migrationId },
|
|
35809
|
+
run: (a, api2) => api2.api("DELETE", `/vps/${a.vpsId}/migrations/${a.migrationId}`)
|
|
35810
|
+
}),
|
|
35811
|
+
defineTool({
|
|
35812
|
+
name: "copy_vps_backup",
|
|
35813
|
+
title: "Copy a restore point to another VPS",
|
|
35814
|
+
kind: "destructive",
|
|
35815
|
+
description: "Copy a restore point of one VPS (vpsId, pointId from list_vps_restore_points) onto ANOTHER VPS of the same account (targetVpsId; to bring the same VPS back use restore_vps_backup). Free. mode=files copies 1-50 paths onto any running VPS: target=folder (default) into /root/restore-<date>, target=inplace OVERWRITES files at the same paths. mode=full turns the target into a full copy: everything on it is replaced; only between two VPS of the same kind running the same OS and version. Runs in the background: follow it with list_vps_migrations on the TARGET VPS. If the result has a command, the target's management agent was unreachable: give the user that one-time command to run as root on the target.",
|
|
35816
|
+
input: {
|
|
35817
|
+
vpsId,
|
|
35818
|
+
pointId,
|
|
35819
|
+
targetVpsId: exports_external.string().describe("The VPS to copy onto (id from list_vps), not vpsId"),
|
|
35820
|
+
mode: exports_external.enum(["files", "full"]),
|
|
35821
|
+
paths: exports_external.array(backupPath).min(1).max(50).optional().describe("mode=files: files or folders to copy"),
|
|
35822
|
+
target: exports_external.enum(["folder", "inplace"]).optional().describe("mode=files: folder (default, safe) or inplace (overwrite)")
|
|
35823
|
+
},
|
|
35824
|
+
run: (a, api2) => api2.api("POST", `/vps/${a.vpsId}/backup/points/${a.pointId}/copy`, { body: { targetVpsId: a.targetVpsId, mode: a.mode, paths: a.paths, target: a.target } })
|
|
35746
35825
|
})
|
|
35747
35826
|
];
|
|
35748
35827
|
|
|
@@ -35752,9 +35831,9 @@ var hostId = exports_external.number().int().positive().describe("Child nameserv
|
|
|
35752
35831
|
var dryRun2 = exports_external.boolean().optional().describe("true = return the exact price (IDR, VAT included) and the wallet balance after it; nothing is charged or registered");
|
|
35753
35832
|
var DRY = "dryRun=true returns the price and walletBalanceAfter without charging or changing anything.";
|
|
35754
35833
|
var record3 = exports_external.object({
|
|
35755
|
-
name: exports_external.string().describe('"@" for the domain itself, or
|
|
35834
|
+
name: exports_external.string().describe('"@" for the domain itself, or the part before the domain, e.g. "www" or "_dmarc.mail". A full name like "www.example.com" is accepted and stored as "www"'),
|
|
35756
35835
|
type: exports_external.enum(["A", "AAAA", "MX", "MXE", "CNAME", "SPF", "TXT"]),
|
|
35757
|
-
content: exports_external.string(),
|
|
35836
|
+
content: exports_external.string().describe('TXT without surrounding quotes (they are added by the DNS server); MX as "10 mail.example.com"'),
|
|
35758
35837
|
ttl: exports_external.number().int().optional()
|
|
35759
35838
|
});
|
|
35760
35839
|
var domainTools = [
|
|
@@ -35855,7 +35934,7 @@ var domainTools = [
|
|
|
35855
35934
|
name: "list_dns_records",
|
|
35856
35935
|
title: "List DNS records",
|
|
35857
35936
|
kind: "read",
|
|
35858
|
-
description:
|
|
35937
|
+
description: 'DNS records of a registered domain. prefix is the name relative to the domain ("@" for the domain itself); TXT content is shown with the quotes the DNS server adds. A domain with no records is not served at all (its nameservers answer REFUSED), so add at least one record for it to resolve.',
|
|
35859
35938
|
input: { domainId },
|
|
35860
35939
|
run: (a, api2) => api2.api("GET", `/domains/${a.domainId}/dns`)
|
|
35861
35940
|
}),
|
|
@@ -35863,7 +35942,7 @@ var domainTools = [
|
|
|
35863
35942
|
name: "set_dns_records",
|
|
35864
35943
|
title: "Replace DNS records",
|
|
35865
35944
|
kind: "destructive",
|
|
35866
|
-
description: "Replace ALL DNS records of the domain with this list. Records not included are deleted
|
|
35945
|
+
description: "Replace ALL DNS records of the domain with this list. Records not included are deleted: read list_dns_records first and send every record that should stay (prefix as name, content as listed; quotes around TXT are removed for you).",
|
|
35867
35946
|
input: { domainId, records: exports_external.array(record3).min(1) },
|
|
35868
35947
|
run: (a, api2) => api2.api("POST", `/domains/${a.domainId}/dns`, { body: { records: a.records } })
|
|
35869
35948
|
}),
|
|
@@ -35917,6 +35996,22 @@ var domainTools = [
|
|
|
35917
35996
|
input: { domainId, ...record3.shape },
|
|
35918
35997
|
run: (a, api2) => api2.api("PUT", `/domains/${a.domainId}/dns`, { body: { name: a.name, type: a.type, content: a.content, ttl: a.ttl } })
|
|
35919
35998
|
}),
|
|
35999
|
+
defineTool({
|
|
36000
|
+
name: "list_domain_email_records",
|
|
36001
|
+
title: "Email records for a domain",
|
|
36002
|
+
kind: "read",
|
|
36003
|
+
description: "Helipod Email sending domains inside this registered domain (e.g. mail.example.com), each DNS record they need and whether the domain has it: present, missing, or conflict (with what to change).",
|
|
36004
|
+
input: { domainId },
|
|
36005
|
+
run: (a, api2) => api2.api("GET", `/domains/${a.domainId}/email-records`)
|
|
36006
|
+
}),
|
|
36007
|
+
defineTool({
|
|
36008
|
+
name: "add_domain_email_records",
|
|
36009
|
+
title: "Add email records to a domain",
|
|
36010
|
+
kind: "write",
|
|
36011
|
+
description: "Add the DNS records a Helipod Email sending domain still needs to this registered domain (Helipod DNS only), then start its verification. Records that would clash (a CNAME on a name in use, a second SPF) are not changed and are listed in conflicts. emailDomainId comes from list_domain_email_records.",
|
|
36012
|
+
input: { domainId, emailDomainId: exports_external.string().uuid() },
|
|
36013
|
+
run: (a, api2) => api2.api("POST", `/domains/${a.domainId}/email-records`, { body: { emailDomainId: a.emailDomainId } })
|
|
36014
|
+
}),
|
|
35920
36015
|
defineTool({
|
|
35921
36016
|
name: "list_child_nameservers",
|
|
35922
36017
|
title: "List child nameservers",
|
|
@@ -36142,7 +36237,7 @@ var emailTools = [
|
|
|
36142
36237
|
name: "verify_email_domain",
|
|
36143
36238
|
title: "Verify sending domain",
|
|
36144
36239
|
kind: "write",
|
|
36145
|
-
description: "Check a sending domain's DNS records again.",
|
|
36240
|
+
description: "Check a sending domain's DNS records again now. Pending domains are also re-checked automatically every 30 minutes for 3 days.",
|
|
36146
36241
|
input: { projectId, domainId: exports_external.string() },
|
|
36147
36242
|
run: (a, api2) => api2.email("POST", `/domains/${a.domainId}/verify`, { project: a.projectId })
|
|
36148
36243
|
}),
|
|
@@ -36190,7 +36285,7 @@ var emailTools = [
|
|
|
36190
36285
|
name: "get_email_domain",
|
|
36191
36286
|
title: "Get sending domain",
|
|
36192
36287
|
kind: "read",
|
|
36193
|
-
description: "One sending domain: verification status and the DNS records to add.",
|
|
36288
|
+
description: "One sending domain: verification status and the DNS records to add. ownershipVerified is true once the ownership TXT is confirmed; status turns verified only when DKIM passes too. When the domain is registered at Helipod, add_domain_email_records adds the records in one call.",
|
|
36194
36289
|
input: { projectId, domainId: exports_external.string() },
|
|
36195
36290
|
run: (a, api2) => api2.email("GET", `/domains/${a.domainId}`, { project: a.projectId })
|
|
36196
36291
|
}),
|
|
@@ -37126,14 +37221,15 @@ var generalTools = [
|
|
|
37126
37221
|
];
|
|
37127
37222
|
|
|
37128
37223
|
// src/server.ts
|
|
37129
|
-
var VERSION = "0.5.
|
|
37224
|
+
var VERSION = "0.5.7";
|
|
37130
37225
|
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.
|
|
37131
37226
|
- 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) cover the AI gateway.
|
|
37132
37227
|
- 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): 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.
|
|
37133
|
-
- Destructive tools (delete, terminate, reinstall, rollback, restore a backup, 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.
|
|
37228
|
+
- 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.
|
|
37134
37229
|
- 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.
|
|
37135
37230
|
- 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.
|
|
37136
37231
|
- Databases: get_database_connection returns a password; put it in a secret variable (DATABASE_URL) instead of code or chat.
|
|
37232
|
+
- 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.
|
|
37137
37233
|
- Deploys and VPS actions run in the background: poll get_deployment / get_vps until they settle; on a failed deploy read get_deployment_logs.
|
|
37138
37234
|
- Never print secrets (passwords, secret variable values, secret files) unless the user asks for them. Domain owner details are personal data: use only what the user gave.
|
|
37139
37235
|
- Errors come back as { status, code, error }: 401/403 means the key lacks access (keys choose products and projects in Settings → API keys; wallet, invoices, tickets and AI money need a whole-account key: with a sign-in, reconnect and choose Whole account), 402 means plan limit or low balance.
|
package/dist/stdio.js
CHANGED
|
@@ -35471,6 +35471,7 @@ var password = exports_external.string().min(12).max(30).describe("12-30 charact
|
|
|
35471
35471
|
var period = exports_external.enum(["monthly", "3month", "6month", "yearly"]);
|
|
35472
35472
|
var pointId = exports_external.string().describe("Restore point id (from list_vps_restore_points)");
|
|
35473
35473
|
var backupPath = exports_external.string().max(1024).describe("Absolute path inside the backup, e.g. /etc/nginx or /var/www/index.html");
|
|
35474
|
+
var migrationId = exports_external.string().describe("Migration id (from list_vps_migrations)");
|
|
35474
35475
|
var vpsTools = [
|
|
35475
35476
|
defineTool({
|
|
35476
35477
|
name: "list_vps_plans",
|
|
@@ -35850,6 +35851,84 @@ var vpsTools = [
|
|
|
35850
35851
|
description: "Status of a prepared backup download; when ready, a short-lived url for the user to open (it expires at expiresAt). The link is for the user to download in a browser.",
|
|
35851
35852
|
input: { vpsId, downloadId: exports_external.string() },
|
|
35852
35853
|
run: (a, api2) => api2.api("GET", `/vps/${a.vpsId}/backup/downloads/${a.downloadId}`)
|
|
35854
|
+
}),
|
|
35855
|
+
defineTool({
|
|
35856
|
+
name: "list_vps_migrations",
|
|
35857
|
+
title: "List VPS migrations",
|
|
35858
|
+
kind: "read",
|
|
35859
|
+
description: "Migrations onto this VPS, newest first: imports from servers outside Helipod (kind=import) and copies from another VPS's restore point (kind=copy). Each has status (waiting, uploading, inspecting, uploaded, awaiting_command, restoring, restored, failed, expired, cancelled), the uploaded server's OS and size, who ran the command (fetchedIp), progress and pause state of a running job, and the result.",
|
|
35860
|
+
input: { vpsId },
|
|
35861
|
+
run: (a, api2) => api2.api("GET", `/vps/${a.vpsId}/migrations`)
|
|
35862
|
+
}),
|
|
35863
|
+
defineTool({
|
|
35864
|
+
name: "get_vps_migration",
|
|
35865
|
+
title: "Get a VPS migration",
|
|
35866
|
+
kind: "read",
|
|
35867
|
+
description: "One migration onto this VPS (see list_vps_migrations). While an upload or restore runs, result.progress has percent (of data read), bytesDone/bytesTotal, sentBytes and sendRate; result.stored the bytes confirmed in storage; result.control the pause state (want vs the job's reported state). Poll it every 15-30 s while it runs.",
|
|
35868
|
+
input: { vpsId, migrationId },
|
|
35869
|
+
run: (a, api2) => api2.api("GET", `/vps/${a.vpsId}/migrations/${a.migrationId}`)
|
|
35870
|
+
}),
|
|
35871
|
+
defineTool({
|
|
35872
|
+
name: "create_vps_import",
|
|
35873
|
+
title: "Start a server import (Migrate to Helipod)",
|
|
35874
|
+
kind: "write",
|
|
35875
|
+
description: "Start moving a server from OUTSIDE Helipod (any Linux server with root) onto this running VPS. Free; one import per VPS, at most 3 per account, kept 7 days, the upload is capped at this VPS's disk. Returns a one-time command (curl ... | sudo bash) that works ONCE and expires in 1 hour: give it to the user to run as root on the OLD server; never run it yourself and do not store it. Pass sourceIp (the old server's public IP, e.g. from curl -4 ifconfig.me there) so the command only works from that server; recommended. Then follow it with get_vps_migration: waiting → uploading → inspecting → uploaded.",
|
|
35876
|
+
input: { vpsId, sourceIp: exports_external.string().max(64).optional().describe("The old server's public IP: the command then only works from it (recommended)") },
|
|
35877
|
+
run: ({ vpsId: id2, sourceIp }, api2) => api2.api("POST", `/vps/${id2}/migrations/import`, { body: { sourceIp: sourceIp ?? null } })
|
|
35878
|
+
}),
|
|
35879
|
+
defineTool({
|
|
35880
|
+
name: "new_vps_migration_command",
|
|
35881
|
+
title: "Get a new migration command",
|
|
35882
|
+
kind: "write",
|
|
35883
|
+
description: "A fresh one-time command for an import (status waiting, uploading or failed before an upload finished) or for a restore that waits for its command (awaiting_command). The previous command stops working, and a job still running with it is told to stop. Shown once: give it to the user to run as root on the right server.",
|
|
35884
|
+
input: { vpsId, migrationId },
|
|
35885
|
+
run: (a, api2) => api2.api("POST", `/vps/${a.vpsId}/migrations/${a.migrationId}/command`)
|
|
35886
|
+
}),
|
|
35887
|
+
defineTool({
|
|
35888
|
+
name: "restore_vps_import",
|
|
35889
|
+
title: "Restore an import onto the VPS",
|
|
35890
|
+
kind: "destructive",
|
|
35891
|
+
description: "Restore an uploaded import (status uploaded, restored, or failed after an upload) onto its VPS. mode=files restores 1-50 paths (e.g. /home, /var/www, /etc/nginx, /var/backups/helipod-db for database dumps): target=folder (default) into /root/restore-<date>, target=inplace OVERWRITES files at the same paths. mode=full turns the whole VPS into a copy of the old server: EVERYTHING on the VPS is replaced, only allowed when the VPS runs the same OS as the upload (sourceOs) and is of the full-VM kind. The first restore needs confirmSource=true: before setting it, show the user fetchedIp (and the uploaded server's hostname) from get_vps_migration and ask whether that is their old server; if not, delete the import instead. Runs in the background: follow it with get_vps_migration.",
|
|
35892
|
+
input: {
|
|
35893
|
+
vpsId,
|
|
35894
|
+
migrationId,
|
|
35895
|
+
mode: exports_external.enum(["files", "full"]),
|
|
35896
|
+
paths: exports_external.array(backupPath).min(1).max(50).optional().describe("mode=files: files or folders to restore"),
|
|
35897
|
+
target: exports_external.enum(["folder", "inplace"]).optional().describe("mode=files: folder (default, safe) or inplace (overwrite)"),
|
|
35898
|
+
confirmSource: exports_external.boolean().optional().describe("true only after the user confirmed fetchedIp is their old server (needed once)")
|
|
35899
|
+
},
|
|
35900
|
+
run: (a, api2) => api2.api("POST", `/vps/${a.vpsId}/migrations/${a.migrationId}/restore`, { body: { mode: a.mode, paths: a.paths, target: a.target, confirmSource: a.confirmSource } })
|
|
35901
|
+
}),
|
|
35902
|
+
defineTool({
|
|
35903
|
+
name: "control_vps_migration",
|
|
35904
|
+
title: "Pause, resume or stop a migration job",
|
|
35905
|
+
kind: "write",
|
|
35906
|
+
description: "Pause, resume or stop an upload or restore that runs from a one-time command on a server (it must have checked in: result.control is set). The job follows within about 5 seconds; result.control.state shows when it did. Stop is final: the job ends as failed (stopped); a new command (new_vps_migration_command) starts the upload again and reuses what was already uploaded where possible. The user can do the same on that server with sudo helipod-migrate pause | resume | stop.",
|
|
35907
|
+
input: { vpsId, migrationId, action: exports_external.enum(["pause", "resume", "stop"]) },
|
|
35908
|
+
run: (a, api2) => api2.api("POST", `/vps/${a.vpsId}/migrations/${a.migrationId}/control`, { body: { action: a.action } })
|
|
35909
|
+
}),
|
|
35910
|
+
defineTool({
|
|
35911
|
+
name: "delete_vps_migration",
|
|
35912
|
+
title: "Delete a VPS migration",
|
|
35913
|
+
kind: "destructive",
|
|
35914
|
+
description: "Delete a migration; an import's uploaded files are deleted from storage and cannot be restored any more. A running upload is told to stop. Not while a restore or the upload check runs.",
|
|
35915
|
+
input: { vpsId, migrationId },
|
|
35916
|
+
run: (a, api2) => api2.api("DELETE", `/vps/${a.vpsId}/migrations/${a.migrationId}`)
|
|
35917
|
+
}),
|
|
35918
|
+
defineTool({
|
|
35919
|
+
name: "copy_vps_backup",
|
|
35920
|
+
title: "Copy a restore point to another VPS",
|
|
35921
|
+
kind: "destructive",
|
|
35922
|
+
description: "Copy a restore point of one VPS (vpsId, pointId from list_vps_restore_points) onto ANOTHER VPS of the same account (targetVpsId; to bring the same VPS back use restore_vps_backup). Free. mode=files copies 1-50 paths onto any running VPS: target=folder (default) into /root/restore-<date>, target=inplace OVERWRITES files at the same paths. mode=full turns the target into a full copy: everything on it is replaced; only between two VPS of the same kind running the same OS and version. Runs in the background: follow it with list_vps_migrations on the TARGET VPS. If the result has a command, the target's management agent was unreachable: give the user that one-time command to run as root on the target.",
|
|
35923
|
+
input: {
|
|
35924
|
+
vpsId,
|
|
35925
|
+
pointId,
|
|
35926
|
+
targetVpsId: exports_external.string().describe("The VPS to copy onto (id from list_vps), not vpsId"),
|
|
35927
|
+
mode: exports_external.enum(["files", "full"]),
|
|
35928
|
+
paths: exports_external.array(backupPath).min(1).max(50).optional().describe("mode=files: files or folders to copy"),
|
|
35929
|
+
target: exports_external.enum(["folder", "inplace"]).optional().describe("mode=files: folder (default, safe) or inplace (overwrite)")
|
|
35930
|
+
},
|
|
35931
|
+
run: (a, api2) => api2.api("POST", `/vps/${a.vpsId}/backup/points/${a.pointId}/copy`, { body: { targetVpsId: a.targetVpsId, mode: a.mode, paths: a.paths, target: a.target } })
|
|
35853
35932
|
})
|
|
35854
35933
|
];
|
|
35855
35934
|
|
|
@@ -35859,9 +35938,9 @@ var hostId = exports_external.number().int().positive().describe("Child nameserv
|
|
|
35859
35938
|
var dryRun2 = exports_external.boolean().optional().describe("true = return the exact price (IDR, VAT included) and the wallet balance after it; nothing is charged or registered");
|
|
35860
35939
|
var DRY = "dryRun=true returns the price and walletBalanceAfter without charging or changing anything.";
|
|
35861
35940
|
var record3 = exports_external.object({
|
|
35862
|
-
name: exports_external.string().describe('"@" for the domain itself, or
|
|
35941
|
+
name: exports_external.string().describe('"@" for the domain itself, or the part before the domain, e.g. "www" or "_dmarc.mail". A full name like "www.example.com" is accepted and stored as "www"'),
|
|
35863
35942
|
type: exports_external.enum(["A", "AAAA", "MX", "MXE", "CNAME", "SPF", "TXT"]),
|
|
35864
|
-
content: exports_external.string(),
|
|
35943
|
+
content: exports_external.string().describe('TXT without surrounding quotes (they are added by the DNS server); MX as "10 mail.example.com"'),
|
|
35865
35944
|
ttl: exports_external.number().int().optional()
|
|
35866
35945
|
});
|
|
35867
35946
|
var domainTools = [
|
|
@@ -35962,7 +36041,7 @@ var domainTools = [
|
|
|
35962
36041
|
name: "list_dns_records",
|
|
35963
36042
|
title: "List DNS records",
|
|
35964
36043
|
kind: "read",
|
|
35965
|
-
description:
|
|
36044
|
+
description: 'DNS records of a registered domain. prefix is the name relative to the domain ("@" for the domain itself); TXT content is shown with the quotes the DNS server adds. A domain with no records is not served at all (its nameservers answer REFUSED), so add at least one record for it to resolve.',
|
|
35966
36045
|
input: { domainId },
|
|
35967
36046
|
run: (a, api2) => api2.api("GET", `/domains/${a.domainId}/dns`)
|
|
35968
36047
|
}),
|
|
@@ -35970,7 +36049,7 @@ var domainTools = [
|
|
|
35970
36049
|
name: "set_dns_records",
|
|
35971
36050
|
title: "Replace DNS records",
|
|
35972
36051
|
kind: "destructive",
|
|
35973
|
-
description: "Replace ALL DNS records of the domain with this list. Records not included are deleted
|
|
36052
|
+
description: "Replace ALL DNS records of the domain with this list. Records not included are deleted: read list_dns_records first and send every record that should stay (prefix as name, content as listed; quotes around TXT are removed for you).",
|
|
35974
36053
|
input: { domainId, records: exports_external.array(record3).min(1) },
|
|
35975
36054
|
run: (a, api2) => api2.api("POST", `/domains/${a.domainId}/dns`, { body: { records: a.records } })
|
|
35976
36055
|
}),
|
|
@@ -36024,6 +36103,22 @@ var domainTools = [
|
|
|
36024
36103
|
input: { domainId, ...record3.shape },
|
|
36025
36104
|
run: (a, api2) => api2.api("PUT", `/domains/${a.domainId}/dns`, { body: { name: a.name, type: a.type, content: a.content, ttl: a.ttl } })
|
|
36026
36105
|
}),
|
|
36106
|
+
defineTool({
|
|
36107
|
+
name: "list_domain_email_records",
|
|
36108
|
+
title: "Email records for a domain",
|
|
36109
|
+
kind: "read",
|
|
36110
|
+
description: "Helipod Email sending domains inside this registered domain (e.g. mail.example.com), each DNS record they need and whether the domain has it: present, missing, or conflict (with what to change).",
|
|
36111
|
+
input: { domainId },
|
|
36112
|
+
run: (a, api2) => api2.api("GET", `/domains/${a.domainId}/email-records`)
|
|
36113
|
+
}),
|
|
36114
|
+
defineTool({
|
|
36115
|
+
name: "add_domain_email_records",
|
|
36116
|
+
title: "Add email records to a domain",
|
|
36117
|
+
kind: "write",
|
|
36118
|
+
description: "Add the DNS records a Helipod Email sending domain still needs to this registered domain (Helipod DNS only), then start its verification. Records that would clash (a CNAME on a name in use, a second SPF) are not changed and are listed in conflicts. emailDomainId comes from list_domain_email_records.",
|
|
36119
|
+
input: { domainId, emailDomainId: exports_external.string().uuid() },
|
|
36120
|
+
run: (a, api2) => api2.api("POST", `/domains/${a.domainId}/email-records`, { body: { emailDomainId: a.emailDomainId } })
|
|
36121
|
+
}),
|
|
36027
36122
|
defineTool({
|
|
36028
36123
|
name: "list_child_nameservers",
|
|
36029
36124
|
title: "List child nameservers",
|
|
@@ -36249,7 +36344,7 @@ var emailTools = [
|
|
|
36249
36344
|
name: "verify_email_domain",
|
|
36250
36345
|
title: "Verify sending domain",
|
|
36251
36346
|
kind: "write",
|
|
36252
|
-
description: "Check a sending domain's DNS records again.",
|
|
36347
|
+
description: "Check a sending domain's DNS records again now. Pending domains are also re-checked automatically every 30 minutes for 3 days.",
|
|
36253
36348
|
input: { projectId, domainId: exports_external.string() },
|
|
36254
36349
|
run: (a, api2) => api2.email("POST", `/domains/${a.domainId}/verify`, { project: a.projectId })
|
|
36255
36350
|
}),
|
|
@@ -36297,7 +36392,7 @@ var emailTools = [
|
|
|
36297
36392
|
name: "get_email_domain",
|
|
36298
36393
|
title: "Get sending domain",
|
|
36299
36394
|
kind: "read",
|
|
36300
|
-
description: "One sending domain: verification status and the DNS records to add.",
|
|
36395
|
+
description: "One sending domain: verification status and the DNS records to add. ownershipVerified is true once the ownership TXT is confirmed; status turns verified only when DKIM passes too. When the domain is registered at Helipod, add_domain_email_records adds the records in one call.",
|
|
36301
36396
|
input: { projectId, domainId: exports_external.string() },
|
|
36302
36397
|
run: (a, api2) => api2.email("GET", `/domains/${a.domainId}`, { project: a.projectId })
|
|
36303
36398
|
}),
|
|
@@ -37233,14 +37328,15 @@ var generalTools = [
|
|
|
37233
37328
|
];
|
|
37234
37329
|
|
|
37235
37330
|
// src/server.ts
|
|
37236
|
-
var VERSION = "0.5.
|
|
37331
|
+
var VERSION = "0.5.7";
|
|
37237
37332
|
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.
|
|
37238
37333
|
- 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) cover the AI gateway.
|
|
37239
37334
|
- 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): 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.
|
|
37240
|
-
- Destructive tools (delete, terminate, reinstall, rollback, restore a backup, 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.
|
|
37335
|
+
- 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.
|
|
37241
37336
|
- 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.
|
|
37242
37337
|
- 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.
|
|
37243
37338
|
- Databases: get_database_connection returns a password; put it in a secret variable (DATABASE_URL) instead of code or chat.
|
|
37339
|
+
- 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.
|
|
37244
37340
|
- Deploys and VPS actions run in the background: poll get_deployment / get_vps until they settle; on a failed deploy read get_deployment_logs.
|
|
37245
37341
|
- Never print secrets (passwords, secret variable values, secret files) unless the user asks for them. Domain owner details are personal data: use only what the user gave.
|
|
37246
37342
|
- Errors come back as { status, code, error }: 401/403 means the key lacks access (keys choose products and projects in Settings → API keys; wallet, invoices, tickets and AI money need a whole-account key: with a sign-in, reconnect and choose Whole account), 402 means plan limit or low balance.
|
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.7",
|
|
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": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: helipod-ai-gateway
|
|
3
|
-
description: Add AI features to an application using the Helipod AI Gateway (ai.helipod.io) - OpenAI and Anthropic compatible endpoints, one key for many models, billed per token in Rupiah. Use when the user wants to call LLMs from their code through Helipod or switch an OpenAI/Anthropic integration to Helipod.
|
|
3
|
+
description: Add AI features to an application using the Helipod AI Gateway (ai.helipod.io) - OpenAI and Anthropic compatible endpoints plus image generation, one key for many models, billed per token in Rupiah. Use when the user wants to call LLMs from their code through Helipod or switch an OpenAI/Anthropic integration to Helipod.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Helipod AI Gateway
|
|
@@ -38,6 +38,18 @@ ai = OpenAI(base_url="https://ai.helipod.io/v1", api_key=os.environ["HELIPOD_AI_
|
|
|
38
38
|
- Prices per 1M tokens (input / output / cached) are on https://helipod.io/ai-gateway. Some models cost more at peak hours (Mon to Fri 08:00 to 11:00 and 13:00 to 17:00 WIB).
|
|
39
39
|
- Small/fast for classification and chat; larger models for reasoning and code.
|
|
40
40
|
|
|
41
|
+
## Images
|
|
42
|
+
|
|
43
|
+
`POST https://ai.helipod.io/v1/images/generations` (OpenAI images format, same key): `{ model, prompt, size?, n?, images?: [https urls], response_format?: "url"|"b64_json", async?: true }`.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
const img = await ai.images.generate({ model: "seedream-image-v5.0-flash", prompt: "A wooden boat at sunrise, watercolor" });
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
- Image models are NOT in `/v1/models`; they are listed with sizes and prices at https://docs.helipod.io/docs/ai-gateway/images (cheapest: `seedream-image-v5.0-flash`, about Rp 320 per image).
|
|
50
|
+
- **Images are kept for 7 days, then deleted with their links** (`expires_at` in every answer). When an app needs images longer, download them right away and store them in the app's own storage; never save only the returned URL. Tell the user this when you build it.
|
|
51
|
+
- The request waits up to ~80 s; otherwise it answers `202` with an `id`: poll `GET /v1/images/generations/{id}` until `status` is `succeeded` or `failed`. Failed images are not charged.
|
|
52
|
+
|
|
41
53
|
## Practical rules
|
|
42
54
|
|
|
43
55
|
- Stream (`stream: true`) for long answers and reasoning models. One request may take at most 10 minutes; set the client timeout to at least 10 minutes. Failed requests are not charged.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: helipod-vps
|
|
3
|
-
description: Create and manage Helipod VPS servers - pick a plan and OS, order, power on/off, reinstall, get SSH/RDP login, firewall rules, snapshots, renewals and upgrades. Use when the user wants a virtual server with root access on Helipod.
|
|
3
|
+
description: Create and manage Helipod VPS servers - pick a plan and OS, order, power on/off, reinstall, get SSH/RDP login, firewall rules, snapshots, renewals and upgrades, and migrate a server from another provider (or copy between VPS). Use when the user wants a virtual server with root access on Helipod, or to move a server to Helipod.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Helipod VPS
|
|
@@ -25,4 +25,16 @@ Follow the `helipod` skill rules. A VPS is prepaid per period: creating one char
|
|
|
25
25
|
- Billing: `get_vps_renewal_options` → `renew_vps`; `upgrade_vps` to a larger plan (charges the difference, restarts); `toggle_vps_auto_renew`.
|
|
26
26
|
- Terminate: `terminate_vps` is permanent, no refund: confirm explicitly.
|
|
27
27
|
|
|
28
|
+
## Migrate a server to Helipod (free)
|
|
29
|
+
|
|
30
|
+
For a server at another provider (Zeabur, DigitalOcean, any Linux server with root):
|
|
31
|
+
|
|
32
|
+
1. The target is a running Helipod VPS with enough disk for the old server's data (create one first if needed). Whole-server restores need the same OS and version as the old server: ask which OS it runs before picking one.
|
|
33
|
+
2. Ask for the old server's public IP (`curl -4 ifconfig.me` there), then `create_vps_import` with `sourceIp`. Give the user the returned command to run as root on the OLD server. It works once, for 1 hour; never run it yourself. Lost or expired: `new_vps_migration_command`.
|
|
34
|
+
3. `get_vps_migration` every 15-30 s: `waiting` → `uploading` (progress, sent bytes; pause / resume / stop with `control_vps_migration`) → `inspecting` → `uploaded` (`sourceOs`, `sourceBytes`, `fetchedIp`).
|
|
35
|
+
4. Show the user `fetchedIp` and the server's hostname and ask whether that is their old server. Only then `restore_vps_import` with `confirmSource: true`. Suggest folders first (`/var/www`, `/home`, `/etc/nginx`, `/var/backups/helipod-db` for database dumps) into a new folder (`target: "folder"`); `inplace` overwrites and `mode: "full"` replaces the whole VPS: confirm explicitly.
|
|
36
|
+
5. The upload is kept 7 days, so the user can restore again; `delete_vps_migration` removes it sooner.
|
|
37
|
+
|
|
38
|
+
Copy between Helipod VPS: `list_vps_restore_points` on the source, then `copy_vps_backup` with `targetVpsId` (folders onto any VPS; whole server only between the same kind and OS), then follow `list_vps_migrations` on the target.
|
|
39
|
+
|
|
28
40
|
Backups (paid add-on) have dedicated tools; to read anything else use `helipod_api_get` with paths from https://docs.helipod.io/docs/vps-api
|