helipod-mcp 0.2.0 → 0.3.0

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
@@ -36410,14 +36410,184 @@ var emailTools = [
36410
36410
  })
36411
36411
  ];
36412
36412
 
36413
+ // src/tools/databases.ts
36414
+ var databaseId = exports_external.string().uuid().describe("Database id (from list_databases)");
36415
+ var environmentId = exports_external.string().uuid().describe("Environment id (from list_projects). Omit for the primary (production) branch.");
36416
+ var dryRun2 = exports_external.boolean().optional().describe("true = only check and show the result or price; nothing changes");
36417
+ var databaseTools = [
36418
+ defineTool({
36419
+ name: "list_database_plans",
36420
+ title: "List database plans",
36421
+ kind: "read",
36422
+ 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.",
36423
+ input: {},
36424
+ run: (_a3, api2) => api2.api("GET", "/databases/plans")
36425
+ }),
36426
+ defineTool({
36427
+ name: "list_databases",
36428
+ title: "List databases",
36429
+ kind: "read",
36430
+ description: "Managed Postgres databases in the projects this key can reach: status, plan and remaining credits, region, branches per environment.",
36431
+ input: {},
36432
+ run: (_a3, api2) => api2.api("GET", "/databases")
36433
+ }),
36434
+ defineTool({
36435
+ name: "get_database",
36436
+ title: "Get database",
36437
+ kind: "read",
36438
+ description: "One database with its plan, compute settings and branches.",
36439
+ input: { databaseId },
36440
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}`)
36441
+ }),
36442
+ defineTool({
36443
+ name: "create_database",
36444
+ title: "Create database",
36445
+ kind: "write",
36446
+ idempotent: true,
36447
+ 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.",
36448
+ input: { projectId: exports_external.string().uuid(), idempotencyKey: exports_external.string().max(128).optional() },
36449
+ run: (a, api2) => api2.api("POST", "/databases", { body: { projectId: a.projectId }, idempotencyKey: a.idempotencyKey })
36450
+ }),
36451
+ defineTool({
36452
+ name: "delete_database",
36453
+ title: "Delete database",
36454
+ kind: "destructive",
36455
+ description: "Delete a database with ALL its data and branches. Irreversible. Only after the user confirms; confirm must be the database name.",
36456
+ input: { databaseId, confirm: exports_external.string().describe("The database name, exactly") },
36457
+ run: (a, api2) => api2.api("DELETE", `/databases/${a.databaseId}`, { body: { confirm: a.confirm } })
36458
+ }),
36459
+ defineTool({
36460
+ name: "get_database_connection",
36461
+ title: "Get database connection",
36462
+ kind: "read",
36463
+ 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.",
36464
+ input: { databaseId, environmentId: environmentId.optional() },
36465
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}/connection`, { query: { environmentId: a.environmentId } })
36466
+ }),
36467
+ defineTool({
36468
+ name: "list_database_branches",
36469
+ title: "List database branches",
36470
+ kind: "read",
36471
+ description: "Branches of a database, one per environment (production is the primary).",
36472
+ input: { databaseId },
36473
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}/branches`)
36474
+ }),
36475
+ defineTool({
36476
+ name: "create_database_branch",
36477
+ title: "Create database branch",
36478
+ kind: "write",
36479
+ idempotent: true,
36480
+ 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).",
36481
+ input: { databaseId, environmentId: exports_external.string().uuid(), sourceEnvironmentId: exports_external.string().uuid().optional() },
36482
+ run: (a, api2) => api2.api("POST", `/databases/${a.databaseId}/branches`, { body: { environmentId: a.environmentId, sourceEnvironmentId: a.sourceEnvironmentId } })
36483
+ }),
36484
+ defineTool({
36485
+ name: "delete_database_branch",
36486
+ title: "Delete database branch",
36487
+ kind: "destructive",
36488
+ description: "Delete an environment's branch and its data. The primary (production) branch cannot be deleted. Confirm with the user.",
36489
+ input: { databaseId, environmentId: exports_external.string().uuid() },
36490
+ run: (a, api2) => api2.api("DELETE", `/databases/${a.databaseId}/branches/${a.environmentId}`)
36491
+ }),
36492
+ defineTool({
36493
+ name: "get_database_settings",
36494
+ title: "Get database compute settings",
36495
+ kind: "read",
36496
+ description: "Compute settings (paid plans): minCu (floor), maxCu (autoscaling ceiling), suspendTimeoutSeconds (idle time before scaling to zero), the plan's maximum CU.",
36497
+ input: { databaseId },
36498
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}/settings`)
36499
+ }),
36500
+ defineTool({
36501
+ name: "update_database_settings",
36502
+ title: "Update database compute settings",
36503
+ kind: "write",
36504
+ 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.",
36505
+ input: {
36506
+ databaseId,
36507
+ minCu: exports_external.number().min(0.25).max(10).optional(),
36508
+ maxCu: exports_external.number().min(0.25).max(10).optional(),
36509
+ suspendTimeoutSeconds: exports_external.number().int().min(0).max(604800).optional().describe("0 = never suspend, or 60-604800"),
36510
+ dryRun: dryRun2
36511
+ },
36512
+ run: (a, api2) => api2.api("PATCH", `/databases/${a.databaseId}/settings`, {
36513
+ body: { minCu: a.minCu, maxCu: a.maxCu, suspendTimeoutSeconds: a.suspendTimeoutSeconds },
36514
+ query: { dryRun: a.dryRun ? "true" : undefined }
36515
+ })
36516
+ }),
36517
+ defineTool({
36518
+ name: "upgrade_database",
36519
+ title: "Upgrade database plan / top up",
36520
+ kind: "write",
36521
+ 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.",
36522
+ 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() },
36523
+ run: (a, api2) => api2.api("POST", `/databases/${a.databaseId}/upgrade`, { body: { plan: a.plan }, query: { dryRun: a.dryRun ? "true" : undefined }, idempotencyKey: a.idempotencyKey })
36524
+ }),
36525
+ defineTool({
36526
+ name: "suspend_database",
36527
+ title: "Suspend database",
36528
+ kind: "destructive",
36529
+ description: "Stop the database's compute (connections fail until resumed). Confirm with the user.",
36530
+ input: { databaseId },
36531
+ run: (a, api2) => api2.api("POST", `/databases/${a.databaseId}/suspend`)
36532
+ }),
36533
+ defineTool({
36534
+ name: "resume_database",
36535
+ title: "Resume database",
36536
+ kind: "write",
36537
+ description: "Start a suspended database again.",
36538
+ input: { databaseId },
36539
+ run: (a, api2) => api2.api("POST", `/databases/${a.databaseId}/resume`)
36540
+ }),
36541
+ defineTool({
36542
+ name: "get_database_usage",
36543
+ title: "Database usage",
36544
+ kind: "read",
36545
+ description: "This period's compute (CU-hours), storage, history and network, plus the plan's remaining credits.",
36546
+ input: { databaseId },
36547
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}/usage`)
36548
+ }),
36549
+ defineTool({
36550
+ name: "list_database_tables",
36551
+ title: "List database tables",
36552
+ kind: "read",
36553
+ description: "Tables and views in the public schema of an environment's branch.",
36554
+ input: { databaseId, environmentId: environmentId.optional() },
36555
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}/tables`, { query: { environmentId: a.environmentId } })
36556
+ }),
36557
+ defineTool({
36558
+ name: "describe_database_table",
36559
+ title: "Describe database table",
36560
+ kind: "read",
36561
+ description: "Columns of a table (name, type, nullable, default).",
36562
+ input: { databaseId, table: exports_external.string().min(1).max(63), environmentId: environmentId.optional() },
36563
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}/tables/${encodeURIComponent(a.table)}/columns`, { query: { environmentId: a.environmentId } })
36564
+ }),
36565
+ defineTool({
36566
+ name: "query_database",
36567
+ title: "Query database (read-only)",
36568
+ kind: "read",
36569
+ 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.",
36570
+ input: { databaseId, sql: exports_external.string().min(1).max(1e4), params: exports_external.array(exports_external.any()).max(100).optional(), environmentId: environmentId.optional() },
36571
+ run: (a, api2) => api2.api("POST", `/databases/${a.databaseId}/query`, { body: { sql: a.sql, params: a.params, environmentId: a.environmentId } })
36572
+ }),
36573
+ defineTool({
36574
+ name: "execute_database_sql",
36575
+ title: "Execute SQL (writes)",
36576
+ kind: "destructive",
36577
+ 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.",
36578
+ input: { databaseId, sql: exports_external.string().min(1).max(1e4), params: exports_external.array(exports_external.any()).max(100).optional(), environmentId: environmentId.optional() },
36579
+ run: (a, api2) => api2.api("POST", `/databases/${a.databaseId}/query`, { body: { sql: a.sql, params: a.params, environmentId: a.environmentId, allowWrite: true } })
36580
+ })
36581
+ ];
36582
+
36413
36583
  // src/tools/general.ts
36414
- var API_PREFIXES = ["/containers/", "/vps", "/backup/", "/domains"];
36584
+ var API_PREFIXES = ["/containers/", "/databases", "/vps", "/backup/", "/domains"];
36415
36585
  var generalTools = [
36416
36586
  defineTool({
36417
36587
  name: "helipod_request",
36418
36588
  title: "Call any Helipod API endpoint",
36419
36589
  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.",
36590
+ 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
36591
  input: {
36422
36592
  api: exports_external.enum(["core", "email"]),
36423
36593
  method: exports_external.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
@@ -36439,16 +36609,17 @@ var generalTools = [
36439
36609
  ];
36440
36610
 
36441
36611
  // 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.
36612
+ var VERSION = "0.3.0";
36613
+ 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
36614
  - 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.
36615
+ - 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.
36616
+ - Destructive tools (delete, terminate, reinstall, rollback, replace DNS, execute_database_sql): confirm with the user and name what will be lost.
36617
+ - Databases: get_database_connection returns a password; put it in a secret variable (DATABASE_URL) instead of code or chat.
36447
36618
  - 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
36619
  - Never print secrets (passwords, secret variable values) unless the user asks for them.
36449
36620
  - 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
36621
  Docs: https://docs.helipod.io`;
36451
- var ALL_TOOLS = [...containerTools, ...vpsTools, ...domainTools, ...emailTools, ...generalTools];
36622
+ var ALL_TOOLS = [...containerTools, ...databaseTools, ...vpsTools, ...domainTools, ...emailTools, ...generalTools];
36452
36623
  function createHelipodServer(config2) {
36453
36624
  const server = new McpServer({ name: "helipod", version: VERSION, title: "Helipod" }, { instructions: INSTRUCTIONS });
36454
36625
  const api2 = new HelipodClient({ userAgent: `helipod-mcp/${VERSION}`, ...config2 });
package/dist/index.js CHANGED
@@ -35555,14 +35555,184 @@ var emailTools = [
35555
35555
  })
35556
35556
  ];
35557
35557
 
35558
+ // src/tools/databases.ts
35559
+ var databaseId = exports_external.string().uuid().describe("Database id (from list_databases)");
35560
+ var environmentId = exports_external.string().uuid().describe("Environment id (from list_projects). Omit for the primary (production) branch.");
35561
+ var dryRun2 = exports_external.boolean().optional().describe("true = only check and show the result or price; nothing changes");
35562
+ var databaseTools = [
35563
+ defineTool({
35564
+ name: "list_database_plans",
35565
+ title: "List database plans",
35566
+ kind: "read",
35567
+ 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.",
35568
+ input: {},
35569
+ run: (_a3, api2) => api2.api("GET", "/databases/plans")
35570
+ }),
35571
+ defineTool({
35572
+ name: "list_databases",
35573
+ title: "List databases",
35574
+ kind: "read",
35575
+ description: "Managed Postgres databases in the projects this key can reach: status, plan and remaining credits, region, branches per environment.",
35576
+ input: {},
35577
+ run: (_a3, api2) => api2.api("GET", "/databases")
35578
+ }),
35579
+ defineTool({
35580
+ name: "get_database",
35581
+ title: "Get database",
35582
+ kind: "read",
35583
+ description: "One database with its plan, compute settings and branches.",
35584
+ input: { databaseId },
35585
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}`)
35586
+ }),
35587
+ defineTool({
35588
+ name: "create_database",
35589
+ title: "Create database",
35590
+ kind: "write",
35591
+ idempotent: true,
35592
+ 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.",
35593
+ input: { projectId: exports_external.string().uuid(), idempotencyKey: exports_external.string().max(128).optional() },
35594
+ run: (a, api2) => api2.api("POST", "/databases", { body: { projectId: a.projectId }, idempotencyKey: a.idempotencyKey })
35595
+ }),
35596
+ defineTool({
35597
+ name: "delete_database",
35598
+ title: "Delete database",
35599
+ kind: "destructive",
35600
+ description: "Delete a database with ALL its data and branches. Irreversible. Only after the user confirms; confirm must be the database name.",
35601
+ input: { databaseId, confirm: exports_external.string().describe("The database name, exactly") },
35602
+ run: (a, api2) => api2.api("DELETE", `/databases/${a.databaseId}`, { body: { confirm: a.confirm } })
35603
+ }),
35604
+ defineTool({
35605
+ name: "get_database_connection",
35606
+ title: "Get database connection",
35607
+ kind: "read",
35608
+ 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.",
35609
+ input: { databaseId, environmentId: environmentId.optional() },
35610
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}/connection`, { query: { environmentId: a.environmentId } })
35611
+ }),
35612
+ defineTool({
35613
+ name: "list_database_branches",
35614
+ title: "List database branches",
35615
+ kind: "read",
35616
+ description: "Branches of a database, one per environment (production is the primary).",
35617
+ input: { databaseId },
35618
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}/branches`)
35619
+ }),
35620
+ defineTool({
35621
+ name: "create_database_branch",
35622
+ title: "Create database branch",
35623
+ kind: "write",
35624
+ idempotent: true,
35625
+ 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).",
35626
+ input: { databaseId, environmentId: exports_external.string().uuid(), sourceEnvironmentId: exports_external.string().uuid().optional() },
35627
+ run: (a, api2) => api2.api("POST", `/databases/${a.databaseId}/branches`, { body: { environmentId: a.environmentId, sourceEnvironmentId: a.sourceEnvironmentId } })
35628
+ }),
35629
+ defineTool({
35630
+ name: "delete_database_branch",
35631
+ title: "Delete database branch",
35632
+ kind: "destructive",
35633
+ description: "Delete an environment's branch and its data. The primary (production) branch cannot be deleted. Confirm with the user.",
35634
+ input: { databaseId, environmentId: exports_external.string().uuid() },
35635
+ run: (a, api2) => api2.api("DELETE", `/databases/${a.databaseId}/branches/${a.environmentId}`)
35636
+ }),
35637
+ defineTool({
35638
+ name: "get_database_settings",
35639
+ title: "Get database compute settings",
35640
+ kind: "read",
35641
+ description: "Compute settings (paid plans): minCu (floor), maxCu (autoscaling ceiling), suspendTimeoutSeconds (idle time before scaling to zero), the plan's maximum CU.",
35642
+ input: { databaseId },
35643
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}/settings`)
35644
+ }),
35645
+ defineTool({
35646
+ name: "update_database_settings",
35647
+ title: "Update database compute settings",
35648
+ kind: "write",
35649
+ 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.",
35650
+ input: {
35651
+ databaseId,
35652
+ minCu: exports_external.number().min(0.25).max(10).optional(),
35653
+ maxCu: exports_external.number().min(0.25).max(10).optional(),
35654
+ suspendTimeoutSeconds: exports_external.number().int().min(0).max(604800).optional().describe("0 = never suspend, or 60-604800"),
35655
+ dryRun: dryRun2
35656
+ },
35657
+ run: (a, api2) => api2.api("PATCH", `/databases/${a.databaseId}/settings`, {
35658
+ body: { minCu: a.minCu, maxCu: a.maxCu, suspendTimeoutSeconds: a.suspendTimeoutSeconds },
35659
+ query: { dryRun: a.dryRun ? "true" : undefined }
35660
+ })
35661
+ }),
35662
+ defineTool({
35663
+ name: "upgrade_database",
35664
+ title: "Upgrade database plan / top up",
35665
+ kind: "write",
35666
+ 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.",
35667
+ 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() },
35668
+ run: (a, api2) => api2.api("POST", `/databases/${a.databaseId}/upgrade`, { body: { plan: a.plan }, query: { dryRun: a.dryRun ? "true" : undefined }, idempotencyKey: a.idempotencyKey })
35669
+ }),
35670
+ defineTool({
35671
+ name: "suspend_database",
35672
+ title: "Suspend database",
35673
+ kind: "destructive",
35674
+ description: "Stop the database's compute (connections fail until resumed). Confirm with the user.",
35675
+ input: { databaseId },
35676
+ run: (a, api2) => api2.api("POST", `/databases/${a.databaseId}/suspend`)
35677
+ }),
35678
+ defineTool({
35679
+ name: "resume_database",
35680
+ title: "Resume database",
35681
+ kind: "write",
35682
+ description: "Start a suspended database again.",
35683
+ input: { databaseId },
35684
+ run: (a, api2) => api2.api("POST", `/databases/${a.databaseId}/resume`)
35685
+ }),
35686
+ defineTool({
35687
+ name: "get_database_usage",
35688
+ title: "Database usage",
35689
+ kind: "read",
35690
+ description: "This period's compute (CU-hours), storage, history and network, plus the plan's remaining credits.",
35691
+ input: { databaseId },
35692
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}/usage`)
35693
+ }),
35694
+ defineTool({
35695
+ name: "list_database_tables",
35696
+ title: "List database tables",
35697
+ kind: "read",
35698
+ description: "Tables and views in the public schema of an environment's branch.",
35699
+ input: { databaseId, environmentId: environmentId.optional() },
35700
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}/tables`, { query: { environmentId: a.environmentId } })
35701
+ }),
35702
+ defineTool({
35703
+ name: "describe_database_table",
35704
+ title: "Describe database table",
35705
+ kind: "read",
35706
+ description: "Columns of a table (name, type, nullable, default).",
35707
+ input: { databaseId, table: exports_external.string().min(1).max(63), environmentId: environmentId.optional() },
35708
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}/tables/${encodeURIComponent(a.table)}/columns`, { query: { environmentId: a.environmentId } })
35709
+ }),
35710
+ defineTool({
35711
+ name: "query_database",
35712
+ title: "Query database (read-only)",
35713
+ kind: "read",
35714
+ 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.",
35715
+ input: { databaseId, sql: exports_external.string().min(1).max(1e4), params: exports_external.array(exports_external.any()).max(100).optional(), environmentId: environmentId.optional() },
35716
+ run: (a, api2) => api2.api("POST", `/databases/${a.databaseId}/query`, { body: { sql: a.sql, params: a.params, environmentId: a.environmentId } })
35717
+ }),
35718
+ defineTool({
35719
+ name: "execute_database_sql",
35720
+ title: "Execute SQL (writes)",
35721
+ kind: "destructive",
35722
+ 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.",
35723
+ input: { databaseId, sql: exports_external.string().min(1).max(1e4), params: exports_external.array(exports_external.any()).max(100).optional(), environmentId: environmentId.optional() },
35724
+ run: (a, api2) => api2.api("POST", `/databases/${a.databaseId}/query`, { body: { sql: a.sql, params: a.params, environmentId: a.environmentId, allowWrite: true } })
35725
+ })
35726
+ ];
35727
+
35558
35728
  // src/tools/general.ts
35559
- var API_PREFIXES = ["/containers/", "/vps", "/backup/", "/domains"];
35729
+ var API_PREFIXES = ["/containers/", "/databases", "/vps", "/backup/", "/domains"];
35560
35730
  var generalTools = [
35561
35731
  defineTool({
35562
35732
  name: "helipod_request",
35563
35733
  title: "Call any Helipod API endpoint",
35564
35734
  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.",
35735
+ 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
35736
  input: {
35567
35737
  api: exports_external.enum(["core", "email"]),
35568
35738
  method: exports_external.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
@@ -35584,16 +35754,17 @@ var generalTools = [
35584
35754
  ];
35585
35755
 
35586
35756
  // 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.
35757
+ var VERSION = "0.3.0";
35758
+ 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
35759
  - 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.
35760
+ - 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.
35761
+ - Destructive tools (delete, terminate, reinstall, rollback, replace DNS, execute_database_sql): confirm with the user and name what will be lost.
35762
+ - Databases: get_database_connection returns a password; put it in a secret variable (DATABASE_URL) instead of code or chat.
35592
35763
  - 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
35764
  - Never print secrets (passwords, secret variable values) unless the user asks for them.
35594
35765
  - 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
35766
  Docs: https://docs.helipod.io`;
35596
- var ALL_TOOLS = [...containerTools, ...vpsTools, ...domainTools, ...emailTools, ...generalTools];
35767
+ var ALL_TOOLS = [...containerTools, ...databaseTools, ...vpsTools, ...domainTools, ...emailTools, ...generalTools];
35597
35768
  function createHelipodServer(config2) {
35598
35769
  const server = new McpServer({ name: "helipod", version: VERSION, title: "Helipod" }, { instructions: INSTRUCTIONS });
35599
35770
  const api2 = new HelipodClient({ userAgent: `helipod-mcp/${VERSION}`, ...config2 });
package/dist/stdio.js CHANGED
@@ -35662,14 +35662,184 @@ var emailTools = [
35662
35662
  })
35663
35663
  ];
35664
35664
 
35665
+ // src/tools/databases.ts
35666
+ var databaseId = exports_external.string().uuid().describe("Database id (from list_databases)");
35667
+ var environmentId = exports_external.string().uuid().describe("Environment id (from list_projects). Omit for the primary (production) branch.");
35668
+ var dryRun2 = exports_external.boolean().optional().describe("true = only check and show the result or price; nothing changes");
35669
+ var databaseTools = [
35670
+ defineTool({
35671
+ name: "list_database_plans",
35672
+ title: "List database plans",
35673
+ kind: "read",
35674
+ 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.",
35675
+ input: {},
35676
+ run: (_a3, api2) => api2.api("GET", "/databases/plans")
35677
+ }),
35678
+ defineTool({
35679
+ name: "list_databases",
35680
+ title: "List databases",
35681
+ kind: "read",
35682
+ description: "Managed Postgres databases in the projects this key can reach: status, plan and remaining credits, region, branches per environment.",
35683
+ input: {},
35684
+ run: (_a3, api2) => api2.api("GET", "/databases")
35685
+ }),
35686
+ defineTool({
35687
+ name: "get_database",
35688
+ title: "Get database",
35689
+ kind: "read",
35690
+ description: "One database with its plan, compute settings and branches.",
35691
+ input: { databaseId },
35692
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}`)
35693
+ }),
35694
+ defineTool({
35695
+ name: "create_database",
35696
+ title: "Create database",
35697
+ kind: "write",
35698
+ idempotent: true,
35699
+ 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.",
35700
+ input: { projectId: exports_external.string().uuid(), idempotencyKey: exports_external.string().max(128).optional() },
35701
+ run: (a, api2) => api2.api("POST", "/databases", { body: { projectId: a.projectId }, idempotencyKey: a.idempotencyKey })
35702
+ }),
35703
+ defineTool({
35704
+ name: "delete_database",
35705
+ title: "Delete database",
35706
+ kind: "destructive",
35707
+ description: "Delete a database with ALL its data and branches. Irreversible. Only after the user confirms; confirm must be the database name.",
35708
+ input: { databaseId, confirm: exports_external.string().describe("The database name, exactly") },
35709
+ run: (a, api2) => api2.api("DELETE", `/databases/${a.databaseId}`, { body: { confirm: a.confirm } })
35710
+ }),
35711
+ defineTool({
35712
+ name: "get_database_connection",
35713
+ title: "Get database connection",
35714
+ kind: "read",
35715
+ 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.",
35716
+ input: { databaseId, environmentId: environmentId.optional() },
35717
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}/connection`, { query: { environmentId: a.environmentId } })
35718
+ }),
35719
+ defineTool({
35720
+ name: "list_database_branches",
35721
+ title: "List database branches",
35722
+ kind: "read",
35723
+ description: "Branches of a database, one per environment (production is the primary).",
35724
+ input: { databaseId },
35725
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}/branches`)
35726
+ }),
35727
+ defineTool({
35728
+ name: "create_database_branch",
35729
+ title: "Create database branch",
35730
+ kind: "write",
35731
+ idempotent: true,
35732
+ 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).",
35733
+ input: { databaseId, environmentId: exports_external.string().uuid(), sourceEnvironmentId: exports_external.string().uuid().optional() },
35734
+ run: (a, api2) => api2.api("POST", `/databases/${a.databaseId}/branches`, { body: { environmentId: a.environmentId, sourceEnvironmentId: a.sourceEnvironmentId } })
35735
+ }),
35736
+ defineTool({
35737
+ name: "delete_database_branch",
35738
+ title: "Delete database branch",
35739
+ kind: "destructive",
35740
+ description: "Delete an environment's branch and its data. The primary (production) branch cannot be deleted. Confirm with the user.",
35741
+ input: { databaseId, environmentId: exports_external.string().uuid() },
35742
+ run: (a, api2) => api2.api("DELETE", `/databases/${a.databaseId}/branches/${a.environmentId}`)
35743
+ }),
35744
+ defineTool({
35745
+ name: "get_database_settings",
35746
+ title: "Get database compute settings",
35747
+ kind: "read",
35748
+ description: "Compute settings (paid plans): minCu (floor), maxCu (autoscaling ceiling), suspendTimeoutSeconds (idle time before scaling to zero), the plan's maximum CU.",
35749
+ input: { databaseId },
35750
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}/settings`)
35751
+ }),
35752
+ defineTool({
35753
+ name: "update_database_settings",
35754
+ title: "Update database compute settings",
35755
+ kind: "write",
35756
+ 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.",
35757
+ input: {
35758
+ databaseId,
35759
+ minCu: exports_external.number().min(0.25).max(10).optional(),
35760
+ maxCu: exports_external.number().min(0.25).max(10).optional(),
35761
+ suspendTimeoutSeconds: exports_external.number().int().min(0).max(604800).optional().describe("0 = never suspend, or 60-604800"),
35762
+ dryRun: dryRun2
35763
+ },
35764
+ run: (a, api2) => api2.api("PATCH", `/databases/${a.databaseId}/settings`, {
35765
+ body: { minCu: a.minCu, maxCu: a.maxCu, suspendTimeoutSeconds: a.suspendTimeoutSeconds },
35766
+ query: { dryRun: a.dryRun ? "true" : undefined }
35767
+ })
35768
+ }),
35769
+ defineTool({
35770
+ name: "upgrade_database",
35771
+ title: "Upgrade database plan / top up",
35772
+ kind: "write",
35773
+ 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.",
35774
+ 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() },
35775
+ run: (a, api2) => api2.api("POST", `/databases/${a.databaseId}/upgrade`, { body: { plan: a.plan }, query: { dryRun: a.dryRun ? "true" : undefined }, idempotencyKey: a.idempotencyKey })
35776
+ }),
35777
+ defineTool({
35778
+ name: "suspend_database",
35779
+ title: "Suspend database",
35780
+ kind: "destructive",
35781
+ description: "Stop the database's compute (connections fail until resumed). Confirm with the user.",
35782
+ input: { databaseId },
35783
+ run: (a, api2) => api2.api("POST", `/databases/${a.databaseId}/suspend`)
35784
+ }),
35785
+ defineTool({
35786
+ name: "resume_database",
35787
+ title: "Resume database",
35788
+ kind: "write",
35789
+ description: "Start a suspended database again.",
35790
+ input: { databaseId },
35791
+ run: (a, api2) => api2.api("POST", `/databases/${a.databaseId}/resume`)
35792
+ }),
35793
+ defineTool({
35794
+ name: "get_database_usage",
35795
+ title: "Database usage",
35796
+ kind: "read",
35797
+ description: "This period's compute (CU-hours), storage, history and network, plus the plan's remaining credits.",
35798
+ input: { databaseId },
35799
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}/usage`)
35800
+ }),
35801
+ defineTool({
35802
+ name: "list_database_tables",
35803
+ title: "List database tables",
35804
+ kind: "read",
35805
+ description: "Tables and views in the public schema of an environment's branch.",
35806
+ input: { databaseId, environmentId: environmentId.optional() },
35807
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}/tables`, { query: { environmentId: a.environmentId } })
35808
+ }),
35809
+ defineTool({
35810
+ name: "describe_database_table",
35811
+ title: "Describe database table",
35812
+ kind: "read",
35813
+ description: "Columns of a table (name, type, nullable, default).",
35814
+ input: { databaseId, table: exports_external.string().min(1).max(63), environmentId: environmentId.optional() },
35815
+ run: (a, api2) => api2.api("GET", `/databases/${a.databaseId}/tables/${encodeURIComponent(a.table)}/columns`, { query: { environmentId: a.environmentId } })
35816
+ }),
35817
+ defineTool({
35818
+ name: "query_database",
35819
+ title: "Query database (read-only)",
35820
+ kind: "read",
35821
+ 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.",
35822
+ input: { databaseId, sql: exports_external.string().min(1).max(1e4), params: exports_external.array(exports_external.any()).max(100).optional(), environmentId: environmentId.optional() },
35823
+ run: (a, api2) => api2.api("POST", `/databases/${a.databaseId}/query`, { body: { sql: a.sql, params: a.params, environmentId: a.environmentId } })
35824
+ }),
35825
+ defineTool({
35826
+ name: "execute_database_sql",
35827
+ title: "Execute SQL (writes)",
35828
+ kind: "destructive",
35829
+ 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.",
35830
+ input: { databaseId, sql: exports_external.string().min(1).max(1e4), params: exports_external.array(exports_external.any()).max(100).optional(), environmentId: environmentId.optional() },
35831
+ run: (a, api2) => api2.api("POST", `/databases/${a.databaseId}/query`, { body: { sql: a.sql, params: a.params, environmentId: a.environmentId, allowWrite: true } })
35832
+ })
35833
+ ];
35834
+
35665
35835
  // src/tools/general.ts
35666
- var API_PREFIXES = ["/containers/", "/vps", "/backup/", "/domains"];
35836
+ var API_PREFIXES = ["/containers/", "/databases", "/vps", "/backup/", "/domains"];
35667
35837
  var generalTools = [
35668
35838
  defineTool({
35669
35839
  name: "helipod_request",
35670
35840
  title: "Call any Helipod API endpoint",
35671
35841
  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.",
35842
+ 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
35843
  input: {
35674
35844
  api: exports_external.enum(["core", "email"]),
35675
35845
  method: exports_external.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
@@ -35691,16 +35861,17 @@ var generalTools = [
35691
35861
  ];
35692
35862
 
35693
35863
  // 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.
35864
+ var VERSION = "0.3.0";
35865
+ 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
35866
  - 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.
35867
+ - 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.
35868
+ - Destructive tools (delete, terminate, reinstall, rollback, replace DNS, execute_database_sql): confirm with the user and name what will be lost.
35869
+ - Databases: get_database_connection returns a password; put it in a secret variable (DATABASE_URL) instead of code or chat.
35699
35870
  - 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
35871
  - Never print secrets (passwords, secret variable values) unless the user asks for them.
35701
35872
  - 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
35873
  Docs: https://docs.helipod.io`;
35703
- var ALL_TOOLS = [...containerTools, ...vpsTools, ...domainTools, ...emailTools, ...generalTools];
35874
+ var ALL_TOOLS = [...containerTools, ...databaseTools, ...vpsTools, ...domainTools, ...emailTools, ...generalTools];
35704
35875
  function createHelipodServer(config2) {
35705
35876
  const server = new McpServer({ name: "helipod", version: VERSION, title: "Helipod" }, { instructions: INSTRUCTIONS });
35706
35877
  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.0",
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).