@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.
- package/README.md +147 -24
- package/dist/auth.d.ts +1 -1
- package/dist/auth.js +3 -3
- package/dist/client.js +1 -1
- package/dist/config.js +1 -1
- package/dist/index.js +29 -4
- package/dist/index.js.map +1 -1
- package/dist/rate-limiter.js +1 -1
- package/dist/tools/billing.js +14 -14
- package/dist/tools/credentials.js +1 -1
- package/dist/tools/database-users.js +9 -9
- package/dist/tools/deployment-domains.d.ts +2 -0
- package/dist/tools/deployment-domains.js +128 -0
- package/dist/tools/deployment-domains.js.map +1 -0
- package/dist/tools/deployment-notifications.d.ts +2 -0
- package/dist/tools/deployment-notifications.js +74 -0
- package/dist/tools/deployment-notifications.js.map +1 -0
- package/dist/tools/deployments.d.ts +2 -0
- package/dist/tools/deployments.js +302 -0
- package/dist/tools/deployments.js.map +1 -0
- package/dist/tools/env-vars.d.ts +2 -0
- package/dist/tools/env-vars.js +121 -0
- package/dist/tools/env-vars.js.map +1 -0
- package/dist/tools/github.d.ts +2 -0
- package/dist/tools/github.js +106 -0
- package/dist/tools/github.js.map +1 -0
- package/dist/tools/hetzner.js +13 -13
- package/dist/tools/instances.js +11 -11
- package/dist/tools/pgbouncer.js +4 -4
- package/dist/tools/pitr.js +16 -16
- package/dist/tools/project-databases.d.ts +2 -0
- package/dist/tools/project-databases.js +169 -0
- package/dist/tools/project-databases.js.map +1 -0
- package/dist/tools/project-services.d.ts +2 -0
- package/dist/tools/project-services.js +93 -0
- package/dist/tools/project-services.js.map +1 -0
- package/dist/tools/projects.d.ts +2 -0
- package/dist/tools/projects.js +141 -0
- package/dist/tools/projects.js.map +1 -0
- package/dist/tools/scaling.js +13 -13
- package/dist/types/models.js +1 -1
- package/dist/types/tiers.js +31 -1
- package/dist/types/tiers.js.map +1 -1
- 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,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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"}
|
package/dist/tools/hetzner.js
CHANGED
|
@@ -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
|
|
5
|
+
* Hetzner Cloud - Postgres-only provisioning.
|
|
6
6
|
*
|
|
7
7
|
* Backend endpoints (added in selfhost-dev/selfhost PR #983):
|
|
8
|
-
* - POST /hetzner/v1/instances
|
|
9
|
-
* - GET /hetzner/v1/instances/:pid
|
|
10
|
-
* - GET /hetzner/v1/instances/:pid/status
|
|
11
|
-
* - DELETE /hetzner/v1/instances/:pid
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 }) => {
|
package/dist/tools/instances.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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}`,
|
package/dist/tools/pgbouncer.js
CHANGED
|
@@ -71,7 +71,7 @@ export function registerPgBouncerTools(server) {
|
|
|
71
71
|
}
|
|
72
72
|
return {
|
|
73
73
|
content: [
|
|
74
|
-
{ type: "text", text: "PgBouncer enable requested
|
|
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
|
|
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
|
|
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
|
|
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
|
};
|
package/dist/tools/pitr.js
CHANGED
|
@@ -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)
|
|
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
|
|
9
|
-
* - POST /aws/v1/instances/:id/pitr
|
|
10
|
-
* - PUT /aws/v1/instances/:id/pitr/configure
|
|
11
|
-
* - POST /aws/v1/instances/:id/pitr/retry
|
|
12
|
-
* - POST /aws/v1/instances/:id/pitr/base_backup
|
|
13
|
-
* - GET /aws/v1/instances/:id/pitr/base_backups
|
|
14
|
-
* - POST /aws/v1/instances/:id/pitr/restore
|
|
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
|
|
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
|
|
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
|
|
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
|
|
90
|
-
: "PITR paused
|
|
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
|
|
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`
|
|
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`
|
|
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")
|