helipod-mcp 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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`): 70 tools over the Container, VPS, Domain and Email APIs. Works in Claude Code, Codex, Cursor, VS Code, Windsurf, Claude Desktop and any MCP client.
5
+ - **MCP server** (`helipod-mcp`): 89 tools over the Container, Database, VPS, Domain and Email 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**
@@ -25,7 +25,7 @@ Prefer a key, a local server, or a tool not listed? Read on.
25
25
 
26
26
  ## 1. Get a key
27
27
 
28
- In the [dashboard](https://helipod.io/project) open a project, then **Settings → API keys**. Choose the products (Container API, VPS API, Domain API) and the reach (one project, or the whole account; the Domain API needs a whole-account key). The key starts with `hel_`.
28
+ In the [dashboard](https://helipod.io/project) open a project, then **Settings → API keys**. Choose the products (Container API, Database API, VPS API, Domain API) and the reach (one project, or the whole account; the Domain API needs a whole-account key). The key starts with `hel_`.
29
29
 
30
30
  Optional: an **email API key** from the project's **Email** section, for the email tools.
31
31
 
@@ -180,7 +180,7 @@ Pick your agents when asked (or pass `-a codex cursor gemini-cli opencode -y`).
180
180
  ## 3. Ask
181
181
 
182
182
  - "Deploy this repo to my Helipod project and give me the URL."
183
- - "Add Postgres to production and connect it to the api service."
183
+ - "Create a Postgres database for this project and connect it to the api service."
184
184
  - "Why did the last deploy fail?"
185
185
  - "Is tokosaya.id available, and how much is it?"
186
186
  - "Point www.tokosaya.id at my web service."
@@ -192,6 +192,7 @@ Pick your agents when asked (or pass `-a codex cursor gemini-cli opencode -y`).
192
192
  | Area | Tools |
193
193
  |---|---|
194
194
  | Containers | projects, services (create from image, Git or Dockerfile), deploy, deployments and build logs, restart, variables, domains, scale, volumes, logs, metrics, templates, multi-service stacks |
195
+ | Databases | managed Postgres: create, connection string, branches per environment, compute settings, plan upgrades, usage, tables, read-only queries and confirmed writes |
195
196
  | VPS | plans, create, power, reinstall, password, login details, terminate, renew, upgrade, auto-renew, firewall, snapshots |
196
197
  | Domains | search, prices, register, transfer, renew, DNS records, nameservers, WHOIS privacy |
197
198
  | Email | status, send, sent log, sending domains, templates, contacts |
package/dist/http.js CHANGED
@@ -35620,7 +35620,11 @@ class HelipodClient {
35620
35620
  return this.request(this.emailApiUrl, this.cfg.emailApiKey, method, path, opts);
35621
35621
  }
35622
35622
  async request(base, key, method, path, opts) {
35623
- const url2 = new URL(base + (path.startsWith("/") ? path : `/${path}`));
35623
+ const raw = path.startsWith("/") ? path : `/${path}`;
35624
+ if (/(^|\/)\.\.?(\/|$)|[?#\\]|\/\/|%2e|%2f|%5c/i.test(raw)) {
35625
+ throw new HelipodError(400, "INVALID_PATH", "An id in this request contains characters that are not allowed.");
35626
+ }
35627
+ const url2 = new URL(base + raw);
35624
35628
  for (const [k, v] of Object.entries(opts.query ?? {}))
35625
35629
  if (v !== undefined && v !== null && v !== "")
35626
35630
  url2.searchParams.set(k, String(v));
@@ -36410,14 +36414,184 @@ var emailTools = [
36410
36414
  })
36411
36415
  ];
36412
36416
 
36417
+ // src/tools/databases.ts
36418
+ var databaseId = exports_external.string().uuid().describe("Database id (from list_databases)");
36419
+ var environmentId = exports_external.string().uuid().describe("Environment id (from list_projects). Omit for the primary (production) branch.");
36420
+ var dryRun2 = exports_external.boolean().optional().describe("true = only check and show the result or price; nothing changes");
36421
+ var databaseTools = [
36422
+ defineTool({
36423
+ name: "list_database_plans",
36424
+ title: "List database plans",
36425
+ kind: "read",
36426
+ description: "Managed Postgres plans: price (IDR), included compute hours and storage, maximum CU. Free is the default; paid plans are prepaid credits that never expire.",
36427
+ input: {},
36428
+ run: (_a3, api2) => api2.api("GET", "/databases/plans")
36429
+ }),
36430
+ defineTool({
36431
+ name: "list_databases",
36432
+ title: "List databases",
36433
+ kind: "read",
36434
+ description: "Managed Postgres databases in the projects this key can reach: status, plan and remaining credits, region, branches per environment.",
36435
+ input: {},
36436
+ run: (_a3, api2) => api2.api("GET", "/databases")
36437
+ }),
36438
+ defineTool({
36439
+ name: "get_database",
36440
+ title: "Get database",
36441
+ kind: "read",
36442
+ description: "One database with its plan, compute settings and branches.",
36443
+ input: { databaseId },
36444
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}`)
36445
+ }),
36446
+ defineTool({
36447
+ name: "create_database",
36448
+ title: "Create database",
36449
+ kind: "write",
36450
+ idempotent: true,
36451
+ description: "Create the managed Postgres database of a project (free plan; one database per project, one branch per environment). Returns the existing one if the project already has it.",
36452
+ input: { projectId: exports_external.string().uuid(), idempotencyKey: exports_external.string().max(128).optional() },
36453
+ run: (a, api2) => api2.api("POST", "/databases", { body: { projectId: a.projectId }, idempotencyKey: a.idempotencyKey })
36454
+ }),
36455
+ defineTool({
36456
+ name: "delete_database",
36457
+ title: "Delete database",
36458
+ kind: "destructive",
36459
+ description: "Delete a database with ALL its data and branches. Irreversible. Only after the user confirms; confirm must be the database name.",
36460
+ input: { databaseId, confirm: exports_external.string().describe("The database name, exactly") },
36461
+ run: (a, api2) => api2.api("DELETE", `/databases/${a.databaseId}`, { body: { confirm: a.confirm } })
36462
+ }),
36463
+ defineTool({
36464
+ name: "get_database_connection",
36465
+ title: "Get database connection",
36466
+ kind: "read",
36467
+ description: "Connection string (postgresql://…, pooled, SSL) and its parts for an environment's branch. Contains the password: store it as a secret variable (e.g. DATABASE_URL via set_variables secrets), never print it unless asked.",
36468
+ input: { databaseId, environmentId: environmentId.optional() },
36469
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}/connection`, { query: { environmentId: a.environmentId } })
36470
+ }),
36471
+ defineTool({
36472
+ name: "list_database_branches",
36473
+ title: "List database branches",
36474
+ kind: "read",
36475
+ description: "Branches of a database, one per environment (production is the primary).",
36476
+ input: { databaseId },
36477
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}/branches`)
36478
+ }),
36479
+ defineTool({
36480
+ name: "create_database_branch",
36481
+ title: "Create database branch",
36482
+ kind: "write",
36483
+ idempotent: true,
36484
+ description: "Give an environment (e.g. staging) its own branch: a copy of the source environment's data at this moment (the primary when omitted).",
36485
+ input: { databaseId, environmentId: exports_external.string().uuid(), sourceEnvironmentId: exports_external.string().uuid().optional() },
36486
+ run: (a, api2) => api2.api("POST", `/databases/${a.databaseId}/branches`, { body: { environmentId: a.environmentId, sourceEnvironmentId: a.sourceEnvironmentId } })
36487
+ }),
36488
+ defineTool({
36489
+ name: "delete_database_branch",
36490
+ title: "Delete database branch",
36491
+ kind: "destructive",
36492
+ description: "Delete an environment's branch and its data. The primary (production) branch cannot be deleted. Confirm with the user.",
36493
+ input: { databaseId, environmentId: exports_external.string().uuid() },
36494
+ run: (a, api2) => api2.api("DELETE", `/databases/${a.databaseId}/branches/${a.environmentId}`)
36495
+ }),
36496
+ defineTool({
36497
+ name: "get_database_settings",
36498
+ title: "Get database compute settings",
36499
+ kind: "read",
36500
+ description: "Compute settings (paid plans): minCu (floor), maxCu (autoscaling ceiling), suspendTimeoutSeconds (idle time before scaling to zero), the plan's maximum CU.",
36501
+ input: { databaseId },
36502
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}/settings`)
36503
+ }),
36504
+ defineTool({
36505
+ name: "update_database_settings",
36506
+ title: "Update database compute settings",
36507
+ kind: "write",
36508
+ description: "Change compute for every branch (paid plans). A minCu above zero keeps compute running and uses credits while idle: run with dryRun first and show idleComputeHoursPerMonth.",
36509
+ input: {
36510
+ databaseId,
36511
+ minCu: exports_external.number().min(0.25).max(10).optional(),
36512
+ maxCu: exports_external.number().min(0.25).max(10).optional(),
36513
+ suspendTimeoutSeconds: exports_external.number().int().min(0).max(604800).optional().describe("0 = never suspend, or 60-604800"),
36514
+ dryRun: dryRun2
36515
+ },
36516
+ run: (a, api2) => api2.api("PATCH", `/databases/${a.databaseId}/settings`, {
36517
+ body: { minCu: a.minCu, maxCu: a.maxCu, suspendTimeoutSeconds: a.suspendTimeoutSeconds },
36518
+ query: { dryRun: a.dryRun ? "true" : undefined }
36519
+ })
36520
+ }),
36521
+ defineTool({
36522
+ name: "upgrade_database",
36523
+ title: "Upgrade database plan / top up",
36524
+ kind: "write",
36525
+ description: "Buy a paid plan or top up credits. COSTS MONEY from the wallet: call with dryRun first, show price and wallet balance after, and get the user's explicit OK.",
36526
+ input: { databaseId, plan: exports_external.string().describe("Plan code (e.g. STARTER) or id from list_database_plans"), dryRun: dryRun2, idempotencyKey: exports_external.string().max(128).optional() },
36527
+ run: (a, api2) => api2.api("POST", `/databases/${a.databaseId}/upgrade`, { body: { plan: a.plan }, query: { dryRun: a.dryRun ? "true" : undefined }, idempotencyKey: a.idempotencyKey })
36528
+ }),
36529
+ defineTool({
36530
+ name: "suspend_database",
36531
+ title: "Suspend database",
36532
+ kind: "destructive",
36533
+ description: "Stop the database's compute (connections fail until resumed). Confirm with the user.",
36534
+ input: { databaseId },
36535
+ run: (a, api2) => api2.api("POST", `/databases/${a.databaseId}/suspend`)
36536
+ }),
36537
+ defineTool({
36538
+ name: "resume_database",
36539
+ title: "Resume database",
36540
+ kind: "write",
36541
+ description: "Start a suspended database again.",
36542
+ input: { databaseId },
36543
+ run: (a, api2) => api2.api("POST", `/databases/${a.databaseId}/resume`)
36544
+ }),
36545
+ defineTool({
36546
+ name: "get_database_usage",
36547
+ title: "Database usage",
36548
+ kind: "read",
36549
+ description: "This period's compute (CU-hours), storage, history and network, plus the plan's remaining credits.",
36550
+ input: { databaseId },
36551
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}/usage`)
36552
+ }),
36553
+ defineTool({
36554
+ name: "list_database_tables",
36555
+ title: "List database tables",
36556
+ kind: "read",
36557
+ description: "Tables and views in the public schema of an environment's branch.",
36558
+ input: { databaseId, environmentId: environmentId.optional() },
36559
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}/tables`, { query: { environmentId: a.environmentId } })
36560
+ }),
36561
+ defineTool({
36562
+ name: "describe_database_table",
36563
+ title: "Describe database table",
36564
+ kind: "read",
36565
+ description: "Columns of a table (name, type, nullable, default).",
36566
+ input: { databaseId, table: exports_external.string().min(1).max(63), environmentId: environmentId.optional() },
36567
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}/tables/${encodeURIComponent(a.table)}/columns`, { query: { environmentId: a.environmentId } })
36568
+ }),
36569
+ defineTool({
36570
+ name: "query_database",
36571
+ title: "Query database (read-only)",
36572
+ kind: "read",
36573
+ description: "Run a read-only SQL statement (SELECT, EXPLAIN, SHOW…) in a read-only transaction; up to 200 rows. Use $1, $2… placeholders with params. Statements that change data are refused: use execute_database_sql.",
36574
+ input: { databaseId, sql: exports_external.string().min(1).max(1e4), params: exports_external.array(exports_external.any()).max(100).optional(), environmentId: environmentId.optional() },
36575
+ run: (a, api2) => api2.api("POST", `/databases/${a.databaseId}/query`, { body: { sql: a.sql, params: a.params, environmentId: a.environmentId } })
36576
+ }),
36577
+ defineTool({
36578
+ name: "execute_database_sql",
36579
+ title: "Execute SQL (writes)",
36580
+ kind: "destructive",
36581
+ description: "Run a statement that changes data or schema (INSERT, UPDATE, DELETE, CREATE, ALTER, DROP…). Show the statement and the target environment to the user and get explicit confirmation first; prefer a staging branch for migrations.",
36582
+ input: { databaseId, sql: exports_external.string().min(1).max(1e4), params: exports_external.array(exports_external.any()).max(100).optional(), environmentId: environmentId.optional() },
36583
+ run: (a, api2) => api2.api("POST", `/databases/${a.databaseId}/query`, { body: { sql: a.sql, params: a.params, environmentId: a.environmentId, allowWrite: true } })
36584
+ })
36585
+ ];
36586
+
36413
36587
  // src/tools/general.ts
36414
- var API_PREFIXES = ["/containers/", "/vps", "/backup/", "/domains"];
36588
+ var API_PREFIXES = ["/containers/", "/databases", "/vps", "/backup/", "/domains"];
36415
36589
  var generalTools = [
36416
36590
  defineTool({
36417
36591
  name: "helipod_request",
36418
36592
  title: "Call any Helipod API endpoint",
36419
36593
  kind: "destructive",
36420
- description: "Call an endpoint that has no dedicated tool, as documented at https://docs.helipod.io. api=core for the VPS, Container and Domain APIs (paths like /vps/{id}/backup), api=email for the Email API (paths like /broadcasts). Prefer the dedicated tools; ask the user before any call that costs money or deletes something.",
36594
+ description: "Call an endpoint that has no dedicated tool, as documented at https://docs.helipod.io. api=core for the Container, Database, VPS and Domain APIs (paths like /vps/{id}/backup), api=email for the Email API (paths like /broadcasts). Prefer the dedicated tools; ask the user before any call that costs money or deletes something.",
36421
36595
  input: {
36422
36596
  api: exports_external.enum(["core", "email"]),
36423
36597
  method: exports_external.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
@@ -36439,16 +36613,17 @@ var generalTools = [
36439
36613
  ];
36440
36614
 
36441
36615
  // src/server.ts
36442
- var VERSION = "0.2.0";
36443
- var INSTRUCTIONS = `Helipod is a cloud platform (Indonesia): containers, VPS, domains, email and an AI gateway, billed from a prepaid wallet in Rupiah.
36616
+ var VERSION = "0.3.1";
36617
+ 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.
36444
36618
  - Start with list_projects to find projects and environment ids.
36445
- - Anything that costs money (create/deploy/scale services, create/renew/upgrade VPS, register/renew/transfer domains): run the dryRun or price lookup first, tell the user the cost in IDR and wait for an explicit yes.
36446
- - Destructive tools (delete, terminate, reinstall, rollback, replace DNS): confirm with the user and name what will be lost.
36619
+ - Anything that costs money (create/deploy/scale services, upgrade a database plan, create/renew/upgrade VPS, register/renew/transfer domains): run the dryRun or price lookup first, tell the user the cost in IDR and wait for an explicit yes.
36620
+ - Destructive tools (delete, terminate, reinstall, rollback, replace DNS, execute_database_sql): confirm with the user and name what will be lost.
36621
+ - Databases: get_database_connection returns a password; put it in a secret variable (DATABASE_URL) instead of code or chat.
36447
36622
  - Deploys and VPS actions run in the background: poll get_deployment / get_vps until they settle; on a failed deploy read get_deployment_logs.
36448
36623
  - Never print secrets (passwords, secret variable values) unless the user asks for them.
36449
36624
  - Errors come back as { status, code, error }: 401/403 means the key lacks access (keys choose products and projects in Settings → API keys), 402 means plan limit or low balance.
36450
36625
  Docs: https://docs.helipod.io`;
36451
- var ALL_TOOLS = [...containerTools, ...vpsTools, ...domainTools, ...emailTools, ...generalTools];
36626
+ var ALL_TOOLS = [...containerTools, ...databaseTools, ...vpsTools, ...domainTools, ...emailTools, ...generalTools];
36452
36627
  function createHelipodServer(config2) {
36453
36628
  const server = new McpServer({ name: "helipod", version: VERSION, title: "Helipod" }, { instructions: INSTRUCTIONS });
36454
36629
  const api2 = new HelipodClient({ userAgent: `helipod-mcp/${VERSION}`, ...config2 });
package/dist/index.js CHANGED
@@ -34765,7 +34765,11 @@ class HelipodClient {
34765
34765
  return this.request(this.emailApiUrl, this.cfg.emailApiKey, method, path, opts);
34766
34766
  }
34767
34767
  async request(base, key, method, path, opts) {
34768
- const url2 = new URL(base + (path.startsWith("/") ? path : `/${path}`));
34768
+ const raw = path.startsWith("/") ? path : `/${path}`;
34769
+ if (/(^|\/)\.\.?(\/|$)|[?#\\]|\/\/|%2e|%2f|%5c/i.test(raw)) {
34770
+ throw new HelipodError(400, "INVALID_PATH", "An id in this request contains characters that are not allowed.");
34771
+ }
34772
+ const url2 = new URL(base + raw);
34769
34773
  for (const [k, v] of Object.entries(opts.query ?? {}))
34770
34774
  if (v !== undefined && v !== null && v !== "")
34771
34775
  url2.searchParams.set(k, String(v));
@@ -35555,14 +35559,184 @@ var emailTools = [
35555
35559
  })
35556
35560
  ];
35557
35561
 
35562
+ // src/tools/databases.ts
35563
+ var databaseId = exports_external.string().uuid().describe("Database id (from list_databases)");
35564
+ var environmentId = exports_external.string().uuid().describe("Environment id (from list_projects). Omit for the primary (production) branch.");
35565
+ var dryRun2 = exports_external.boolean().optional().describe("true = only check and show the result or price; nothing changes");
35566
+ var databaseTools = [
35567
+ defineTool({
35568
+ name: "list_database_plans",
35569
+ title: "List database plans",
35570
+ kind: "read",
35571
+ description: "Managed Postgres plans: price (IDR), included compute hours and storage, maximum CU. Free is the default; paid plans are prepaid credits that never expire.",
35572
+ input: {},
35573
+ run: (_a3, api2) => api2.api("GET", "/databases/plans")
35574
+ }),
35575
+ defineTool({
35576
+ name: "list_databases",
35577
+ title: "List databases",
35578
+ kind: "read",
35579
+ description: "Managed Postgres databases in the projects this key can reach: status, plan and remaining credits, region, branches per environment.",
35580
+ input: {},
35581
+ run: (_a3, api2) => api2.api("GET", "/databases")
35582
+ }),
35583
+ defineTool({
35584
+ name: "get_database",
35585
+ title: "Get database",
35586
+ kind: "read",
35587
+ description: "One database with its plan, compute settings and branches.",
35588
+ input: { databaseId },
35589
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}`)
35590
+ }),
35591
+ defineTool({
35592
+ name: "create_database",
35593
+ title: "Create database",
35594
+ kind: "write",
35595
+ idempotent: true,
35596
+ description: "Create the managed Postgres database of a project (free plan; one database per project, one branch per environment). Returns the existing one if the project already has it.",
35597
+ input: { projectId: exports_external.string().uuid(), idempotencyKey: exports_external.string().max(128).optional() },
35598
+ run: (a, api2) => api2.api("POST", "/databases", { body: { projectId: a.projectId }, idempotencyKey: a.idempotencyKey })
35599
+ }),
35600
+ defineTool({
35601
+ name: "delete_database",
35602
+ title: "Delete database",
35603
+ kind: "destructive",
35604
+ description: "Delete a database with ALL its data and branches. Irreversible. Only after the user confirms; confirm must be the database name.",
35605
+ input: { databaseId, confirm: exports_external.string().describe("The database name, exactly") },
35606
+ run: (a, api2) => api2.api("DELETE", `/databases/${a.databaseId}`, { body: { confirm: a.confirm } })
35607
+ }),
35608
+ defineTool({
35609
+ name: "get_database_connection",
35610
+ title: "Get database connection",
35611
+ kind: "read",
35612
+ description: "Connection string (postgresql://…, pooled, SSL) and its parts for an environment's branch. Contains the password: store it as a secret variable (e.g. DATABASE_URL via set_variables secrets), never print it unless asked.",
35613
+ input: { databaseId, environmentId: environmentId.optional() },
35614
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}/connection`, { query: { environmentId: a.environmentId } })
35615
+ }),
35616
+ defineTool({
35617
+ name: "list_database_branches",
35618
+ title: "List database branches",
35619
+ kind: "read",
35620
+ description: "Branches of a database, one per environment (production is the primary).",
35621
+ input: { databaseId },
35622
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}/branches`)
35623
+ }),
35624
+ defineTool({
35625
+ name: "create_database_branch",
35626
+ title: "Create database branch",
35627
+ kind: "write",
35628
+ idempotent: true,
35629
+ description: "Give an environment (e.g. staging) its own branch: a copy of the source environment's data at this moment (the primary when omitted).",
35630
+ input: { databaseId, environmentId: exports_external.string().uuid(), sourceEnvironmentId: exports_external.string().uuid().optional() },
35631
+ run: (a, api2) => api2.api("POST", `/databases/${a.databaseId}/branches`, { body: { environmentId: a.environmentId, sourceEnvironmentId: a.sourceEnvironmentId } })
35632
+ }),
35633
+ defineTool({
35634
+ name: "delete_database_branch",
35635
+ title: "Delete database branch",
35636
+ kind: "destructive",
35637
+ description: "Delete an environment's branch and its data. The primary (production) branch cannot be deleted. Confirm with the user.",
35638
+ input: { databaseId, environmentId: exports_external.string().uuid() },
35639
+ run: (a, api2) => api2.api("DELETE", `/databases/${a.databaseId}/branches/${a.environmentId}`)
35640
+ }),
35641
+ defineTool({
35642
+ name: "get_database_settings",
35643
+ title: "Get database compute settings",
35644
+ kind: "read",
35645
+ description: "Compute settings (paid plans): minCu (floor), maxCu (autoscaling ceiling), suspendTimeoutSeconds (idle time before scaling to zero), the plan's maximum CU.",
35646
+ input: { databaseId },
35647
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}/settings`)
35648
+ }),
35649
+ defineTool({
35650
+ name: "update_database_settings",
35651
+ title: "Update database compute settings",
35652
+ kind: "write",
35653
+ description: "Change compute for every branch (paid plans). A minCu above zero keeps compute running and uses credits while idle: run with dryRun first and show idleComputeHoursPerMonth.",
35654
+ input: {
35655
+ databaseId,
35656
+ minCu: exports_external.number().min(0.25).max(10).optional(),
35657
+ maxCu: exports_external.number().min(0.25).max(10).optional(),
35658
+ suspendTimeoutSeconds: exports_external.number().int().min(0).max(604800).optional().describe("0 = never suspend, or 60-604800"),
35659
+ dryRun: dryRun2
35660
+ },
35661
+ run: (a, api2) => api2.api("PATCH", `/databases/${a.databaseId}/settings`, {
35662
+ body: { minCu: a.minCu, maxCu: a.maxCu, suspendTimeoutSeconds: a.suspendTimeoutSeconds },
35663
+ query: { dryRun: a.dryRun ? "true" : undefined }
35664
+ })
35665
+ }),
35666
+ defineTool({
35667
+ name: "upgrade_database",
35668
+ title: "Upgrade database plan / top up",
35669
+ kind: "write",
35670
+ description: "Buy a paid plan or top up credits. COSTS MONEY from the wallet: call with dryRun first, show price and wallet balance after, and get the user's explicit OK.",
35671
+ input: { databaseId, plan: exports_external.string().describe("Plan code (e.g. STARTER) or id from list_database_plans"), dryRun: dryRun2, idempotencyKey: exports_external.string().max(128).optional() },
35672
+ run: (a, api2) => api2.api("POST", `/databases/${a.databaseId}/upgrade`, { body: { plan: a.plan }, query: { dryRun: a.dryRun ? "true" : undefined }, idempotencyKey: a.idempotencyKey })
35673
+ }),
35674
+ defineTool({
35675
+ name: "suspend_database",
35676
+ title: "Suspend database",
35677
+ kind: "destructive",
35678
+ description: "Stop the database's compute (connections fail until resumed). Confirm with the user.",
35679
+ input: { databaseId },
35680
+ run: (a, api2) => api2.api("POST", `/databases/${a.databaseId}/suspend`)
35681
+ }),
35682
+ defineTool({
35683
+ name: "resume_database",
35684
+ title: "Resume database",
35685
+ kind: "write",
35686
+ description: "Start a suspended database again.",
35687
+ input: { databaseId },
35688
+ run: (a, api2) => api2.api("POST", `/databases/${a.databaseId}/resume`)
35689
+ }),
35690
+ defineTool({
35691
+ name: "get_database_usage",
35692
+ title: "Database usage",
35693
+ kind: "read",
35694
+ description: "This period's compute (CU-hours), storage, history and network, plus the plan's remaining credits.",
35695
+ input: { databaseId },
35696
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}/usage`)
35697
+ }),
35698
+ defineTool({
35699
+ name: "list_database_tables",
35700
+ title: "List database tables",
35701
+ kind: "read",
35702
+ description: "Tables and views in the public schema of an environment's branch.",
35703
+ input: { databaseId, environmentId: environmentId.optional() },
35704
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}/tables`, { query: { environmentId: a.environmentId } })
35705
+ }),
35706
+ defineTool({
35707
+ name: "describe_database_table",
35708
+ title: "Describe database table",
35709
+ kind: "read",
35710
+ description: "Columns of a table (name, type, nullable, default).",
35711
+ input: { databaseId, table: exports_external.string().min(1).max(63), environmentId: environmentId.optional() },
35712
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}/tables/${encodeURIComponent(a.table)}/columns`, { query: { environmentId: a.environmentId } })
35713
+ }),
35714
+ defineTool({
35715
+ name: "query_database",
35716
+ title: "Query database (read-only)",
35717
+ kind: "read",
35718
+ description: "Run a read-only SQL statement (SELECT, EXPLAIN, SHOW…) in a read-only transaction; up to 200 rows. Use $1, $2… placeholders with params. Statements that change data are refused: use execute_database_sql.",
35719
+ input: { databaseId, sql: exports_external.string().min(1).max(1e4), params: exports_external.array(exports_external.any()).max(100).optional(), environmentId: environmentId.optional() },
35720
+ run: (a, api2) => api2.api("POST", `/databases/${a.databaseId}/query`, { body: { sql: a.sql, params: a.params, environmentId: a.environmentId } })
35721
+ }),
35722
+ defineTool({
35723
+ name: "execute_database_sql",
35724
+ title: "Execute SQL (writes)",
35725
+ kind: "destructive",
35726
+ description: "Run a statement that changes data or schema (INSERT, UPDATE, DELETE, CREATE, ALTER, DROP…). Show the statement and the target environment to the user and get explicit confirmation first; prefer a staging branch for migrations.",
35727
+ input: { databaseId, sql: exports_external.string().min(1).max(1e4), params: exports_external.array(exports_external.any()).max(100).optional(), environmentId: environmentId.optional() },
35728
+ run: (a, api2) => api2.api("POST", `/databases/${a.databaseId}/query`, { body: { sql: a.sql, params: a.params, environmentId: a.environmentId, allowWrite: true } })
35729
+ })
35730
+ ];
35731
+
35558
35732
  // src/tools/general.ts
35559
- var API_PREFIXES = ["/containers/", "/vps", "/backup/", "/domains"];
35733
+ var API_PREFIXES = ["/containers/", "/databases", "/vps", "/backup/", "/domains"];
35560
35734
  var generalTools = [
35561
35735
  defineTool({
35562
35736
  name: "helipod_request",
35563
35737
  title: "Call any Helipod API endpoint",
35564
35738
  kind: "destructive",
35565
- description: "Call an endpoint that has no dedicated tool, as documented at https://docs.helipod.io. api=core for the VPS, Container and Domain APIs (paths like /vps/{id}/backup), api=email for the Email API (paths like /broadcasts). Prefer the dedicated tools; ask the user before any call that costs money or deletes something.",
35739
+ description: "Call an endpoint that has no dedicated tool, as documented at https://docs.helipod.io. api=core for the Container, Database, VPS and Domain APIs (paths like /vps/{id}/backup), api=email for the Email API (paths like /broadcasts). Prefer the dedicated tools; ask the user before any call that costs money or deletes something.",
35566
35740
  input: {
35567
35741
  api: exports_external.enum(["core", "email"]),
35568
35742
  method: exports_external.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
@@ -35584,16 +35758,17 @@ var generalTools = [
35584
35758
  ];
35585
35759
 
35586
35760
  // src/server.ts
35587
- var VERSION = "0.2.0";
35588
- var INSTRUCTIONS = `Helipod is a cloud platform (Indonesia): containers, VPS, domains, email and an AI gateway, billed from a prepaid wallet in Rupiah.
35761
+ var VERSION = "0.3.1";
35762
+ 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.
35589
35763
  - Start with list_projects to find projects and environment ids.
35590
- - Anything that costs money (create/deploy/scale services, create/renew/upgrade VPS, register/renew/transfer domains): run the dryRun or price lookup first, tell the user the cost in IDR and wait for an explicit yes.
35591
- - Destructive tools (delete, terminate, reinstall, rollback, replace DNS): confirm with the user and name what will be lost.
35764
+ - Anything that costs money (create/deploy/scale services, upgrade a database plan, create/renew/upgrade VPS, register/renew/transfer domains): run the dryRun or price lookup first, tell the user the cost in IDR and wait for an explicit yes.
35765
+ - Destructive tools (delete, terminate, reinstall, rollback, replace DNS, execute_database_sql): confirm with the user and name what will be lost.
35766
+ - Databases: get_database_connection returns a password; put it in a secret variable (DATABASE_URL) instead of code or chat.
35592
35767
  - Deploys and VPS actions run in the background: poll get_deployment / get_vps until they settle; on a failed deploy read get_deployment_logs.
35593
35768
  - Never print secrets (passwords, secret variable values) unless the user asks for them.
35594
35769
  - Errors come back as { status, code, error }: 401/403 means the key lacks access (keys choose products and projects in Settings → API keys), 402 means plan limit or low balance.
35595
35770
  Docs: https://docs.helipod.io`;
35596
- var ALL_TOOLS = [...containerTools, ...vpsTools, ...domainTools, ...emailTools, ...generalTools];
35771
+ var ALL_TOOLS = [...containerTools, ...databaseTools, ...vpsTools, ...domainTools, ...emailTools, ...generalTools];
35597
35772
  function createHelipodServer(config2) {
35598
35773
  const server = new McpServer({ name: "helipod", version: VERSION, title: "Helipod" }, { instructions: INSTRUCTIONS });
35599
35774
  const api2 = new HelipodClient({ userAgent: `helipod-mcp/${VERSION}`, ...config2 });
package/dist/stdio.js CHANGED
@@ -34872,7 +34872,11 @@ class HelipodClient {
34872
34872
  return this.request(this.emailApiUrl, this.cfg.emailApiKey, method, path, opts);
34873
34873
  }
34874
34874
  async request(base, key, method, path, opts) {
34875
- const url2 = new URL(base + (path.startsWith("/") ? path : `/${path}`));
34875
+ const raw = path.startsWith("/") ? path : `/${path}`;
34876
+ if (/(^|\/)\.\.?(\/|$)|[?#\\]|\/\/|%2e|%2f|%5c/i.test(raw)) {
34877
+ throw new HelipodError(400, "INVALID_PATH", "An id in this request contains characters that are not allowed.");
34878
+ }
34879
+ const url2 = new URL(base + raw);
34876
34880
  for (const [k, v] of Object.entries(opts.query ?? {}))
34877
34881
  if (v !== undefined && v !== null && v !== "")
34878
34882
  url2.searchParams.set(k, String(v));
@@ -35662,14 +35666,184 @@ var emailTools = [
35662
35666
  })
35663
35667
  ];
35664
35668
 
35669
+ // src/tools/databases.ts
35670
+ var databaseId = exports_external.string().uuid().describe("Database id (from list_databases)");
35671
+ var environmentId = exports_external.string().uuid().describe("Environment id (from list_projects). Omit for the primary (production) branch.");
35672
+ var dryRun2 = exports_external.boolean().optional().describe("true = only check and show the result or price; nothing changes");
35673
+ var databaseTools = [
35674
+ defineTool({
35675
+ name: "list_database_plans",
35676
+ title: "List database plans",
35677
+ kind: "read",
35678
+ description: "Managed Postgres plans: price (IDR), included compute hours and storage, maximum CU. Free is the default; paid plans are prepaid credits that never expire.",
35679
+ input: {},
35680
+ run: (_a3, api2) => api2.api("GET", "/databases/plans")
35681
+ }),
35682
+ defineTool({
35683
+ name: "list_databases",
35684
+ title: "List databases",
35685
+ kind: "read",
35686
+ description: "Managed Postgres databases in the projects this key can reach: status, plan and remaining credits, region, branches per environment.",
35687
+ input: {},
35688
+ run: (_a3, api2) => api2.api("GET", "/databases")
35689
+ }),
35690
+ defineTool({
35691
+ name: "get_database",
35692
+ title: "Get database",
35693
+ kind: "read",
35694
+ description: "One database with its plan, compute settings and branches.",
35695
+ input: { databaseId },
35696
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}`)
35697
+ }),
35698
+ defineTool({
35699
+ name: "create_database",
35700
+ title: "Create database",
35701
+ kind: "write",
35702
+ idempotent: true,
35703
+ description: "Create the managed Postgres database of a project (free plan; one database per project, one branch per environment). Returns the existing one if the project already has it.",
35704
+ input: { projectId: exports_external.string().uuid(), idempotencyKey: exports_external.string().max(128).optional() },
35705
+ run: (a, api2) => api2.api("POST", "/databases", { body: { projectId: a.projectId }, idempotencyKey: a.idempotencyKey })
35706
+ }),
35707
+ defineTool({
35708
+ name: "delete_database",
35709
+ title: "Delete database",
35710
+ kind: "destructive",
35711
+ description: "Delete a database with ALL its data and branches. Irreversible. Only after the user confirms; confirm must be the database name.",
35712
+ input: { databaseId, confirm: exports_external.string().describe("The database name, exactly") },
35713
+ run: (a, api2) => api2.api("DELETE", `/databases/${a.databaseId}`, { body: { confirm: a.confirm } })
35714
+ }),
35715
+ defineTool({
35716
+ name: "get_database_connection",
35717
+ title: "Get database connection",
35718
+ kind: "read",
35719
+ description: "Connection string (postgresql://…, pooled, SSL) and its parts for an environment's branch. Contains the password: store it as a secret variable (e.g. DATABASE_URL via set_variables secrets), never print it unless asked.",
35720
+ input: { databaseId, environmentId: environmentId.optional() },
35721
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}/connection`, { query: { environmentId: a.environmentId } })
35722
+ }),
35723
+ defineTool({
35724
+ name: "list_database_branches",
35725
+ title: "List database branches",
35726
+ kind: "read",
35727
+ description: "Branches of a database, one per environment (production is the primary).",
35728
+ input: { databaseId },
35729
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}/branches`)
35730
+ }),
35731
+ defineTool({
35732
+ name: "create_database_branch",
35733
+ title: "Create database branch",
35734
+ kind: "write",
35735
+ idempotent: true,
35736
+ description: "Give an environment (e.g. staging) its own branch: a copy of the source environment's data at this moment (the primary when omitted).",
35737
+ input: { databaseId, environmentId: exports_external.string().uuid(), sourceEnvironmentId: exports_external.string().uuid().optional() },
35738
+ run: (a, api2) => api2.api("POST", `/databases/${a.databaseId}/branches`, { body: { environmentId: a.environmentId, sourceEnvironmentId: a.sourceEnvironmentId } })
35739
+ }),
35740
+ defineTool({
35741
+ name: "delete_database_branch",
35742
+ title: "Delete database branch",
35743
+ kind: "destructive",
35744
+ description: "Delete an environment's branch and its data. The primary (production) branch cannot be deleted. Confirm with the user.",
35745
+ input: { databaseId, environmentId: exports_external.string().uuid() },
35746
+ run: (a, api2) => api2.api("DELETE", `/databases/${a.databaseId}/branches/${a.environmentId}`)
35747
+ }),
35748
+ defineTool({
35749
+ name: "get_database_settings",
35750
+ title: "Get database compute settings",
35751
+ kind: "read",
35752
+ description: "Compute settings (paid plans): minCu (floor), maxCu (autoscaling ceiling), suspendTimeoutSeconds (idle time before scaling to zero), the plan's maximum CU.",
35753
+ input: { databaseId },
35754
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}/settings`)
35755
+ }),
35756
+ defineTool({
35757
+ name: "update_database_settings",
35758
+ title: "Update database compute settings",
35759
+ kind: "write",
35760
+ description: "Change compute for every branch (paid plans). A minCu above zero keeps compute running and uses credits while idle: run with dryRun first and show idleComputeHoursPerMonth.",
35761
+ input: {
35762
+ databaseId,
35763
+ minCu: exports_external.number().min(0.25).max(10).optional(),
35764
+ maxCu: exports_external.number().min(0.25).max(10).optional(),
35765
+ suspendTimeoutSeconds: exports_external.number().int().min(0).max(604800).optional().describe("0 = never suspend, or 60-604800"),
35766
+ dryRun: dryRun2
35767
+ },
35768
+ run: (a, api2) => api2.api("PATCH", `/databases/${a.databaseId}/settings`, {
35769
+ body: { minCu: a.minCu, maxCu: a.maxCu, suspendTimeoutSeconds: a.suspendTimeoutSeconds },
35770
+ query: { dryRun: a.dryRun ? "true" : undefined }
35771
+ })
35772
+ }),
35773
+ defineTool({
35774
+ name: "upgrade_database",
35775
+ title: "Upgrade database plan / top up",
35776
+ kind: "write",
35777
+ description: "Buy a paid plan or top up credits. COSTS MONEY from the wallet: call with dryRun first, show price and wallet balance after, and get the user's explicit OK.",
35778
+ input: { databaseId, plan: exports_external.string().describe("Plan code (e.g. STARTER) or id from list_database_plans"), dryRun: dryRun2, idempotencyKey: exports_external.string().max(128).optional() },
35779
+ run: (a, api2) => api2.api("POST", `/databases/${a.databaseId}/upgrade`, { body: { plan: a.plan }, query: { dryRun: a.dryRun ? "true" : undefined }, idempotencyKey: a.idempotencyKey })
35780
+ }),
35781
+ defineTool({
35782
+ name: "suspend_database",
35783
+ title: "Suspend database",
35784
+ kind: "destructive",
35785
+ description: "Stop the database's compute (connections fail until resumed). Confirm with the user.",
35786
+ input: { databaseId },
35787
+ run: (a, api2) => api2.api("POST", `/databases/${a.databaseId}/suspend`)
35788
+ }),
35789
+ defineTool({
35790
+ name: "resume_database",
35791
+ title: "Resume database",
35792
+ kind: "write",
35793
+ description: "Start a suspended database again.",
35794
+ input: { databaseId },
35795
+ run: (a, api2) => api2.api("POST", `/databases/${a.databaseId}/resume`)
35796
+ }),
35797
+ defineTool({
35798
+ name: "get_database_usage",
35799
+ title: "Database usage",
35800
+ kind: "read",
35801
+ description: "This period's compute (CU-hours), storage, history and network, plus the plan's remaining credits.",
35802
+ input: { databaseId },
35803
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}/usage`)
35804
+ }),
35805
+ defineTool({
35806
+ name: "list_database_tables",
35807
+ title: "List database tables",
35808
+ kind: "read",
35809
+ description: "Tables and views in the public schema of an environment's branch.",
35810
+ input: { databaseId, environmentId: environmentId.optional() },
35811
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}/tables`, { query: { environmentId: a.environmentId } })
35812
+ }),
35813
+ defineTool({
35814
+ name: "describe_database_table",
35815
+ title: "Describe database table",
35816
+ kind: "read",
35817
+ description: "Columns of a table (name, type, nullable, default).",
35818
+ input: { databaseId, table: exports_external.string().min(1).max(63), environmentId: environmentId.optional() },
35819
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}/tables/${encodeURIComponent(a.table)}/columns`, { query: { environmentId: a.environmentId } })
35820
+ }),
35821
+ defineTool({
35822
+ name: "query_database",
35823
+ title: "Query database (read-only)",
35824
+ kind: "read",
35825
+ description: "Run a read-only SQL statement (SELECT, EXPLAIN, SHOW…) in a read-only transaction; up to 200 rows. Use $1, $2… placeholders with params. Statements that change data are refused: use execute_database_sql.",
35826
+ input: { databaseId, sql: exports_external.string().min(1).max(1e4), params: exports_external.array(exports_external.any()).max(100).optional(), environmentId: environmentId.optional() },
35827
+ run: (a, api2) => api2.api("POST", `/databases/${a.databaseId}/query`, { body: { sql: a.sql, params: a.params, environmentId: a.environmentId } })
35828
+ }),
35829
+ defineTool({
35830
+ name: "execute_database_sql",
35831
+ title: "Execute SQL (writes)",
35832
+ kind: "destructive",
35833
+ description: "Run a statement that changes data or schema (INSERT, UPDATE, DELETE, CREATE, ALTER, DROP…). Show the statement and the target environment to the user and get explicit confirmation first; prefer a staging branch for migrations.",
35834
+ input: { databaseId, sql: exports_external.string().min(1).max(1e4), params: exports_external.array(exports_external.any()).max(100).optional(), environmentId: environmentId.optional() },
35835
+ run: (a, api2) => api2.api("POST", `/databases/${a.databaseId}/query`, { body: { sql: a.sql, params: a.params, environmentId: a.environmentId, allowWrite: true } })
35836
+ })
35837
+ ];
35838
+
35665
35839
  // src/tools/general.ts
35666
- var API_PREFIXES = ["/containers/", "/vps", "/backup/", "/domains"];
35840
+ var API_PREFIXES = ["/containers/", "/databases", "/vps", "/backup/", "/domains"];
35667
35841
  var generalTools = [
35668
35842
  defineTool({
35669
35843
  name: "helipod_request",
35670
35844
  title: "Call any Helipod API endpoint",
35671
35845
  kind: "destructive",
35672
- description: "Call an endpoint that has no dedicated tool, as documented at https://docs.helipod.io. api=core for the VPS, Container and Domain APIs (paths like /vps/{id}/backup), api=email for the Email API (paths like /broadcasts). Prefer the dedicated tools; ask the user before any call that costs money or deletes something.",
35846
+ description: "Call an endpoint that has no dedicated tool, as documented at https://docs.helipod.io. api=core for the Container, Database, VPS and Domain APIs (paths like /vps/{id}/backup), api=email for the Email API (paths like /broadcasts). Prefer the dedicated tools; ask the user before any call that costs money or deletes something.",
35673
35847
  input: {
35674
35848
  api: exports_external.enum(["core", "email"]),
35675
35849
  method: exports_external.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
@@ -35691,16 +35865,17 @@ var generalTools = [
35691
35865
  ];
35692
35866
 
35693
35867
  // src/server.ts
35694
- var VERSION = "0.2.0";
35695
- var INSTRUCTIONS = `Helipod is a cloud platform (Indonesia): containers, VPS, domains, email and an AI gateway, billed from a prepaid wallet in Rupiah.
35868
+ var VERSION = "0.3.1";
35869
+ 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.
35696
35870
  - Start with list_projects to find projects and environment ids.
35697
- - Anything that costs money (create/deploy/scale services, create/renew/upgrade VPS, register/renew/transfer domains): run the dryRun or price lookup first, tell the user the cost in IDR and wait for an explicit yes.
35698
- - Destructive tools (delete, terminate, reinstall, rollback, replace DNS): confirm with the user and name what will be lost.
35871
+ - Anything that costs money (create/deploy/scale services, upgrade a database plan, create/renew/upgrade VPS, register/renew/transfer domains): run the dryRun or price lookup first, tell the user the cost in IDR and wait for an explicit yes.
35872
+ - Destructive tools (delete, terminate, reinstall, rollback, replace DNS, execute_database_sql): confirm with the user and name what will be lost.
35873
+ - Databases: get_database_connection returns a password; put it in a secret variable (DATABASE_URL) instead of code or chat.
35699
35874
  - Deploys and VPS actions run in the background: poll get_deployment / get_vps until they settle; on a failed deploy read get_deployment_logs.
35700
35875
  - Never print secrets (passwords, secret variable values) unless the user asks for them.
35701
35876
  - Errors come back as { status, code, error }: 401/403 means the key lacks access (keys choose products and projects in Settings → API keys), 402 means plan limit or low balance.
35702
35877
  Docs: https://docs.helipod.io`;
35703
- var ALL_TOOLS = [...containerTools, ...vpsTools, ...domainTools, ...emailTools, ...generalTools];
35878
+ var ALL_TOOLS = [...containerTools, ...databaseTools, ...vpsTools, ...domainTools, ...emailTools, ...generalTools];
35704
35879
  function createHelipodServer(config2) {
35705
35880
  const server = new McpServer({ name: "helipod", version: VERSION, title: "Helipod" }, { instructions: INSTRUCTIONS });
35706
35881
  const api2 = new HelipodClient({ userAgent: `helipod-mcp/${VERSION}`, ...config2 });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "helipod-mcp",
3
- "version": "0.2.0",
4
- "description": "MCP server for Helipod: containers, VPS, domains and email from Claude Code, Codex, Cursor and other AI tools.",
3
+ "version": "0.3.1",
4
+ "description": "MCP server for Helipod: containers, databases, VPS, domains and email from Claude Code, Codex, Cursor and other AI tools.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "helipod-mcp": "dist/stdio.js"
@@ -5,13 +5,13 @@ description: Use when the user mentions Helipod (helipod.io) or wants to work wi
5
5
 
6
6
  # Helipod
7
7
 
8
- Helipod is a cloud platform in Indonesia: containers (deploy apps from Git, images or Dockerfiles), VPS, domain registration and DNS, email sending, and an AI gateway. Everything is billed from a prepaid wallet in Rupiah (IDR).
8
+ Helipod is a cloud platform in Indonesia: containers (deploy apps from Git, images or Dockerfiles), managed Postgres databases, VPS, domain registration and DNS, email sending, and an AI gateway. Everything is billed from a prepaid wallet in Rupiah (IDR).
9
9
 
10
10
  ## Access
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
- - VPS, Container, Domain APIs: `https://api.helipod.io/api` (`/containers/...`, `/vps/...`, `/domains/...`)
14
+ - Container, Database, VPS, Domain APIs: `https://api.helipod.io/api` (`/containers/...`, `/databases/...`, `/vps/...`, `/domains/...`)
15
15
  - Email API: `https://mail.helipod.io/api/email` with `$HELIPOD_EMAIL_API_KEY` (a separate email 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,7 +29,8 @@ 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 or database, env vars, domains, scaling, logs | `helipod-deploy` |
32
+ | Deploy an app, env vars, domains, scaling, logs | `helipod-deploy` |
33
+ | Managed Postgres: create, connect, branches, SQL, plans | `helipod-databases` |
33
34
  | Servers with root access | `helipod-vps` |
34
35
  | Buy a domain, DNS, point a domain at an app | `helipod-domains` |
35
36
  | Send email from an app | `helipod-email` |
@@ -0,0 +1,37 @@
1
+ ---
2
+ name: helipod-databases
3
+ description: Create and use a managed Postgres database on Helipod - activate it for a project, connect an app through a secret DATABASE_URL, give each environment its own branch, inspect tables, run migrations safely, scale and upgrade plans. Use when the user needs a Postgres database for a Helipod project or wants to query or change one.
4
+ ---
5
+
6
+ # Helipod managed Postgres
7
+
8
+ Follow the `helipod` skill rules (price first, confirm destructive actions, never print secrets). The key needs the **Database API** product.
9
+
10
+ ## Create and connect
11
+
12
+ 1. `list_projects` → the project. One database per project; each environment gets its own branch (production is the primary).
13
+ 2. `create_database` with the projectId (free plan; returns the existing database if there is one).
14
+ 3. `get_database_connection` (add `environmentId` for a non-production environment) → `uri`. Never paste it into code or chat.
15
+ 4. Give it to the app as a secret variable: `set_variables` on the service with `secrets: { "DATABASE_URL": "<uri>" }`, then `deploy_service`. The URI is pooled and requires SSL (already in the string).
16
+
17
+ ## Environments
18
+
19
+ - `create_database_branch` gives staging (or any environment) its own copy of production's data at that moment. Then `get_database_connection` with that environmentId.
20
+ - Run migrations on staging first, check, then production.
21
+ - `delete_database_branch` removes a non-primary branch and its data (confirm first).
22
+
23
+ ## Inspect and change data
24
+
25
+ - `list_database_tables`, `describe_database_table`, `query_database` (read-only, up to 200 rows, `$1` placeholders with `params`).
26
+ - `execute_database_sql` for INSERT/UPDATE/DELETE/CREATE/ALTER/DROP: show the exact statement and the environment, wait for a clear yes. Prefer the app's own migration tool for schema changes when the project has one.
27
+
28
+ ## Plans, credits, scaling
29
+
30
+ - Free plan: fixed small limits. Paid plans (`list_database_plans`): prepaid compute hours and storage that never expire; when credits run out the database is suspended until topped up.
31
+ - `upgrade_database` with `dryRun: true` first → show price and wallet balance after → only then without dryRun.
32
+ - `get_database_usage` shows what is used and what is left.
33
+ - Paid plans: `update_database_settings` (dryRun first). A `minCu` above 0 keeps compute warm but uses credits even when idle; `maxCu` caps autoscaling; `suspendTimeoutSeconds` is the idle time before scaling to zero.
34
+
35
+ ## Errors
36
+
37
+ `PLAN_REQUIRED` (settings need a paid plan), `INSUFFICIENT_BALANCE` (top up the wallet), `ENVIRONMENT_BRANCH_MISSING` (create the branch first), `WRITE_NOT_ALLOWED` (use execute_database_sql), `SQL_ERROR` (the database's own message).