@selfhost.dev/mcp-server 0.3.0 → 0.4.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.
Files changed (44) hide show
  1. package/README.md +147 -24
  2. package/dist/auth.d.ts +1 -1
  3. package/dist/auth.js +3 -3
  4. package/dist/client.js +1 -1
  5. package/dist/config.js +1 -1
  6. package/dist/index.js +29 -4
  7. package/dist/index.js.map +1 -1
  8. package/dist/rate-limiter.js +1 -1
  9. package/dist/tools/billing.js +14 -14
  10. package/dist/tools/credentials.js +1 -1
  11. package/dist/tools/database-users.js +9 -9
  12. package/dist/tools/deployment-domains.d.ts +2 -0
  13. package/dist/tools/deployment-domains.js +128 -0
  14. package/dist/tools/deployment-domains.js.map +1 -0
  15. package/dist/tools/deployment-notifications.d.ts +2 -0
  16. package/dist/tools/deployment-notifications.js +74 -0
  17. package/dist/tools/deployment-notifications.js.map +1 -0
  18. package/dist/tools/deployments.d.ts +2 -0
  19. package/dist/tools/deployments.js +302 -0
  20. package/dist/tools/deployments.js.map +1 -0
  21. package/dist/tools/env-vars.d.ts +2 -0
  22. package/dist/tools/env-vars.js +121 -0
  23. package/dist/tools/env-vars.js.map +1 -0
  24. package/dist/tools/github.d.ts +2 -0
  25. package/dist/tools/github.js +106 -0
  26. package/dist/tools/github.js.map +1 -0
  27. package/dist/tools/hetzner.js +13 -13
  28. package/dist/tools/instances.js +11 -11
  29. package/dist/tools/pgbouncer.js +4 -4
  30. package/dist/tools/pitr.js +16 -16
  31. package/dist/tools/project-databases.d.ts +2 -0
  32. package/dist/tools/project-databases.js +169 -0
  33. package/dist/tools/project-databases.js.map +1 -0
  34. package/dist/tools/project-services.d.ts +2 -0
  35. package/dist/tools/project-services.js +93 -0
  36. package/dist/tools/project-services.js.map +1 -0
  37. package/dist/tools/projects.d.ts +2 -0
  38. package/dist/tools/projects.js +141 -0
  39. package/dist/tools/projects.js.map +1 -0
  40. package/dist/tools/scaling.js +13 -13
  41. package/dist/types/models.js +1 -1
  42. package/dist/types/tiers.js +31 -1
  43. package/dist/types/tiers.js.map +1 -1
  44. package/package.json +10 -3
@@ -0,0 +1,121 @@
1
+ import { z } from "zod";
2
+ import { apiRequest } from "../client.js";
3
+ import { session } from "../session.js";
4
+ /**
5
+ * Deployment environment variables. Stored encrypted at rest but returned in
6
+ * cleartext by the API (treat values as secrets).
7
+ *
8
+ * Endpoints (base /api/v1/platform/github_repo_deployments/:pid/env_vars):
9
+ * - GET /env_vars list { key, value }
10
+ * - PUT /env_vars FULL REPLACE - deletes keys not in the payload
11
+ * - PATCH /env_vars merge/upsert - never deletes
12
+ * - DELETE /env_vars/:key remove one key
13
+ *
14
+ * Saving env vars triggers a redeploy on active deployments, rate-limited to
15
+ * one per 60s (the response `redeploy_triggered` flag tells you if it fired).
16
+ */
17
+ const DEPLOYMENTS_BASE = "/api/v1/platform/github_repo_deployments";
18
+ const KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
19
+ const NO_ORG = "No active organization. Call `list_organizations` then `select_organization` first.";
20
+ function hasOrg() {
21
+ return session.getActiveOrgId() !== null;
22
+ }
23
+ /** Validate a { KEY: value } map against the backend rules. Returns an error string or null. */
24
+ function validateEnvVars(env) {
25
+ const keys = Object.keys(env);
26
+ if (keys.length === 0)
27
+ return "env_vars is empty - provide at least one { KEY: value } pair.";
28
+ for (const [k, v] of Object.entries(env)) {
29
+ if (!KEY_RE.test(k)) {
30
+ return `Invalid key "${k}": must match ^[A-Za-z_][A-Za-z0-9_]*$ (start with a letter or underscore).`;
31
+ }
32
+ if (v === undefined || v === null || v === "") {
33
+ return `Value for "${k}" is blank. Values must be non-empty - use delete_env_var to remove a variable.`;
34
+ }
35
+ }
36
+ return null;
37
+ }
38
+ export function registerEnvVarTools(server) {
39
+ server.tool("list_env_vars", "List a deployment's environment variables as { key, value } pairs. NOTE: values are returned in cleartext - handle as secrets. Call this before `set_env_vars` so you know the full current set (since `set_env_vars` replaces everything).", {
40
+ deployment_pid: z.string().min(1),
41
+ }, async ({ deployment_pid }) => {
42
+ if (!hasOrg())
43
+ return { content: [{ type: "text", text: NO_ORG }] };
44
+ const result = await apiRequest(`${DEPLOYMENTS_BASE}/${deployment_pid}/env_vars`, {
45
+ toolName: "list_env_vars",
46
+ });
47
+ if (!result.success) {
48
+ return { content: [{ type: "text", text: `Failed to list env vars (${result.statusCode}): ${result.message}` }] };
49
+ }
50
+ return { content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }] };
51
+ });
52
+ server.tool("set_env_vars", "REPLACE a deployment's ENTIRE set of environment variables with the provided map. ⚠️ Any existing key NOT included here is DELETED. To add or edit a few variables without removing the rest, use `merge_env_vars` instead - or call `list_env_vars` first and pass the full merged set here. Keys must match ^[A-Za-z_][A-Za-z0-9_]*$; values must be non-empty. Saving may trigger a rate-limited redeploy.", {
53
+ deployment_pid: z.string().min(1),
54
+ env_vars: z.record(z.string()).describe("The COMPLETE desired set { KEY: value }; replaces all existing vars"),
55
+ }, async ({ deployment_pid, env_vars }) => {
56
+ if (!hasOrg())
57
+ return { content: [{ type: "text", text: NO_ORG }] };
58
+ const err = validateEnvVars(env_vars);
59
+ if (err)
60
+ return { content: [{ type: "text", text: err }] };
61
+ const result = await apiRequest(`${DEPLOYMENTS_BASE}/${deployment_pid}/env_vars`, {
62
+ method: "PUT",
63
+ body: { env_vars },
64
+ toolName: "set_env_vars",
65
+ });
66
+ if (!result.success) {
67
+ return { content: [{ type: "text", text: `Failed to set env vars (${result.statusCode}): ${result.message}` }] };
68
+ }
69
+ return {
70
+ content: [
71
+ { type: "text", text: "Environment variables replaced. If `redeploy_triggered` is false the change was throttled and applies within ~60s." },
72
+ { type: "text", text: JSON.stringify(result.data, null, 2) },
73
+ ],
74
+ };
75
+ });
76
+ server.tool("merge_env_vars", "Add or update environment variables WITHOUT deleting any existing ones (upsert/merge). This is the safe way to set a few variables. Keys must match ^[A-Za-z_][A-Za-z0-9_]*$; values must be non-empty. To remove a variable use `delete_env_var`. Saving may trigger a rate-limited redeploy.", {
77
+ deployment_pid: z.string().min(1),
78
+ env_vars: z.record(z.string()).describe("The variables to add/update { KEY: value }; existing keys are preserved"),
79
+ }, async ({ deployment_pid, env_vars }) => {
80
+ if (!hasOrg())
81
+ return { content: [{ type: "text", text: NO_ORG }] };
82
+ const err = validateEnvVars(env_vars);
83
+ if (err)
84
+ return { content: [{ type: "text", text: err }] };
85
+ const result = await apiRequest(`${DEPLOYMENTS_BASE}/${deployment_pid}/env_vars`, {
86
+ method: "PATCH",
87
+ body: { env_vars },
88
+ toolName: "merge_env_vars",
89
+ });
90
+ if (!result.success) {
91
+ return { content: [{ type: "text", text: `Failed to merge env vars (${result.statusCode}): ${result.message}` }] };
92
+ }
93
+ return {
94
+ content: [
95
+ { type: "text", text: "Environment variables merged. If `redeploy_triggered` is false the change was throttled and applies within ~60s." },
96
+ { type: "text", text: JSON.stringify(result.data, null, 2) },
97
+ ],
98
+ };
99
+ });
100
+ server.tool("delete_env_var", "Delete a single environment variable by key. Returns 404 if the key doesn't exist. Saving may trigger a rate-limited redeploy.", {
101
+ deployment_pid: z.string().min(1),
102
+ key: z.string().min(1).describe("The variable key to remove"),
103
+ }, async ({ deployment_pid, key }) => {
104
+ if (!hasOrg())
105
+ return { content: [{ type: "text", text: NO_ORG }] };
106
+ const result = await apiRequest(`${DEPLOYMENTS_BASE}/${deployment_pid}/env_vars/${encodeURIComponent(key)}`, {
107
+ method: "DELETE",
108
+ toolName: "delete_env_var",
109
+ });
110
+ if (!result.success) {
111
+ return { content: [{ type: "text", text: `Failed to delete env var (${result.statusCode}): ${result.message}` }] };
112
+ }
113
+ const parts = [
114
+ { type: "text", text: `Environment variable \`${key}\` removed.` },
115
+ ];
116
+ if (result.data)
117
+ parts.push({ type: "text", text: JSON.stringify(result.data, null, 2) });
118
+ return { content: parts };
119
+ });
120
+ }
121
+ //# sourceMappingURL=env-vars.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env-vars.js","sourceRoot":"","sources":["../../src/tools/env-vars.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerGithubTools(server: McpServer): void;
@@ -0,0 +1,106 @@
1
+ import { z } from "zod";
2
+ import { apiRequest } from "../client.js";
3
+ import { session } from "../session.js";
4
+ /**
5
+ * GitHub integration - list connected App installations (+ their repos),
6
+ * list branches, auto-detect a repo's build config, and get the install URL.
7
+ *
8
+ * Endpoints:
9
+ * - GET /api/v1/platform/github_installations
10
+ * - GET /api/v1/platform/github_installations/:installation_pid/branches?repo_full_name=
11
+ * - POST /api/v1/platform/github_installations/detect_repo_config
12
+ * - GET /github/connect?organization_pid= (returns a browser install URL)
13
+ *
14
+ * NOTE: connecting a GitHub account is an interactive, browser-based GitHub App
15
+ * installation - it CANNOT be completed headlessly. `get_github_connect_url`
16
+ * returns a link for the human to open; afterwards poll `list_github_installations`
17
+ * to confirm the connection landed.
18
+ */
19
+ const INSTALLATIONS_BASE = "/api/v1/platform/github_installations";
20
+ const NO_ORG = "No active organization. Call `list_organizations` then `select_organization` first.";
21
+ function orgId() {
22
+ return session.getActiveOrgId();
23
+ }
24
+ export function registerGithubTools(server) {
25
+ server.tool("list_github_installations", "List the GitHub App installations connected to the active organization, each with its accessible repositories embedded (`repos`: name, full_name, url, private, default_branch, description). Use `isActive`/status to pick a usable installation, and the repo's `default_branch` when listing branches. If empty, the org hasn't connected GitHub yet - use `get_github_connect_url`.", {}, async () => {
26
+ if (!orgId())
27
+ return { content: [{ type: "text", text: NO_ORG }] };
28
+ const result = await apiRequest(INSTALLATIONS_BASE, {
29
+ toolName: "list_github_installations",
30
+ });
31
+ if (!result.success) {
32
+ return { content: [{ type: "text", text: `Failed to list GitHub installations (${result.statusCode}): ${result.message}` }] };
33
+ }
34
+ return { content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }] };
35
+ });
36
+ server.tool("list_repo_branches", "List branches of a repository accessible through a GitHub installation. Provide the installation pid (ghi_..., from `list_github_installations`) and the repo as owner/repo. The API does not flag the default branch - cross-reference the repo's `default_branch` from `list_github_installations`.", {
37
+ installation_pid: z.string().min(1).describe("Installation pid, e.g. ghi_01h..."),
38
+ repo_full_name: z.string().regex(/^[^/]+\/[^/]+$/, "owner/repo").describe("owner/repo"),
39
+ page: z.number().int().min(1).optional().describe("Page (default 1)"),
40
+ per_page: z.number().int().min(1).max(100).optional().describe("Per page (default 100, max 100)"),
41
+ }, async ({ installation_pid, repo_full_name, page, per_page }) => {
42
+ if (!orgId())
43
+ return { content: [{ type: "text", text: NO_ORG }] };
44
+ const result = await apiRequest(`${INSTALLATIONS_BASE}/${installation_pid}/branches`, {
45
+ query: {
46
+ repo_full_name,
47
+ page: page === undefined ? undefined : String(page),
48
+ per_page: per_page === undefined ? undefined : String(per_page),
49
+ },
50
+ toolName: "list_repo_branches",
51
+ });
52
+ if (!result.success) {
53
+ return { content: [{ type: "text", text: `Failed to list branches (${result.statusCode}): ${result.message}` }] };
54
+ }
55
+ return { content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }] };
56
+ });
57
+ server.tool("detect_repo_config", "Scan a GitHub repo and auto-detect how to build/deploy it: visibility (`private`), `build_pack` (nixpacks | dockerfile | dockercompose | static), `port`, `dockerfile_path`, monorepo `base_directories`, and detected environment variables (`detected_vars` from .env.example/CI/workflows - these are hints, not secrets). Run this before `create_deployment` to pre-fill build settings. For PRIVATE repos you must pass `installation_pid` (else 422).", {
58
+ repo_url: z.string().min(1).describe("https://github.com/owner/repo"),
59
+ installation_pid: z.string().optional().describe("Installation pid (ghi_...); required for private repos"),
60
+ branch: z.string().optional().describe("Branch to scan (defaults to the repo's default branch)"),
61
+ }, async ({ repo_url, installation_pid, branch }) => {
62
+ if (!orgId())
63
+ return { content: [{ type: "text", text: NO_ORG }] };
64
+ const body = { repo_url };
65
+ if (installation_pid !== undefined)
66
+ body.installation_pid = installation_pid;
67
+ if (branch !== undefined)
68
+ body.branch = branch;
69
+ const result = await apiRequest(`${INSTALLATIONS_BASE}/detect_repo_config`, {
70
+ method: "POST",
71
+ body,
72
+ toolName: "detect_repo_config",
73
+ });
74
+ if (!result.success) {
75
+ return { content: [{ type: "text", text: `Failed to detect repo config (${result.statusCode}): ${result.message}` }] };
76
+ }
77
+ return { content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }] };
78
+ });
79
+ server.tool("get_github_connect_url", "Get a URL for the human to open in their browser to connect/install the SelfHost GitHub App on their account or org. Connecting GitHub is interactive (GitHub requires a logged-in user to approve and pick repos) and CANNOT be done headlessly. After the user completes it in the browser, poll `list_github_installations` to confirm the new installation appears (status `active`).", {
80
+ redirect_to_frontend: z.string().url().optional().describe("Optional URL to return the browser to after install"),
81
+ }, async ({ redirect_to_frontend }) => {
82
+ const id = orgId();
83
+ if (!id)
84
+ return { content: [{ type: "text", text: NO_ORG }] };
85
+ // /github/connect lives outside /api/v1/platform and expects `organization_pid` (not organization_id).
86
+ const result = await apiRequest("/github/connect", {
87
+ query: { organization_pid: id, redirect_to_frontend },
88
+ toolName: "get_github_connect_url",
89
+ skipOrgInjection: true,
90
+ });
91
+ if (!result.success) {
92
+ return { content: [{ type: "text", text: `Failed to get GitHub connect URL (${result.statusCode}): ${result.message}` }] };
93
+ }
94
+ const url = (result.data && result.data.redirect_url) || null;
95
+ if (!url) {
96
+ return { content: [{ type: "text", text: `Could not read the connect URL from the response.\n${JSON.stringify(result.data, null, 2)}` }] };
97
+ }
98
+ return {
99
+ content: [{
100
+ type: "text",
101
+ text: `Open this link in a browser to connect GitHub, then approve the app and select repositories:\n\n${url}\n\nWhen done, call \`list_github_installations\` to confirm the installation appears.`,
102
+ }],
103
+ };
104
+ });
105
+ }
106
+ //# sourceMappingURL=github.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github.js","sourceRoot":"","sources":["../../src/tools/github.ts"],"names":[],"mappings":"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"}
@@ -2,17 +2,17 @@ import { z } from "zod";
2
2
  import { apiRequest } from "../client.js";
3
3
  import { session } from "../session.js";
4
4
  /**
5
- * Hetzner Cloud — Postgres-only provisioning.
5
+ * Hetzner Cloud - Postgres-only provisioning.
6
6
  *
7
7
  * Backend endpoints (added in selfhost-dev/selfhost PR #983):
8
- * - POST /hetzner/v1/instances — provision
9
- * - GET /hetzner/v1/instances/:pid — read
10
- * - GET /hetzner/v1/instances/:pid/status — provisioning progress
11
- * - DELETE /hetzner/v1/instances/:pid — destroy
8
+ * - POST /hetzner/v1/instances - provision
9
+ * - GET /hetzner/v1/instances/:pid - read
10
+ * - GET /hetzner/v1/instances/:pid/status - provisioning progress
11
+ * - DELETE /hetzner/v1/instances/:pid - destroy
12
12
  *
13
13
  * Limitations vs AWS today:
14
14
  * - Postgres only (no MySQL, no MongoDB)
15
- * - No list endpoint — callers must keep track of `pid` after create
15
+ * - No list endpoint - callers must keep track of `pid` after create
16
16
  * - No start / stop / reboot / update / fork
17
17
  * - No scaling, no PITR, no PgBouncer, no database-user management
18
18
  *
@@ -70,7 +70,7 @@ const SERVER_TYPE_CODES = HETZNER_SERVER_TYPES.map((s) => s.code);
70
70
  // --- Registration ------------------------------------------------------------
71
71
  export function registerHetznerTools(server) {
72
72
  // ----- Provider discovery -------------------------------------------------
73
- server.tool("list_cloud_providers", "List the cloud providers SelfHost can provision Postgres on. Call this whenever the user expresses an intent to create a database — present the options and ask which one they want before choosing the create tool. The response includes capabilities and the specific create tool for each provider.", {}, async () => {
73
+ server.tool("list_cloud_providers", "List the cloud providers SelfHost can provision Postgres on. Call this whenever the user expresses an intent to create a database - present the options and ask which one they want before choosing the create tool. The response includes capabilities and the specific create tool for each provider.", {}, async () => {
74
74
  return {
75
75
  content: [
76
76
  { type: "text", text: JSON.stringify(CLOUD_PROVIDERS, null, 2) },
@@ -102,7 +102,7 @@ RECOMMENDED FLOW:
102
102
  5. Then call this tool.
103
103
 
104
104
  IMPORTANT:
105
- - The password returned in the response is shown ONCE. Save it immediately — there's no way to retrieve it.
105
+ - The password returned in the response is shown ONCE. Save it immediately - there's no way to retrieve it.
106
106
  - Hetzner integration is currently provision/delete only. After creation, use \`get_hetzner_instance_status\` to poll provisioning progress, and \`get_hetzner_instance\` to read details. To remove it, use \`delete_hetzner_instance\`.
107
107
  - Provisioning takes ~5-10 minutes (network + firewall + SSH key + server + volume + load balancer + Ansible/autobase playbook + Cloudflare DNS).`, {
108
108
  organization_id: z.string().optional()
@@ -110,9 +110,9 @@ IMPORTANT:
110
110
  name: z.string().min(1).max(63)
111
111
  .describe("Instance name (used for DNS host)"),
112
112
  region: z.enum(LOCATION_CODES)
113
- .describe("Hetzner location code — call list_hetzner_locations to see options"),
113
+ .describe("Hetzner location code - call list_hetzner_locations to see options"),
114
114
  server_type: z.enum(SERVER_TYPE_CODES)
115
- .describe("Hetzner server type — call list_hetzner_server_types to see options"),
115
+ .describe("Hetzner server type - call list_hetzner_server_types to see options"),
116
116
  storage_size: z.number().int().min(10).max(10240)
117
117
  .describe("Database storage size in GB (Hetzner Volume, separate from server local disk)"),
118
118
  db_version: z.string().optional()
@@ -182,10 +182,10 @@ IMPORTANT:
182
182
  if (data.password) {
183
183
  lines.push(`Password: ${data.password}`);
184
184
  lines.push("");
185
- lines.push("⚠️ SAVE THIS PASSWORD NOW — it cannot be retrieved again.");
185
+ lines.push("⚠️ SAVE THIS PASSWORD NOW - it cannot be retrieved again.");
186
186
  }
187
187
  lines.push("");
188
- lines.push(`Poll provisioning progress with \`get_hetzner_instance_status\` (pid: ${data.pid}). Provisioning typically takes 5–10 minutes.`);
188
+ lines.push(`Poll provisioning progress with \`get_hetzner_instance_status\` (pid: ${data.pid}). Provisioning typically takes 5-10 minutes.`);
189
189
  return {
190
190
  content: [
191
191
  { type: "text", text: lines.join("\n") },
@@ -227,7 +227,7 @@ IMPORTANT:
227
227
  }
228
228
  return { content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }] };
229
229
  });
230
- server.tool("delete_hetzner_instance", "Delete a Hetzner instance and all associated resources (server, volume, firewall, network, load balancer if HA, DNS record). IRREVERSIBLE — there's no `start` / `stop` on Hetzner; deletion is the only way to remove an instance.", {
230
+ server.tool("delete_hetzner_instance", "Delete a Hetzner instance and all associated resources (server, volume, firewall, network, load balancer if HA, DNS record). IRREVERSIBLE - there's no `start` / `stop` on Hetzner; deletion is the only way to remove an instance.", {
231
231
  pid: z.string().min(1),
232
232
  confirm: z.boolean().describe("Must be true to proceed. ALL DATA on this instance will be permanently lost."),
233
233
  }, async ({ pid, confirm }) => {
@@ -179,14 +179,14 @@ Returns an itemized cost breakdown including compute, storage, markup, and add-o
179
179
  return { content: [{ type: "text", text: lines.join("\n") }] };
180
180
  });
181
181
  // --- Instance lifecycle tools ---
182
- server.tool("create_instance", `Create a new database instance on AWS. This is the core AWS provisioning tool. For Hetzner Cloud, use \`create_hetzner_instance\` instead — call \`list_cloud_providers\` first to confirm which provider the user wants.
182
+ server.tool("create_instance", `Create a new database instance on AWS. This is the core AWS provisioning tool. For Hetzner Cloud, use \`create_hetzner_instance\` instead - call \`list_cloud_providers\` first to confirm which provider the user wants.
183
183
 
184
184
  RECOMMENDED FLOW (follow this before calling create_instance):
185
185
  1. Ask the user: host on SelfHost's cloud (platform-managed) or their own AWS account (BYOC)?
186
- 2. Call list_credentials — filter results by cloud_provider matching chosen provider AND status "active".
186
+ 2. Call list_credentials - filter results by cloud_provider matching chosen provider AND status "active".
187
187
  - Platform-managed: pick credential where is_platform_managed=true
188
188
  - BYOC: pick credential where is_platform_managed=false. If none exist, user must add_credential first.
189
- 3. If BYOC: call list_vpcs with the credential's id and chosen region — only present VPCs from the "public" group. vpc_id is REQUIRED for BYOC.
189
+ 3. If BYOC: call list_vpcs with the credential's id and chosen region - only present VPCs from the "public" group. vpc_id is REQUIRED for BYOC.
190
190
  4. If platform-managed: vpc_id is NOT needed (skip VPC selection entirely).
191
191
  5. Call estimate_instance_cost to show the user the estimated monthly cost before deploying.
192
192
  6. Only call create_instance after the user confirms the cost is acceptable.
@@ -209,7 +209,7 @@ IMPORTANT:
209
209
  custom_password: z.string().min(8).max(128).optional().describe("Database password (8-128 printable ASCII chars). Auto-generated if omitted."),
210
210
  description: z.string().optional(),
211
211
  cloud_credential_id: z.string().optional().describe("Credential PID from list_credentials. Use is_platform_managed=true credential for SelfHost's cloud, or is_platform_managed=false for BYOC. Always ask the user which they prefer."),
212
- vpc_id: z.string().optional().describe("VPC ID — REQUIRED when using BYOC credentials (is_platform_managed=false). Use list_vpcs and only pick from the 'public' group. Not needed for platform-managed."),
212
+ vpc_id: z.string().optional().describe("VPC ID - REQUIRED when using BYOC credentials (is_platform_managed=false). Use list_vpcs and only pick from the 'public' group. Not needed for platform-managed."),
213
213
  multi_az: z.boolean().optional().describe("Enable multi-AZ replication (default: false)"),
214
214
  replica_count: z.number().int().min(1).max(2).optional().describe("Number of replicas if multi_az (1 or 2)"),
215
215
  delete_protection: z.boolean().optional().describe("Prevent accidental deletion (default: true recommended)"),
@@ -235,7 +235,7 @@ IMPORTANT:
235
235
  if (params.storage.size < spec.minSize) {
236
236
  return { content: [{ type: "text", text: `For ${params.storage.type}, storage size must be at least ${spec.minSize} GiB` }] };
237
237
  }
238
- // Build request body — only include defined fields
238
+ // Build request body - only include defined fields
239
239
  const body = {
240
240
  organization_id: orgId,
241
241
  identifier: params.identifier,
@@ -247,7 +247,7 @@ IMPORTANT:
247
247
  iops: params.storage.iops ?? 0,
248
248
  throughput: params.storage.throughput ?? 0,
249
249
  },
250
- public_access: true, // Always true — private access not yet supported
250
+ public_access: true, // Always true - private access not yet supported
251
251
  };
252
252
  if (params.db_version)
253
253
  body.db_version = params.db_version;
@@ -306,7 +306,7 @@ IMPORTANT:
306
306
  `Username: ${data.username}`,
307
307
  `Password: ${data.password}`,
308
308
  "",
309
- "⚠️ SAVE THIS PASSWORD NOW — it cannot be retrieved again.",
309
+ "⚠️ SAVE THIS PASSWORD NOW - it cannot be retrieved again.",
310
310
  "",
311
311
  `Group ID: ${data.group_id}`,
312
312
  `Organization: ${data.organization_id}`,
@@ -357,7 +357,7 @@ IMPORTANT:
357
357
  }
358
358
  return { content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }] };
359
359
  });
360
- server.tool("update_instance", "Update an instance's configuration. Some changes are synchronous (db_port, backup, CIDR ranges), others are async (instance_type, multi_az, storage). Cannot update replicas — update the master instead.", {
360
+ server.tool("update_instance", "Update an instance's configuration. Some changes are synchronous (db_port, backup, CIDR ranges), others are async (instance_type, multi_az, storage). Cannot update replicas - update the master instead.", {
361
361
  instance_id: z.string().min(1, "Instance PID is required"),
362
362
  identifier: z.string().optional().describe("New instance name"),
363
363
  delete_protection: z.boolean().optional(),
@@ -400,7 +400,7 @@ IMPORTANT:
400
400
  ],
401
401
  };
402
402
  });
403
- server.tool("stop_instance", "DESTRUCTIVE: Stop one or more running database instances. Databases will become unavailable. Cannot stop replicas independently — stop the master to stop all. You MUST set confirm=true to proceed.", {
403
+ server.tool("stop_instance", "DESTRUCTIVE: Stop one or more running database instances. Databases will become unavailable. Cannot stop replicas independently - stop the master to stop all. You MUST set confirm=true to proceed.", {
404
404
  pids: z.array(z.string().min(1)).min(1, "At least one instance PID is required"),
405
405
  confirm: z.boolean().describe("Must be true to proceed. Ask the user for explicit confirmation before setting this."),
406
406
  }, async ({ pids, confirm }) => {
@@ -489,7 +489,7 @@ You MUST set confirm=true to proceed. Before calling this tool, always ask the u
489
489
  }
490
490
  return { content: [{ type: "text", text: `Instance(s) ${pids.join(", ")} deletion initiated.` }] };
491
491
  });
492
- server.tool("fork_instance", "Fork (clone) an existing database instance into a new independent instance. Cannot fork replicas or deleted instances. Returns a new password — save it immediately.", {
492
+ server.tool("fork_instance", "Fork (clone) an existing database instance into a new independent instance. Cannot fork replicas or deleted instances. Returns a new password - save it immediately.", {
493
493
  instance_id: z.string().min(1, "Source instance PID is required"),
494
494
  instance_name: z.string().min(1, "Name for the forked instance"),
495
495
  cloud_credential_id: z.string().optional().describe("Credential PID (defaults to source instance's credential)"),
@@ -518,7 +518,7 @@ You MUST set confirm=true to proceed. Before calling this tool, always ask the u
518
518
  `Username: ${data.username}`,
519
519
  `Password: ${data.password}`,
520
520
  "",
521
- "⚠️ SAVE THIS PASSWORD NOW — it cannot be retrieved again.",
521
+ "⚠️ SAVE THIS PASSWORD NOW - it cannot be retrieved again.",
522
522
  "",
523
523
  `Group ID: ${data.group_id}`,
524
524
  `Organization: ${data.organization_id}`,
@@ -71,7 +71,7 @@ export function registerPgBouncerTools(server) {
71
71
  }
72
72
  return {
73
73
  content: [
74
- { type: "text", text: "PgBouncer enable requested — status will transition to `enabled` once the configuration is applied. Poll `get_pgbouncer` to track progress." },
74
+ { type: "text", text: "PgBouncer enable requested - status will transition to `enabled` once the configuration is applied. Poll `get_pgbouncer` to track progress." },
75
75
  { type: "text", text: JSON.stringify(result.data, null, 2) },
76
76
  ],
77
77
  };
@@ -99,7 +99,7 @@ export function registerPgBouncerTools(server) {
99
99
  return {
100
100
  content: [{
101
101
  type: "text",
102
- text: "No changes to apply — pass at least one of pool_mode / max_client_conn / default_pool_size / min_pool_size / reserve_pool_size.",
102
+ text: "No changes to apply - pass at least one of pool_mode / max_client_conn / default_pool_size / min_pool_size / reserve_pool_size.",
103
103
  }],
104
104
  };
105
105
  }
@@ -119,7 +119,7 @@ export function registerPgBouncerTools(server) {
119
119
  }
120
120
  return { content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }] };
121
121
  });
122
- server.tool("disable_pgbouncer", "Disable PgBouncer on an instance. Existing connections drain gracefully. The config row is preserved so you can re-enable later with `enable_pgbouncer`. Valid from `enabled` or `error` state — use this to recover from `error`.", {
122
+ server.tool("disable_pgbouncer", "Disable PgBouncer on an instance. Existing connections drain gracefully. The config row is preserved so you can re-enable later with `enable_pgbouncer`. Valid from `enabled` or `error` state - use this to recover from `error`.", {
123
123
  instance_id: z.string().min(1),
124
124
  confirm: z.boolean().describe("Must be true to proceed. Disabling will drop pool warmth and force clients back to port 5432."),
125
125
  }, async ({ instance_id, confirm }) => {
@@ -146,7 +146,7 @@ export function registerPgBouncerTools(server) {
146
146
  }
147
147
  return {
148
148
  content: [
149
- { type: "text", text: "PgBouncer disable requested — clients should switch to port 5432." },
149
+ { type: "text", text: "PgBouncer disable requested - clients should switch to port 5432." },
150
150
  { type: "text", text: JSON.stringify(result.data, null, 2) },
151
151
  ],
152
152
  };
@@ -1,20 +1,20 @@
1
1
  import { z } from "zod";
2
2
  import { apiRequest } from "../client.js";
3
3
  /**
4
- * Point-in-Time Recovery (PITR) — continuous WAL archiving to S3 + restore
4
+ * Point-in-Time Recovery (PITR) - continuous WAL archiving to S3 + restore
5
5
  * to any second within the retention window.
6
6
  *
7
7
  * Endpoints:
8
- * - GET /aws/v1/instances/:id/pitr — current config + restore window
9
- * - POST /aws/v1/instances/:id/pitr — enable
10
- * - PUT /aws/v1/instances/:id/pitr/configure — toggle enabled/disabled
11
- * - POST /aws/v1/instances/:id/pitr/retry — recover from error
12
- * - POST /aws/v1/instances/:id/pitr/base_backup — trigger one-off backup
13
- * - GET /aws/v1/instances/:id/pitr/base_backups — list base backups
14
- * - POST /aws/v1/instances/:id/pitr/restore — restore to point-in-time
8
+ * - GET /aws/v1/instances/:id/pitr - current config + restore window
9
+ * - POST /aws/v1/instances/:id/pitr - enable
10
+ * - PUT /aws/v1/instances/:id/pitr/configure - toggle enabled/disabled
11
+ * - POST /aws/v1/instances/:id/pitr/retry - recover from error
12
+ * - POST /aws/v1/instances/:id/pitr/base_backup - trigger one-off backup
13
+ * - GET /aws/v1/instances/:id/pitr/base_backups - list base backups
14
+ * - POST /aws/v1/instances/:id/pitr/restore - restore to point-in-time
15
15
  *
16
16
  * Postgres-only, Pro+ plan only. Status: configuring | enabled | disabled |
17
- * error. Restore creates a NEW instance — the source stays untouched.
17
+ * error. Restore creates a NEW instance - the source stays untouched.
18
18
  */
19
19
  const PITR_TOGGLE = ["enabled", "disabled"];
20
20
  export function registerPitrTools(server) {
@@ -35,7 +35,7 @@ export function registerPitrTools(server) {
35
35
  }
36
36
  return { content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }] };
37
37
  });
38
- server.tool("enable_pitr", "Enable PITR on a Postgres instance — streams WAL to S3 continuously and takes nightly base backups. The S3 bucket must exist in the same region; the instance role needs s3:PutObject / GetObject / ListBucket. Status transitions: configuring → enabled (or error).", {
38
+ server.tool("enable_pitr", "Enable PITR on a Postgres instance - streams WAL to S3 continuously and takes nightly base backups. The S3 bucket must exist in the same region; the instance role needs s3:PutObject / GetObject / ListBucket. Status transitions: configuring → enabled (or error).", {
39
39
  instance_id: z.string().min(1),
40
40
  s3_bucket: z.string().min(3).max(63).regex(/^[a-z0-9.-]+$/, "lowercase letters, digits, dots, hyphens only")
41
41
  .describe("S3 bucket name (must exist in same region)"),
@@ -65,7 +65,7 @@ export function registerPitrTools(server) {
65
65
  });
66
66
  server.tool("configure_pitr", "Toggle an existing PITR config between `enabled` (resume WAL archiving) and `disabled` (pause). The bucket and retention are preserved either way. Use `enable_pitr` for first-time setup, not this.", {
67
67
  instance_id: z.string().min(1),
68
- status: z.enum(PITR_TOGGLE).describe("Target state — enabled resumes WAL, disabled pauses it"),
68
+ status: z.enum(PITR_TOGGLE).describe("Target state - enabled resumes WAL, disabled pauses it"),
69
69
  }, async ({ instance_id, status }) => {
70
70
  const result = await apiRequest(`/aws/v1/instances/${instance_id}/pitr/configure`, {
71
71
  method: "PUT",
@@ -86,8 +86,8 @@ export function registerPitrTools(server) {
86
86
  {
87
87
  type: "text",
88
88
  text: status === "enabled"
89
- ? "PITR resumed — WAL archiving is back on."
90
- : "PITR paused — no new WAL or base backups. Existing backups are preserved.",
89
+ ? "PITR resumed - WAL archiving is back on."
90
+ : "PITR paused - no new WAL or base backups. Existing backups are preserved.",
91
91
  },
92
92
  { type: "text", text: JSON.stringify(result.data, null, 2) },
93
93
  ],
@@ -116,7 +116,7 @@ export function registerPitrTools(server) {
116
116
  ],
117
117
  };
118
118
  });
119
- server.tool("trigger_pitr_base_backup", "Trigger a one-off base backup now. The backup runs `wal-g backup-push` in the background — your instance keeps serving traffic. Useful before risky migrations or to seed the restore window after a fresh enable.", {
119
+ server.tool("trigger_pitr_base_backup", "Trigger a one-off base backup now. The backup runs `wal-g backup-push` in the background - your instance keeps serving traffic. Useful before risky migrations or to seed the restore window after a fresh enable.", {
120
120
  instance_id: z.string().min(1),
121
121
  }, async ({ instance_id }) => {
122
122
  const result = await apiRequest(`/aws/v1/instances/${instance_id}/pitr/base_backup`, {
@@ -134,7 +134,7 @@ export function registerPitrTools(server) {
134
134
  }
135
135
  return {
136
136
  content: [
137
- { type: "text", text: "Base backup triggered. Track it via `list_pitr_base_backups` — status moves pending → running → completed (or failed)." },
137
+ { type: "text", text: "Base backup triggered. Track it via `list_pitr_base_backups` - status moves pending → running → completed (or failed)." },
138
138
  { type: "text", text: JSON.stringify(result.data, null, 2) },
139
139
  ],
140
140
  };
@@ -156,7 +156,7 @@ export function registerPitrTools(server) {
156
156
  }
157
157
  return { content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }] };
158
158
  });
159
- server.tool("restore_pitr", "Restore an instance to a point in time. Creates a NEW Postgres instance recovered to `target_time` — the source instance is untouched. `target_time` must fall within the source instance's [oldest_restore_point, latest_restore_point] window (see `get_pitr`). `name` is the new instance name; must start with a letter and use letters/digits/hyphens only.", {
159
+ server.tool("restore_pitr", "Restore an instance to a point in time. Creates a NEW Postgres instance recovered to `target_time` - the source instance is untouched. `target_time` must fall within the source instance's [oldest_restore_point, latest_restore_point] window (see `get_pitr`). `name` is the new instance name; must start with a letter and use letters/digits/hyphens only.", {
160
160
  instance_id: z.string().min(1).describe("Source instance to restore FROM"),
161
161
  target_time: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z?$/, "ISO 8601 timestamp, ideally with Z suffix (UTC)").describe("Target moment, e.g. 2026-05-12T14:30:00Z"),
162
162
  name: z.string().regex(/^[a-zA-Z][a-zA-Z0-9-]{0,62}$/, "1-63 chars, must start with a letter")
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerProjectDatabaseTools(server: McpServer): void;