@suzko/mcp-server 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/CHANGELOG.md +32 -0
- package/GUIDE.md +952 -0
- package/LICENSE +21 -0
- package/README.md +171 -0
- package/dist/bin/mcp-server.d.ts +11 -0
- package/dist/bin/mcp-server.js +108 -0
- package/dist/src/auth.d.ts +14 -0
- package/dist/src/auth.js +140 -0
- package/dist/src/client.d.ts +18 -0
- package/dist/src/client.js +78 -0
- package/dist/src/index.d.ts +8 -0
- package/dist/src/index.js +44 -0
- package/dist/src/prompts/index.d.ts +5 -0
- package/dist/src/prompts/index.js +87 -0
- package/dist/src/resources/index.d.ts +7 -0
- package/dist/src/resources/index.js +86 -0
- package/dist/src/security.d.ts +18 -0
- package/dist/src/security.js +108 -0
- package/dist/src/tools/account.d.ts +3 -0
- package/dist/src/tools/account.js +254 -0
- package/dist/src/tools/deploy.d.ts +3 -0
- package/dist/src/tools/deploy.js +468 -0
- package/dist/src/tools/dns.d.ts +3 -0
- package/dist/src/tools/dns.js +313 -0
- package/dist/src/tools/domains.d.ts +3 -0
- package/dist/src/tools/domains.js +499 -0
- package/dist/src/tools/server-admin.d.ts +3 -0
- package/dist/src/tools/server-admin.js +738 -0
- package/dist/src/tools/services.d.ts +3 -0
- package/dist/src/tools/services.js +181 -0
- package/dist/src/tools/support.d.ts +3 -0
- package/dist/src/tools/support.js +259 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Suzko
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# @suzko/mcp-server
|
|
2
|
+
|
|
3
|
+
AI-powered infrastructure management for VS Code, Claude, and Cursor.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
### VS Code (Copilot agent mode)
|
|
8
|
+
|
|
9
|
+
Add to your `settings.json`:
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"mcp": {
|
|
14
|
+
"servers": {
|
|
15
|
+
"suzko": {
|
|
16
|
+
"command": "npx",
|
|
17
|
+
"args": ["-y", "@suzko/mcp-server"],
|
|
18
|
+
"env": {
|
|
19
|
+
"SUZKO_API_BASE": "https://www.suzko.com"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Claude Desktop
|
|
28
|
+
|
|
29
|
+
Add to `claude_desktop_config.json`:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"mcpServers": {
|
|
34
|
+
"suzko": {
|
|
35
|
+
"command": "npx",
|
|
36
|
+
"args": ["-y", "@suzko/mcp-server"]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Claude Code CLI
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
claude mcp add suzko -- npx -y @suzko/mcp-server
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Authentication
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Login (opens browser)
|
|
52
|
+
npx @suzko/mcp-server auth login
|
|
53
|
+
|
|
54
|
+
# Check status
|
|
55
|
+
npx @suzko/mcp-server auth status
|
|
56
|
+
|
|
57
|
+
# Logout
|
|
58
|
+
npx @suzko/mcp-server auth logout
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Credentials are stored in `~/.suzko/mcp-credentials.json`.
|
|
62
|
+
|
|
63
|
+
## Tools
|
|
64
|
+
|
|
65
|
+
### Deploy (12 tools)
|
|
66
|
+
- `list_deploy_templates` — Browse available service templates
|
|
67
|
+
- `list_deploy_projects` — List your deployed projects
|
|
68
|
+
- `create_deploy_project` — Create a new project (with cost confirmation)
|
|
69
|
+
- `confirm_deploy_project` — Confirm project creation
|
|
70
|
+
- `get_deploy_project` — Project details and status
|
|
71
|
+
- `control_deploy_project` — Start/stop/restart/delete
|
|
72
|
+
- `get_deploy_logs` — Runtime logs
|
|
73
|
+
- `get_deploy_build_logs` — Build output
|
|
74
|
+
- `redeploy_project` — Trigger redeployment
|
|
75
|
+
- `set_deploy_env` — Set environment variables
|
|
76
|
+
- `check_subdomain` — Check subdomain availability
|
|
77
|
+
- `get_deploy_usage` — Usage stats and billing
|
|
78
|
+
|
|
79
|
+
### Domains (12 tools)
|
|
80
|
+
- `search_domains` — Check domain availability
|
|
81
|
+
- `get_tld_pricing` — TLD pricing table
|
|
82
|
+
- `suggest_domain_names` — AI domain name suggestions
|
|
83
|
+
- `register_domain` / `confirm_domain_registration` — Register with confirmation
|
|
84
|
+
- `transfer_domain` / `confirm_domain_transfer` — Transfer with confirmation
|
|
85
|
+
- `list_domains` — Your owned domains
|
|
86
|
+
- `get_domain_details` — Full domain info
|
|
87
|
+
- `update_domain_nameservers` — Update NS records
|
|
88
|
+
- `get_domain_lock_status` / `toggle_domain_lock` — Lock management
|
|
89
|
+
|
|
90
|
+
### DNS (5 tools)
|
|
91
|
+
- `enable_dns_management` — Enable DNS for a domain
|
|
92
|
+
- `list_dns_records` — List all records
|
|
93
|
+
- `create_dns_record` — Create A/AAAA/CNAME/TXT/MX/NS/SRV/CAA
|
|
94
|
+
- `update_dns_record` — Update records
|
|
95
|
+
- `delete_dns_record` — Delete records
|
|
96
|
+
|
|
97
|
+
### Account & Billing (7 tools)
|
|
98
|
+
- `get_account_info` — Profile and account details
|
|
99
|
+
- `list_invoices` / `get_invoice` — Billing history
|
|
100
|
+
- `list_subscriptions` — Active subscriptions
|
|
101
|
+
- `check_perks` — Feature access
|
|
102
|
+
- `list_orders` — Order history
|
|
103
|
+
- `get_account_balance` — Credit balance
|
|
104
|
+
|
|
105
|
+
### Services (4 tools)
|
|
106
|
+
- `list_services` — WHMCS hosting services
|
|
107
|
+
- `get_service_details` — Service configuration
|
|
108
|
+
- `get_service_sso_url` — Control panel SSO link
|
|
109
|
+
- `upgrade_service` — Upgrade guidance
|
|
110
|
+
|
|
111
|
+
### Support (5 tools)
|
|
112
|
+
- `list_tickets` / `get_ticket` — Ticket management
|
|
113
|
+
- `open_support_ticket` — Create tickets
|
|
114
|
+
- `reply_to_ticket` — Reply to tickets
|
|
115
|
+
- `close_ticket` — Close tickets
|
|
116
|
+
|
|
117
|
+
### BYOS Server Admin (12 tools)
|
|
118
|
+
- `connect_server` — Register a server via SSH
|
|
119
|
+
- `list_servers` — List registered servers
|
|
120
|
+
- `inspect_server` — System diagnostics
|
|
121
|
+
- `run_server_command` — Execute commands (with safety checks)
|
|
122
|
+
- `install_docker` — Install Docker
|
|
123
|
+
- `deploy_to_server` — Deploy via SCP + docker-compose
|
|
124
|
+
- `list_server_containers` / `manage_server_container` — Docker management
|
|
125
|
+
- `setup_ssl` — SSL certificate setup
|
|
126
|
+
- `get_server_status` — System status
|
|
127
|
+
- `get_server_logs` — Log reading
|
|
128
|
+
- `manage_env_file` — .env file management
|
|
129
|
+
|
|
130
|
+
## Resources
|
|
131
|
+
|
|
132
|
+
The server exposes read-only resources via `suzko://` URIs:
|
|
133
|
+
- `suzko://services` — Active hosting services
|
|
134
|
+
- `suzko://deploy/projects` — Deploy projects
|
|
135
|
+
- `suzko://domains` — Registered domains
|
|
136
|
+
- `suzko://invoices` — Billing history
|
|
137
|
+
- `suzko://perks` — Subscription perks
|
|
138
|
+
|
|
139
|
+
## Prompts
|
|
140
|
+
|
|
141
|
+
Pre-built workflow templates:
|
|
142
|
+
- `deploy-app` — Deploy an application
|
|
143
|
+
- `find-domain` — Search and register domains
|
|
144
|
+
- `troubleshoot-service` — Diagnose service issues
|
|
145
|
+
- `setup-server` — Set up a new server (BYOS)
|
|
146
|
+
|
|
147
|
+
## Human-in-the-Loop
|
|
148
|
+
|
|
149
|
+
Tools that create resources or cost money use a two-step confirmation:
|
|
150
|
+
|
|
151
|
+
1. Tool returns a cost preview with a confirmation token
|
|
152
|
+
2. AI presents the cost to the user
|
|
153
|
+
3. User approves → AI calls the confirm tool with the token
|
|
154
|
+
4. Action is executed
|
|
155
|
+
|
|
156
|
+
Confirmation tokens expire after 5 minutes and are single-use.
|
|
157
|
+
|
|
158
|
+
## Environment Variables
|
|
159
|
+
|
|
160
|
+
| Variable | Default | Description |
|
|
161
|
+
|----------|---------|-------------|
|
|
162
|
+
| `SUZKO_API_BASE` | `https://www.suzko.com` | API base URL |
|
|
163
|
+
|
|
164
|
+
## Development
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
cd packages/mcp-server
|
|
168
|
+
bun install
|
|
169
|
+
bun run build # Compile TypeScript
|
|
170
|
+
bun run dev # Watch mode
|
|
171
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Suzko MCP Server CLI
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx @suzko/mcp-server — Start MCP server (stdio transport)
|
|
7
|
+
* npx @suzko/mcp-server auth login — Login to Suzko account
|
|
8
|
+
* npx @suzko/mcp-server auth status — Check auth status
|
|
9
|
+
* npx @suzko/mcp-server auth logout — Remove saved credentials
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Suzko MCP Server CLI
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx @suzko/mcp-server — Start MCP server (stdio transport)
|
|
7
|
+
* npx @suzko/mcp-server auth login — Login to Suzko account
|
|
8
|
+
* npx @suzko/mcp-server auth status — Check auth status
|
|
9
|
+
* npx @suzko/mcp-server auth logout — Remove saved credentials
|
|
10
|
+
*/
|
|
11
|
+
import { login, loadCredentials, resolveApiBase } from "../src/auth.js";
|
|
12
|
+
import { unlink } from "node:fs/promises";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
async function main() {
|
|
16
|
+
const args = process.argv.slice(2);
|
|
17
|
+
let apiBase;
|
|
18
|
+
try {
|
|
19
|
+
// Throws (and we exit) if SUZKO_API_BASE is malicious.
|
|
20
|
+
apiBase = resolveApiBase();
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
// No args = start MCP server
|
|
27
|
+
if (args.length === 0) {
|
|
28
|
+
// Dynamic import to avoid loading MCP SDK for CLI commands
|
|
29
|
+
await import("../src/index.js");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const command = args[0];
|
|
33
|
+
const subcommand = args[1];
|
|
34
|
+
if (command === "auth") {
|
|
35
|
+
switch (subcommand) {
|
|
36
|
+
case "login": {
|
|
37
|
+
await login(apiBase);
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
case "status": {
|
|
41
|
+
const creds = await loadCredentials();
|
|
42
|
+
if (!creds) {
|
|
43
|
+
console.log("Not logged in. Run: npx @suzko/mcp-server auth login");
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const expiresAt = new Date(creds.expiresAt);
|
|
47
|
+
const isExpired = expiresAt.getTime() < Date.now();
|
|
48
|
+
console.log(`✓ Logged in`);
|
|
49
|
+
console.log(` API: ${creds.apiBase}`);
|
|
50
|
+
console.log(` Token: ${isExpired ? "EXPIRED" : "Valid"} (expires ${expiresAt.toLocaleString()})`);
|
|
51
|
+
}
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
case "logout": {
|
|
55
|
+
const credFile = join(homedir(), ".suzko", "mcp-credentials.json");
|
|
56
|
+
try {
|
|
57
|
+
await unlink(credFile);
|
|
58
|
+
console.log("✓ Credentials removed.");
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
console.log("No credentials to remove.");
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
default:
|
|
66
|
+
console.log("Usage: npx @suzko/mcp-server auth [login|status|logout]");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else if (command === "help" || command === "--help" || command === "-h") {
|
|
70
|
+
console.log(`
|
|
71
|
+
Suzko MCP Server — AI-powered infrastructure management
|
|
72
|
+
|
|
73
|
+
Usage:
|
|
74
|
+
npx @suzko/mcp-server Start MCP server (stdio transport)
|
|
75
|
+
npx @suzko/mcp-server auth login Login to your Suzko account
|
|
76
|
+
npx @suzko/mcp-server auth status Check authentication status
|
|
77
|
+
npx @suzko/mcp-server auth logout Remove saved credentials
|
|
78
|
+
|
|
79
|
+
Configuration (VS Code settings.json):
|
|
80
|
+
{
|
|
81
|
+
"mcp": {
|
|
82
|
+
"servers": {
|
|
83
|
+
"suzko": {
|
|
84
|
+
"command": "npx",
|
|
85
|
+
"args": ["-y", "@suzko/mcp-server"]
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
Environment:
|
|
92
|
+
SUZKO_API_BASE API base URL (default: https://www.suzko.com)
|
|
93
|
+
`);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
console.error(`Unknown command: ${command}`);
|
|
97
|
+
console.log("Run: npx @suzko/mcp-server --help");
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
main().then(() => {
|
|
102
|
+
// CLI commands should exit; MCP server stays running (handled by stdio transport)
|
|
103
|
+
if (process.argv.length > 2)
|
|
104
|
+
process.exit(0);
|
|
105
|
+
}).catch((error) => {
|
|
106
|
+
console.error("Error:", error.message || error);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
interface Credentials {
|
|
2
|
+
accessToken: string;
|
|
3
|
+
refreshToken: string;
|
|
4
|
+
expiresAt: string;
|
|
5
|
+
apiBase: string;
|
|
6
|
+
}
|
|
7
|
+
/** Resolve the API base from env, validating it against the allowlist. */
|
|
8
|
+
export declare function resolveApiBase(override?: string): string;
|
|
9
|
+
export declare function loadCredentials(): Promise<Credentials | null>;
|
|
10
|
+
export declare function saveCredentials(creds: Credentials): Promise<void>;
|
|
11
|
+
export declare function getValidToken(apiBase: string): Promise<string | null>;
|
|
12
|
+
/** Interactive CLI login. Opens browser for auth, exchanges code for tokens. */
|
|
13
|
+
export declare function login(apiBase: string): Promise<boolean>;
|
|
14
|
+
export {};
|
package/dist/src/auth.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Device-token authentication. Credentials stored in ~/.suzko/mcp-credentials.json.
|
|
2
|
+
import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { validateApiBase } from "./security.js";
|
|
6
|
+
const CONFIG_DIR = join(homedir(), ".suzko");
|
|
7
|
+
const CREDENTIALS_FILE = join(CONFIG_DIR, "mcp-credentials.json");
|
|
8
|
+
const DEFAULT_API_BASE = "https://www.suzko.com";
|
|
9
|
+
/** Resolve the API base from env, validating it against the allowlist. */
|
|
10
|
+
export function resolveApiBase(override) {
|
|
11
|
+
return validateApiBase(override || process.env.SUZKO_API_BASE || DEFAULT_API_BASE);
|
|
12
|
+
}
|
|
13
|
+
export async function loadCredentials() {
|
|
14
|
+
try {
|
|
15
|
+
const raw = await readFile(CREDENTIALS_FILE, "utf-8");
|
|
16
|
+
return JSON.parse(raw);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export async function saveCredentials(creds) {
|
|
23
|
+
validateApiBase(creds.apiBase);
|
|
24
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
25
|
+
try {
|
|
26
|
+
await chmod(CONFIG_DIR, 0o700);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// chmod unsupported on Windows.
|
|
30
|
+
}
|
|
31
|
+
await writeFile(CREDENTIALS_FILE, JSON.stringify(creds, null, 2), "utf-8");
|
|
32
|
+
try {
|
|
33
|
+
await chmod(CREDENTIALS_FILE, 0o600);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// chmod unsupported on Windows.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Shared in-flight refresh so parallel callers don't double-spend the refresh token.
|
|
40
|
+
let refreshingPromise = null;
|
|
41
|
+
export async function getValidToken(apiBase) {
|
|
42
|
+
const creds = await loadCredentials();
|
|
43
|
+
if (!creds)
|
|
44
|
+
return null;
|
|
45
|
+
// Check expiry — auto-refresh if within 1 hour of expiration
|
|
46
|
+
const expiresAt = new Date(creds.expiresAt).getTime();
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
const oneHour = 60 * 60 * 1000;
|
|
49
|
+
if (now > expiresAt) {
|
|
50
|
+
return refreshTokenShared(apiBase, creds);
|
|
51
|
+
}
|
|
52
|
+
if (expiresAt - now < oneHour) {
|
|
53
|
+
refreshTokenShared(apiBase, creds).catch(() => { });
|
|
54
|
+
}
|
|
55
|
+
return creds.accessToken;
|
|
56
|
+
}
|
|
57
|
+
function refreshTokenShared(apiBase, creds) {
|
|
58
|
+
if (refreshingPromise)
|
|
59
|
+
return refreshingPromise;
|
|
60
|
+
refreshingPromise = refreshToken(apiBase, creds).finally(() => {
|
|
61
|
+
refreshingPromise = null;
|
|
62
|
+
});
|
|
63
|
+
return refreshingPromise;
|
|
64
|
+
}
|
|
65
|
+
async function refreshToken(apiBase, creds) {
|
|
66
|
+
try {
|
|
67
|
+
const res = await fetch(`${apiBase}/api/auth/device-refresh`, {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: { "Content-Type": "application/json" },
|
|
70
|
+
body: JSON.stringify({ refreshToken: creds.refreshToken }),
|
|
71
|
+
});
|
|
72
|
+
if (!res.ok)
|
|
73
|
+
return null;
|
|
74
|
+
const data = await res.json();
|
|
75
|
+
if (!data.success || !data.data?.accessToken)
|
|
76
|
+
return null;
|
|
77
|
+
const expiresInMs = (data.data.expiresIn || 7 * 24 * 60 * 60) * 1000;
|
|
78
|
+
const updated = {
|
|
79
|
+
accessToken: data.data.accessToken,
|
|
80
|
+
refreshToken: data.data.refreshToken || creds.refreshToken,
|
|
81
|
+
expiresAt: new Date(Date.now() + expiresInMs).toISOString(),
|
|
82
|
+
apiBase,
|
|
83
|
+
};
|
|
84
|
+
await saveCredentials(updated);
|
|
85
|
+
return updated.accessToken;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** Interactive CLI login. Opens browser for auth, exchanges code for tokens. */
|
|
92
|
+
export async function login(apiBase) {
|
|
93
|
+
validateApiBase(apiBase);
|
|
94
|
+
const loginUrl = `${apiBase}/auth/device?client=mcp`;
|
|
95
|
+
console.log(`\nOpen this URL in your browser to log in:\n\n ${loginUrl}\n`);
|
|
96
|
+
console.log("Verify the URL is on https://www.suzko.com before pasting your code.\n");
|
|
97
|
+
// Prompt for the auth code
|
|
98
|
+
const code = await promptInput("Paste your auth code: ");
|
|
99
|
+
if (!code) {
|
|
100
|
+
console.error("No code provided.");
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const res = await fetch(`${apiBase}/api/auth/device-token`, {
|
|
105
|
+
method: "POST",
|
|
106
|
+
headers: { "Content-Type": "application/json" },
|
|
107
|
+
body: JSON.stringify({ code, client: "mcp" }),
|
|
108
|
+
});
|
|
109
|
+
const data = await res.json();
|
|
110
|
+
if (!data.success || !data.data?.accessToken) {
|
|
111
|
+
console.error("Login failed:", data.error || "Unknown error");
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
const expiresInMs = (data.data.expiresIn || 7 * 24 * 60 * 60) * 1000;
|
|
115
|
+
await saveCredentials({
|
|
116
|
+
accessToken: data.data.accessToken,
|
|
117
|
+
refreshToken: data.data.refreshToken,
|
|
118
|
+
expiresAt: new Date(Date.now() + expiresInMs).toISOString(),
|
|
119
|
+
apiBase,
|
|
120
|
+
});
|
|
121
|
+
console.log("✓ Login successful! Credentials saved.");
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
console.error("Login failed:", e instanceof Error ? e.message : String(e));
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function promptInput(message) {
|
|
130
|
+
return new Promise((resolve) => {
|
|
131
|
+
process.stdout.write(message);
|
|
132
|
+
process.stdin.setEncoding("utf-8");
|
|
133
|
+
process.stdin.resume();
|
|
134
|
+
process.stdin.once("data", (chunk) => {
|
|
135
|
+
process.stdin.pause();
|
|
136
|
+
process.stdin.unref();
|
|
137
|
+
resolve(chunk.toString().trim());
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare class SuzkoClient {
|
|
2
|
+
private apiBase;
|
|
3
|
+
constructor(apiBase?: string);
|
|
4
|
+
/** Make an authenticated GET request */
|
|
5
|
+
get<T = unknown>(path: string, params?: Record<string, string>): Promise<T>;
|
|
6
|
+
/** Make an authenticated POST request */
|
|
7
|
+
post<T = unknown>(path: string, body?: unknown): Promise<T>;
|
|
8
|
+
/** Make an authenticated PUT request */
|
|
9
|
+
put<T = unknown>(path: string, body?: unknown): Promise<T>;
|
|
10
|
+
/** Make an authenticated DELETE request */
|
|
11
|
+
del<T = unknown>(path: string, body?: unknown): Promise<T>;
|
|
12
|
+
/** Make an authenticated PATCH request */
|
|
13
|
+
patch<T = unknown>(path: string, body?: unknown): Promise<T>;
|
|
14
|
+
private request;
|
|
15
|
+
/** Check if we have valid credentials */
|
|
16
|
+
isAuthenticated(): Promise<boolean>;
|
|
17
|
+
}
|
|
18
|
+
export declare function getClient(): SuzkoClient;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Authenticated client for the Suzko API. Auto-refreshes tokens on expiry.
|
|
2
|
+
import { getValidToken, resolveApiBase } from "./auth.js";
|
|
3
|
+
export class SuzkoClient {
|
|
4
|
+
apiBase;
|
|
5
|
+
constructor(apiBase) {
|
|
6
|
+
this.apiBase = resolveApiBase(apiBase);
|
|
7
|
+
}
|
|
8
|
+
/** Make an authenticated GET request */
|
|
9
|
+
async get(path, params) {
|
|
10
|
+
const url = new URL(path, this.apiBase);
|
|
11
|
+
if (params) {
|
|
12
|
+
for (const [k, v] of Object.entries(params)) {
|
|
13
|
+
url.searchParams.set(k, v);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return this.request("GET", url.toString());
|
|
17
|
+
}
|
|
18
|
+
/** Make an authenticated POST request */
|
|
19
|
+
async post(path, body) {
|
|
20
|
+
const url = new URL(path, this.apiBase);
|
|
21
|
+
return this.request("POST", url.toString(), body);
|
|
22
|
+
}
|
|
23
|
+
/** Make an authenticated PUT request */
|
|
24
|
+
async put(path, body) {
|
|
25
|
+
const url = new URL(path, this.apiBase);
|
|
26
|
+
return this.request("PUT", url.toString(), body);
|
|
27
|
+
}
|
|
28
|
+
/** Make an authenticated DELETE request */
|
|
29
|
+
async del(path, body) {
|
|
30
|
+
const url = new URL(path, this.apiBase);
|
|
31
|
+
return this.request("DELETE", url.toString(), body);
|
|
32
|
+
}
|
|
33
|
+
/** Make an authenticated PATCH request */
|
|
34
|
+
async patch(path, body) {
|
|
35
|
+
const url = new URL(path, this.apiBase);
|
|
36
|
+
return this.request("PATCH", url.toString(), body);
|
|
37
|
+
}
|
|
38
|
+
async request(method, url, body) {
|
|
39
|
+
const token = await getValidToken(this.apiBase);
|
|
40
|
+
const headers = {
|
|
41
|
+
"X-Client": "mcp-server",
|
|
42
|
+
};
|
|
43
|
+
if (token) {
|
|
44
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
45
|
+
}
|
|
46
|
+
if (body !== undefined) {
|
|
47
|
+
headers["Content-Type"] = "application/json";
|
|
48
|
+
}
|
|
49
|
+
const res = await fetch(url, {
|
|
50
|
+
method,
|
|
51
|
+
headers,
|
|
52
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
53
|
+
});
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
const text = await res.text().catch(() => "");
|
|
56
|
+
const trimmed = text.trim();
|
|
57
|
+
const looksLikeHtml = trimmed.startsWith("<") || /<html|<!doctype/i.test(trimmed.slice(0, 64));
|
|
58
|
+
const msg = looksLikeHtml
|
|
59
|
+
? `Backend returned an HTML error page (${res.status}).`
|
|
60
|
+
: trimmed.slice(0, 200) || res.statusText;
|
|
61
|
+
throw new Error(`API error ${res.status}: ${msg}`);
|
|
62
|
+
}
|
|
63
|
+
return res.json();
|
|
64
|
+
}
|
|
65
|
+
/** Check if we have valid credentials */
|
|
66
|
+
async isAuthenticated() {
|
|
67
|
+
const token = await getValidToken(this.apiBase);
|
|
68
|
+
return token !== null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** Singleton client instance */
|
|
72
|
+
let _client = null;
|
|
73
|
+
export function getClient() {
|
|
74
|
+
if (!_client) {
|
|
75
|
+
_client = new SuzkoClient();
|
|
76
|
+
}
|
|
77
|
+
return _client;
|
|
78
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Suzko MCP Server
|
|
4
|
+
*
|
|
5
|
+
* AI-powered infrastructure management for VS Code, Claude, and Cursor.
|
|
6
|
+
* Uses stdio transport for universal IDE compatibility.
|
|
7
|
+
*/
|
|
8
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
10
|
+
import { getClient } from "./client.js";
|
|
11
|
+
import { registerDeployTools } from "./tools/deploy.js";
|
|
12
|
+
import { registerDomainTools } from "./tools/domains.js";
|
|
13
|
+
import { registerAccountTools } from "./tools/account.js";
|
|
14
|
+
import { registerServicesTools } from "./tools/services.js";
|
|
15
|
+
import { registerDnsTools } from "./tools/dns.js";
|
|
16
|
+
import { registerSupportTools } from "./tools/support.js";
|
|
17
|
+
import { registerServerAdminTools } from "./tools/server-admin.js";
|
|
18
|
+
import { registerResources } from "./resources/index.js";
|
|
19
|
+
import { registerPrompts } from "./prompts/index.js";
|
|
20
|
+
async function main() {
|
|
21
|
+
const server = new McpServer({
|
|
22
|
+
name: "suzko",
|
|
23
|
+
version: "0.1.0",
|
|
24
|
+
});
|
|
25
|
+
const client = getClient();
|
|
26
|
+
// Register all tool categories
|
|
27
|
+
registerDeployTools(server, client);
|
|
28
|
+
registerDomainTools(server, client);
|
|
29
|
+
registerDnsTools(server, client);
|
|
30
|
+
registerAccountTools(server, client);
|
|
31
|
+
registerServicesTools(server, client);
|
|
32
|
+
registerSupportTools(server, client);
|
|
33
|
+
registerServerAdminTools(server, client);
|
|
34
|
+
// Register resources and prompts
|
|
35
|
+
registerResources(server, client);
|
|
36
|
+
registerPrompts(server);
|
|
37
|
+
// Connect via stdio
|
|
38
|
+
const transport = new StdioServerTransport();
|
|
39
|
+
await server.connect(transport);
|
|
40
|
+
}
|
|
41
|
+
main().catch((error) => {
|
|
42
|
+
console.error("MCP Server failed to start:", error);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
});
|