@awesomate/hosting-mcp 0.1.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/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@awesomate/hosting-mcp",
3
+ "version": "0.1.0",
4
+ "description": "Awesomate Hosting MCP server — lets Claude manage your Awesomate WordPress hosting, plan, and limits",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "bin": {
11
+ "awesomate-hosting-mcp": "dist/index.js"
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "skill",
16
+ "README.md"
17
+ ],
18
+ "scripts": {
19
+ "build": "esbuild src/index.ts --bundle --platform=node --target=node18 --format=esm --outfile=dist/index.js --banner:js='#!/usr/bin/env node' --external:node:*",
20
+ "typecheck": "tsc --noEmit",
21
+ "prepublishOnly": "npm run typecheck && npm run build"
22
+ },
23
+ "dependencies": {
24
+ "@modelcontextprotocol/sdk": "^1.29.0",
25
+ "zod": "^3.24.1"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "^25.9.3",
29
+ "esbuild": "^0.25.0",
30
+ "typescript": "^6.0.3"
31
+ }
32
+ }
@@ -0,0 +1,130 @@
1
+ ---
2
+ name: awesomate-hosting
3
+ description: >
4
+ Manage, build, and grow your Awesomate WordPress hosting from Claude. Use when
5
+ the user mentions their Awesomate site or hosting, cPanel, a site on
6
+ *.awesomate.io or a custom domain hosted with Awesomate, WordPress admin,
7
+ installing a plugin/theme, running WP-CLI, "my site is slow / out of space /
8
+ broke", snapshotting or rolling back a site, deploying from a local WordPress
9
+ Studio site to live, checking hosting stats or how close they are to a plan
10
+ limit, workflow or AI-editor credits, or upgrading/downgrading their Awesomate
11
+ plan. Trigger phrases: "my awesomate site", "connect my hosting", "spin up a
12
+ wordpress site", "install wordpress", "add my domain", "why is my site slow",
13
+ "am I near my limit", "snapshot my site", "roll back my site", "deploy to
14
+ live", "run wp-cli", "upgrade my plan", "how many sites can I have".
15
+ ---
16
+
17
+ # Awesomate Hosting
18
+
19
+ You are helping the user run their **Awesomate-hosted WordPress** through the
20
+ **Awesomate Hosting MCP** (tools prefixed `awesomate_`). This skill is the thin
21
+ hosting/account/deploy/plan layer — for WordPress theming, blocks, and content
22
+ generation, defer to WordPress Studio and any installed WordPress skills
23
+ (`WordPress/agent-skills`, respira). Don't reimplement those.
24
+
25
+ ## 0. First run — orient before acting
26
+
27
+ On the first hosting request in a session, call **`awesomate_get_context`** once
28
+ (then `awesomate_get_limits` if you're about to create/change anything). From the
29
+ context note three things and cache them for the session:
30
+
31
+ - **`plan`** and **`capabilities.shell`** — this decides which branch below you're on.
32
+ - **`patExpiresAt`** — if it's within ~7 days, tell the user to open
33
+ **hub.awesomate.ai/sites → Connect Claude Code** and re-run the setup prompt to
34
+ refresh the token. If any tool returns a 401, do the same.
35
+ - **`cpanel`** routing (host/user), present once hosting is provisioned.
36
+
37
+ If `awesomate_get_context` fails with a connectivity error, it's the user's
38
+ network or the API base — not an auth problem; say so.
39
+
40
+ ## 1. The plan model (so your nudges are accurate)
41
+
42
+ | Plan | WP sites | Custom domains | Shell / Claude Code | Notes |
43
+ |---|---|---|---|---|
44
+ | **Essentials** | 1 | 0 | **No** | read/status + upgrade nudges only |
45
+ | **Support Plus** | (per plan) | ≥1 | **Yes** | shell, WP-CLI, snapshot/rollback, deploy |
46
+ | **Pro** | more | more | **Yes** | + higher limits |
47
+ | **Embedded** | ∞ | ∞ | **Yes** | agency tier |
48
+
49
+ Authoritative limits are always what `awesomate_get_limits` returns for THIS
50
+ account — never quote the table above as fact; use it only to explain upgrades.
51
+
52
+ ## 2. Branch by capability
53
+
54
+ ### Essentials (no shell) — read, report, nudge
55
+
56
+ Allowed: `awesomate_get_context`, `_get_hosting_status`, `_get_hosting_account`,
57
+ `_list_sites`, `_list_domains`, `_get_limits`, `_get_plan` / `_get_plan_features`,
58
+ `_get_hosting_stats`, `_get_grafana_url`, `_get_credit_balance`, the billing
59
+ preview/deep-link tools, and 1-click WP-admin / cPanel SSO links.
60
+
61
+ Never attempt SSH, WP-CLI, snapshot/rollback, deploy, multi-site, or custom
62
+ domains — those tools will 403. Instead, surface the specific limit and what
63
+ Support Plus/Pro unlocks (see §4), and offer to preview the upgrade.
64
+
65
+ ### Support Plus and above (shell) — full workflow
66
+
67
+ You additionally have: jailed SSH to the client's own cPanel account, WP-CLI
68
+ (`awesomate_run_wp_cli` or raw SSH via `scripts/ssh-connect.sh`),
69
+ snapshot/rollback, and the local-first deploy flow (§5). Golden rule, always:
70
+
71
+ > **Local first, live never — and snapshot before you touch live.**
72
+
73
+ ## 3. Safety rules (non-negotiable)
74
+
75
+ - **Before ANY change to a live site** — plugin/theme/core update, `search-replace`,
76
+ bulk edit, deploy — call **`awesomate_snapshot_site`** first and tell the user the
77
+ `snapshotId`. If it goes wrong, `awesomate_rollback_site` restores files + DB.
78
+ - **Confirm before destructive actions**: `awesomate_uninstall_site`,
79
+ `awesomate_rollback_site`, dropping tables, `wp db reset`, deleting content.
80
+ State exactly what will be lost and wait for an explicit "yes".
81
+ - Prefer building/testing in **WordPress Studio locally**, then deploy (§5).
82
+ - Dry-run risky WP-CLI where the command supports it before the real run.
83
+
84
+ ## 4. Limits & upgrade etiquette
85
+
86
+ Before any create action (install a site, add a domain, a heavy build) call
87
+ `awesomate_get_limits`. If a dimension is `approaching`/`exceeded`, or a tool
88
+ returns 403 for a plan reason:
89
+
90
+ 1. Explain plainly what they've hit and what the next plan unlocks (use the
91
+ nudge's `recommendedPlan`).
92
+ 2. If they want to upgrade, **preview first** with the billing preview tool and
93
+ show the exact prorated cost.
94
+ 3. Only proceed to an execute/upgrade tool after an explicit in-chat "yes".
95
+ **Never** trigger a charge, downgrade, or cancellation on your own —
96
+ downgrades/cancels always go to the hub billing page via the deep-link tool.
97
+
98
+ ## 5. Local-first deploy (Support Plus+)
99
+
100
+ The build happens in **WordPress Studio** on the user's machine; going live,
101
+ today, is a guided manual flow (a one-command `deploy.sh` is the next increment):
102
+
103
+ 1. **Snapshot the live site first** — `awesomate_snapshot_site`. Never skip this.
104
+ 2. Push the local Studio site to live over the jailed SSH:
105
+ - Files: `rsync` the Studio site's `wp-content` (and any changed root files)
106
+ to the live docroot via `scripts/ssh-connect.sh`.
107
+ - Database: export the Studio DB, import it live, then
108
+ `wp.sh <live-docroot> search-replace <studio-url> <live-url> --all-tables`.
109
+ 3. Verify the live site loads. If anything's wrong,
110
+ `awesomate_rollback_site` to the snapshot from step 1 (the current state was
111
+ also auto-snapshotted, so the rollback is itself reversible).
112
+
113
+ To start FROM an existing live site, pull it down: snapshot, then `rsync` the
114
+ docroot + `wp db export` down into a new Studio site.
115
+
116
+ Because deploy overwrites a live site, always snapshot first and verify before
117
+ telling the user it's done.
118
+
119
+ ## 6. Bundled scripts (`scripts/`)
120
+
121
+ - `bootstrap.mjs` — one-time setup: redeems the hub setup code for a scoped
122
+ access token, stores it locally (chmod 600), generates an SSH keypair and
123
+ registers the public key (Support Plus+), and registers the MCP server. The
124
+ Connect-Claude-Code prompt from the hub runs this.
125
+ - `ssh-connect.sh` — open a jailed SSH session (or run one remote command).
126
+ - `wp.sh` — run a WP-CLI command against a live site over that SSH.
127
+
128
+ A one-command `deploy.sh` / `pull-live.sh` (Studio↔live) is the next increment;
129
+ until then use the guided flow in §5. All scripts read
130
+ `~/.awesomate/credentials.json`; none take secrets on the command line.
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Awesomate Hosting — one-time bootstrap.
4
+ *
5
+ * Redeems the single-use setup code from the hub's "Connect Claude Code" prompt
6
+ * for a durable, scoped access token; stores it locally; and (on Support Plus+)
7
+ * generates an SSH keypair and authorizes the PUBLIC key on the client's own
8
+ * cPanel account. The private key never leaves this machine; the setup code is
9
+ * the only secret that ever crossed the clipboard, and it's single-use + short-
10
+ * lived.
11
+ *
12
+ * Usage (Claude runs this — the user doesn't touch a terminal):
13
+ * node bootstrap.mjs --code amt_bs_XXXX [--api https://hub.awesomate.ai]
14
+ *
15
+ * Dependency-free: Node 18+ built-ins only (global fetch, node:fs/os/path,
16
+ * child_process for ssh-keygen).
17
+ */
18
+
19
+ import { execFileSync } from 'node:child_process';
20
+ import { existsSync, mkdirSync, writeFileSync, chmodSync, readFileSync } from 'node:fs';
21
+ import { homedir, hostname } from 'node:os';
22
+ import { join } from 'node:path';
23
+
24
+ function arg(name, fallback) {
25
+ const i = process.argv.indexOf(`--${name}`);
26
+ return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : fallback;
27
+ }
28
+
29
+ const code = arg('code');
30
+ const apiBase = (arg('api', 'https://hub.awesomate.ai')).replace(/\/$/, '');
31
+ if (!code || !code.startsWith('amt_bs_')) {
32
+ console.error('Missing/invalid --code. Copy a fresh setup prompt from hub.awesomate.ai/sites.');
33
+ process.exit(1);
34
+ }
35
+
36
+ const dir = join(homedir(), '.awesomate');
37
+ const keysDir = join(dir, 'keys');
38
+ const credPath = join(dir, 'credentials.json');
39
+ const keyPath = join(keysDir, 'id_ed25519');
40
+
41
+ async function post(path, { body, token } = {}) {
42
+ const headers = { 'Content-Type': 'application/json' };
43
+ if (token) headers.Authorization = `Bearer ${token}`;
44
+ const res = await fetch(`${apiBase}${path}`, { method: 'POST', headers, body: JSON.stringify(body ?? {}) });
45
+ const text = await res.text();
46
+ let json;
47
+ try { json = text ? JSON.parse(text) : null; } catch { json = text; }
48
+ if (!res.ok) {
49
+ const msg = json && typeof json === 'object' && json.error ? json.error : `HTTP ${res.status}`;
50
+ throw new Error(`${path} failed: ${msg}`);
51
+ }
52
+ return json;
53
+ }
54
+
55
+ function writeCreds(creds) {
56
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
57
+ chmodSync(dir, 0o700);
58
+ writeFileSync(credPath, JSON.stringify(creds, null, 2));
59
+ chmodSync(credPath, 0o600);
60
+ }
61
+
62
+ async function main() {
63
+ console.log(`Redeeming setup code against ${apiBase} …`);
64
+ const redeemed = await post('/api/hosting-access/redeem', {
65
+ body: { code, machineLabel: `Claude Code on ${hostname()}` },
66
+ });
67
+
68
+ const creds = {
69
+ apiBase,
70
+ pat: redeemed.pat,
71
+ contactId: redeemed.contactId,
72
+ slug: redeemed.slug ?? null,
73
+ plan: redeemed.plan,
74
+ cpanel: redeemed.cpanel ?? null,
75
+ expiresAt: redeemed.expiresAt ?? null,
76
+ };
77
+ writeCreds(creds);
78
+ console.log(`✓ Access token stored at ${credPath} (chmod 600). Plan: ${redeemed.plan}.`);
79
+
80
+ if (redeemed.shellAccess) {
81
+ if (!existsSync(keyPath)) {
82
+ mkdirSync(keysDir, { recursive: true, mode: 0o700 });
83
+ execFileSync('ssh-keygen', ['-t', 'ed25519', '-f', keyPath, '-N', '', '-C', `awesomate:${redeemed.slug ?? 'site'}`], { stdio: 'ignore' });
84
+ chmodSync(keyPath, 0o600);
85
+ }
86
+ const publicKey = readFileSync(`${keyPath}.pub`, 'utf8').trim();
87
+ const reg = await post('/api/hosting-access/ssh-keys', {
88
+ token: redeemed.pat,
89
+ body: { publicKey, label: `Claude Code on ${hostname()}` },
90
+ });
91
+ creds.ssh = { host: reg.sshHost, user: reg.sshUser, port: reg.sshPort ?? 22, keyPath, fingerprint: reg.fingerprint };
92
+ writeCreds(creds);
93
+ console.log(`✓ SSH key authorized on ${reg.sshUser}@${reg.sshHost} (jailed to your account).`);
94
+ } else {
95
+ console.log('• Plan has no shell access — read/status mode. Upgrade to Support Plus for SSH + deploy.');
96
+ }
97
+
98
+ // Register the MCP server. Prefer the Claude CLI if present; otherwise print
99
+ // the config for Claude to apply.
100
+ const env = `AWESOMATE_PAT=${redeemed.pat} AWESOMATE_API_BASE=${apiBase}`;
101
+ const cmd = `claude mcp add awesomate-hosting --env AWESOMATE_PAT=${redeemed.pat} --env AWESOMATE_API_BASE=${apiBase} -- npx -y @awesomate/hosting-mcp`;
102
+ let registered = false;
103
+ try {
104
+ execFileSync('claude', ['mcp', 'add', 'awesomate-hosting', '--env', `AWESOMATE_PAT=${redeemed.pat}`, '--env', `AWESOMATE_API_BASE=${apiBase}`, '--', 'npx', '-y', '@awesomate/hosting-mcp'], { stdio: 'ignore' });
105
+ registered = true;
106
+ console.log('✓ Registered the Awesomate Hosting MCP server with Claude Code.');
107
+ } catch {
108
+ console.log('\nAdd the Awesomate Hosting MCP server to this project — run:');
109
+ console.log(` ${cmd}`);
110
+ console.log('…or add to .mcp.json:');
111
+ console.log(JSON.stringify({ mcpServers: { 'awesomate-hosting': { command: 'npx', args: ['-y', '@awesomate/hosting-mcp'], env: { AWESOMATE_PAT: redeemed.pat, AWESOMATE_API_BASE: apiBase } } } }, null, 2));
112
+ }
113
+
114
+ console.log('\nSetup complete. Next: ask me to read your plan, list your sites, and tell you what we can do.');
115
+ if (!registered) console.log('(Restart Claude Code first so it picks up the new MCP server.)');
116
+ }
117
+
118
+ main().catch((err) => {
119
+ console.error('Bootstrap failed:', err.message);
120
+ console.error('If the code expired or was already used, grab a fresh one from hub.awesomate.ai/sites.');
121
+ process.exit(1);
122
+ });
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env bash
2
+ # Open a jailed SSH session to the client's own cPanel account, using the key
3
+ # provisioned by bootstrap.mjs. Any extra args are run as a remote command
4
+ # (non-interactive); with none, opens an interactive shell.
5
+ # ssh-connect.sh # interactive shell
6
+ # ssh-connect.sh 'wp --info' # one-off command
7
+ set -euo pipefail
8
+ CRED="$HOME/.awesomate/credentials.json"
9
+ [ -f "$CRED" ] || { echo "Not connected. Run bootstrap first (Connect Claude Code on hub.awesomate.ai/sites)."; exit 1; }
10
+
11
+ # Parse the ssh block with node (present wherever the MCP runs via npx).
12
+ eval "$(node -e '
13
+ const c = require(process.env.HOME + "/.awesomate/credentials.json");
14
+ if (!c.ssh) { console.error("Your plan does not include shell access (Support Plus+ required)."); process.exit(2); }
15
+ const q = (s) => "'"'"'" + String(s).replace(/'"'"'/g, "") + "'"'"'";
16
+ console.log(`H=${q(c.ssh.host)}; U=${q(c.ssh.user)}; P=${q(c.ssh.port||22)}; K=${q(c.ssh.keyPath)}`);
17
+ ')"
18
+
19
+ exec ssh -i "$K" -p "$P" -o StrictHostKeyChecking=accept-new "$U@$H" "$@"
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env bash
2
+ # Run a WP-CLI command on the live site over the jailed SSH connection.
3
+ # Pass the WordPress path for the target site and the wp args.
4
+ # wp.sh /home/<user>/public_html plugin list
5
+ # wp.sh /home/<user>/public_html option get siteurl
6
+ # (Or use the awesomate_run_wp_cli MCP tool, which is audited server-side.)
7
+ set -euo pipefail
8
+ here="$(cd "$(dirname "$0")" && pwd)"
9
+ [ "$#" -ge 2 ] || { echo "Usage: wp.sh <wp-path> <wp args...>"; exit 1; }
10
+ WP_PATH="$1"; shift
11
+ # Delegate the connection to ssh-connect.sh; run wp in the given path.
12
+ exec "$here/ssh-connect.sh" "wp --path=$(printf %q "$WP_PATH") $*"