@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
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { assertSafeDnsName, assertSafeDnsContent } from "../security.js";
|
|
3
|
+
const DNS_RECORD_TYPES = [
|
|
4
|
+
"A",
|
|
5
|
+
"AAAA",
|
|
6
|
+
"CNAME",
|
|
7
|
+
"TXT",
|
|
8
|
+
"MX",
|
|
9
|
+
"NS",
|
|
10
|
+
"SRV",
|
|
11
|
+
"CAA",
|
|
12
|
+
];
|
|
13
|
+
const pendingDeletes = new Map();
|
|
14
|
+
const CONFIRM_TTL_MS = 30 * 60 * 1000;
|
|
15
|
+
function stashDelete(domainId, recordId) {
|
|
16
|
+
const id = crypto.randomUUID();
|
|
17
|
+
pendingDeletes.set(id, {
|
|
18
|
+
domainId,
|
|
19
|
+
recordId,
|
|
20
|
+
expiresAt: Date.now() + CONFIRM_TTL_MS,
|
|
21
|
+
});
|
|
22
|
+
return id;
|
|
23
|
+
}
|
|
24
|
+
function popDelete(id) {
|
|
25
|
+
const entry = pendingDeletes.get(id);
|
|
26
|
+
if (!entry)
|
|
27
|
+
return null;
|
|
28
|
+
pendingDeletes.delete(id);
|
|
29
|
+
if (Date.now() > entry.expiresAt)
|
|
30
|
+
return null;
|
|
31
|
+
return { domainId: entry.domainId, recordId: entry.recordId };
|
|
32
|
+
}
|
|
33
|
+
export function registerDnsTools(server, client) {
|
|
34
|
+
// 1. Enable DNS management
|
|
35
|
+
server.tool("enable_dns_management", "Enable DNS management for a domain. This must be enabled before you can create, update, or delete DNS records.", {
|
|
36
|
+
domainId: z.number().int().describe("The domain ID (from list_domains)"),
|
|
37
|
+
}, async ({ domainId }) => {
|
|
38
|
+
const res = await client.post(`/api/client/domains/${domainId}/dns/enable`);
|
|
39
|
+
if (!res.success) {
|
|
40
|
+
return {
|
|
41
|
+
content: [{ type: "text", text: `Error: ${res.error}` }],
|
|
42
|
+
isError: true,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
content: [
|
|
47
|
+
{
|
|
48
|
+
type: "text",
|
|
49
|
+
text: `✅ DNS management enabled for domain ID ${domainId}. You can now manage DNS records with \`list_dns_records\`, \`create_dns_record\`, etc.`,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
// 2. List DNS records
|
|
55
|
+
server.tool("list_dns_records", "List all DNS records for a domain. DNS management must be enabled first.", {
|
|
56
|
+
domainId: z.number().int().describe("The domain ID (from list_domains)"),
|
|
57
|
+
}, async ({ domainId }) => {
|
|
58
|
+
const res = await client.get(`/api/client/domains/${domainId}/dns`);
|
|
59
|
+
if (!res.success) {
|
|
60
|
+
return {
|
|
61
|
+
content: [{ type: "text", text: `Error: ${res.error}` }],
|
|
62
|
+
isError: true,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const records = res.data ?? [];
|
|
66
|
+
if (records.length === 0) {
|
|
67
|
+
return {
|
|
68
|
+
content: [
|
|
69
|
+
{
|
|
70
|
+
type: "text",
|
|
71
|
+
text: `No DNS records found for domain ID ${domainId}. Use \`create_dns_record\` to add one.`,
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const header = `| ID | Type | Name | Content | TTL | Priority |`;
|
|
77
|
+
const divider = `|----|------|------|---------|-----|----------|`;
|
|
78
|
+
const rows = records.map((r) => `| ${r.id} | ${r.type} | ${r.name} | ${r.content} | ${r.ttl ?? "—"} | ${r.priority ?? "—"} |`);
|
|
79
|
+
return {
|
|
80
|
+
content: [
|
|
81
|
+
{
|
|
82
|
+
type: "text",
|
|
83
|
+
text: `**DNS Records for domain ID ${domainId} (${records.length}):**\n\n${header}\n${divider}\n${rows.join("\n")}`,
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
// 3. Create DNS record
|
|
89
|
+
server.tool("create_dns_record", "Create a new DNS record for a domain. Supports A, AAAA, CNAME, TXT, MX, NS, SRV, and CAA record types.", {
|
|
90
|
+
domainId: z.number().int().describe("The domain ID (from list_domains)"),
|
|
91
|
+
type: z
|
|
92
|
+
.enum(DNS_RECORD_TYPES)
|
|
93
|
+
.describe("DNS record type (A, AAAA, CNAME, TXT, MX, NS, SRV, CAA)"),
|
|
94
|
+
name: z
|
|
95
|
+
.string()
|
|
96
|
+
.describe("Record name/hostname (e.g. '@' for root, 'www', 'mail', 'sub.domain')"),
|
|
97
|
+
content: z
|
|
98
|
+
.string()
|
|
99
|
+
.describe("Record value (e.g. IP address for A/AAAA, hostname for CNAME/MX, text for TXT)"),
|
|
100
|
+
ttl: z
|
|
101
|
+
.number()
|
|
102
|
+
.int()
|
|
103
|
+
.min(60)
|
|
104
|
+
.optional()
|
|
105
|
+
.describe("Time to live in seconds (minimum 60, default varies by provider)"),
|
|
106
|
+
priority: z
|
|
107
|
+
.number()
|
|
108
|
+
.int()
|
|
109
|
+
.min(0)
|
|
110
|
+
.optional()
|
|
111
|
+
.describe("Record priority (required for MX and SRV records, lower = higher priority)"),
|
|
112
|
+
}, async ({ domainId, type, name, content, ttl, priority }) => {
|
|
113
|
+
try {
|
|
114
|
+
assertSafeDnsName(name);
|
|
115
|
+
assertSafeDnsContent(content);
|
|
116
|
+
}
|
|
117
|
+
catch (e) {
|
|
118
|
+
return {
|
|
119
|
+
content: [
|
|
120
|
+
{
|
|
121
|
+
type: "text",
|
|
122
|
+
text: e instanceof Error ? e.message : String(e),
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
isError: true,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const body = { type, name, content };
|
|
129
|
+
if (ttl != null)
|
|
130
|
+
body.ttl = ttl;
|
|
131
|
+
if (priority != null)
|
|
132
|
+
body.priority = priority;
|
|
133
|
+
const res = await client.post(`/api/client/domains/${domainId}/dns`, body);
|
|
134
|
+
if (!res.success) {
|
|
135
|
+
return {
|
|
136
|
+
content: [{ type: "text", text: `Error: ${res.error}` }],
|
|
137
|
+
isError: true,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const r = res.data;
|
|
141
|
+
const details = [
|
|
142
|
+
`✅ DNS record created successfully!`,
|
|
143
|
+
``,
|
|
144
|
+
`| Field | Value |`,
|
|
145
|
+
`|-------|-------|`,
|
|
146
|
+
r?.id != null ? `| ID | ${r.id} |` : null,
|
|
147
|
+
`| Type | ${type} |`,
|
|
148
|
+
`| Name | ${name} |`,
|
|
149
|
+
`| Content | ${content} |`,
|
|
150
|
+
ttl != null ? `| TTL | ${ttl}s |` : null,
|
|
151
|
+
priority != null ? `| Priority | ${priority} |` : null,
|
|
152
|
+
``,
|
|
153
|
+
`DNS changes may take a few minutes to propagate.`,
|
|
154
|
+
]
|
|
155
|
+
.filter(Boolean)
|
|
156
|
+
.join("\n");
|
|
157
|
+
return {
|
|
158
|
+
content: [{ type: "text", text: details }],
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
// 4. Update DNS record
|
|
162
|
+
server.tool("update_dns_record", "Update an existing DNS record. Provide the record ID from list_dns_records and the fields to change.", {
|
|
163
|
+
domainId: z.number().int().describe("The domain ID (from list_domains)"),
|
|
164
|
+
recordId: z.string().describe("The DNS record ID (from list_dns_records)"),
|
|
165
|
+
type: z
|
|
166
|
+
.enum(DNS_RECORD_TYPES)
|
|
167
|
+
.optional()
|
|
168
|
+
.describe("New record type"),
|
|
169
|
+
name: z.string().optional().describe("New record name/hostname"),
|
|
170
|
+
content: z.string().optional().describe("New record value"),
|
|
171
|
+
ttl: z
|
|
172
|
+
.number()
|
|
173
|
+
.int()
|
|
174
|
+
.min(60)
|
|
175
|
+
.optional()
|
|
176
|
+
.describe("New TTL in seconds"),
|
|
177
|
+
priority: z
|
|
178
|
+
.number()
|
|
179
|
+
.int()
|
|
180
|
+
.min(0)
|
|
181
|
+
.optional()
|
|
182
|
+
.describe("New priority value"),
|
|
183
|
+
}, async ({ domainId, recordId, type, name, content, ttl, priority }) => {
|
|
184
|
+
try {
|
|
185
|
+
if (name != null)
|
|
186
|
+
assertSafeDnsName(name);
|
|
187
|
+
if (content != null)
|
|
188
|
+
assertSafeDnsContent(content);
|
|
189
|
+
}
|
|
190
|
+
catch (e) {
|
|
191
|
+
return {
|
|
192
|
+
content: [
|
|
193
|
+
{
|
|
194
|
+
type: "text",
|
|
195
|
+
text: e instanceof Error ? e.message : String(e),
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
isError: true,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
const body = {};
|
|
202
|
+
if (type != null)
|
|
203
|
+
body.type = type;
|
|
204
|
+
if (name != null)
|
|
205
|
+
body.name = name;
|
|
206
|
+
if (content != null)
|
|
207
|
+
body.content = content;
|
|
208
|
+
if (ttl != null)
|
|
209
|
+
body.ttl = ttl;
|
|
210
|
+
if (priority != null)
|
|
211
|
+
body.priority = priority;
|
|
212
|
+
if (Object.keys(body).length === 0) {
|
|
213
|
+
return {
|
|
214
|
+
content: [
|
|
215
|
+
{
|
|
216
|
+
type: "text",
|
|
217
|
+
text: "No fields provided to update. Specify at least one of: type, name, content, ttl, priority.",
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
isError: true,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const res = await client.put(`/api/client/domains/${domainId}/dns/${recordId}`, body);
|
|
224
|
+
if (!res.success) {
|
|
225
|
+
return {
|
|
226
|
+
content: [{ type: "text", text: `Error: ${res.error}` }],
|
|
227
|
+
isError: true,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
const r = res.data;
|
|
231
|
+
const details = [
|
|
232
|
+
`✅ DNS record ${recordId} updated successfully!`,
|
|
233
|
+
``,
|
|
234
|
+
`| Field | Value |`,
|
|
235
|
+
`|-------|-------|`,
|
|
236
|
+
r?.type ? `| Type | ${r.type} |` : null,
|
|
237
|
+
r?.name ? `| Name | ${r.name} |` : null,
|
|
238
|
+
r?.content ? `| Content | ${r.content} |` : null,
|
|
239
|
+
r?.ttl != null ? `| TTL | ${r.ttl}s |` : null,
|
|
240
|
+
r?.priority != null ? `| Priority | ${r.priority} |` : null,
|
|
241
|
+
``,
|
|
242
|
+
`DNS changes may take a few minutes to propagate.`,
|
|
243
|
+
]
|
|
244
|
+
.filter(Boolean)
|
|
245
|
+
.join("\n");
|
|
246
|
+
return {
|
|
247
|
+
content: [{ type: "text", text: details }],
|
|
248
|
+
};
|
|
249
|
+
});
|
|
250
|
+
// 5. Delete DNS record (two-step — returns a confirmation token first)
|
|
251
|
+
server.tool("delete_dns_record", "Begin deletion of a DNS record. Returns a confirmation ID; pass it to confirm_delete_dns_record to actually delete. This two-step flow guards against accidental destruction by AI agents.", {
|
|
252
|
+
domainId: z.number().int().describe("The domain ID (from list_domains)"),
|
|
253
|
+
recordId: z.string().min(1).max(128).describe("The DNS record ID to delete (from list_dns_records)"),
|
|
254
|
+
}, async ({ domainId, recordId }) => {
|
|
255
|
+
// Best-effort preview lookup.
|
|
256
|
+
let preview = "";
|
|
257
|
+
try {
|
|
258
|
+
const lookup = await client.get(`/api/client/domains/${domainId}/dns`);
|
|
259
|
+
const rec = Array.isArray(lookup?.data)
|
|
260
|
+
? lookup.data.find((r) => String(r.id) === String(recordId))
|
|
261
|
+
: null;
|
|
262
|
+
if (rec) {
|
|
263
|
+
preview = `\n| Type | ${rec.type} |\n| Name | ${rec.name} |\n| Content | ${rec.content} |`;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// Preview is best-effort.
|
|
268
|
+
}
|
|
269
|
+
const confirmId = stashDelete(domainId, recordId);
|
|
270
|
+
return {
|
|
271
|
+
content: [
|
|
272
|
+
{
|
|
273
|
+
type: "text",
|
|
274
|
+
text: `**Delete DNS record — confirmation required**\n\n` +
|
|
275
|
+
`| Field | Value |\n|-------|-------|\n| Domain ID | ${domainId} |\n| Record ID | ${recordId} |${preview}\n\n` +
|
|
276
|
+
`⚠️ This cannot be undone. Call \`confirm_delete_dns_record\` with:\n\n\`${confirmId}\`\n\nExpires in 30 minutes.`,
|
|
277
|
+
},
|
|
278
|
+
],
|
|
279
|
+
};
|
|
280
|
+
});
|
|
281
|
+
// 5b. Confirm DNS record deletion
|
|
282
|
+
server.tool("confirm_delete_dns_record", "Confirm and execute a pending DNS record deletion. Pass the confirmation ID returned by delete_dns_record.", {
|
|
283
|
+
confirmationId: z.string().min(1).max(128).describe("Confirmation ID from delete_dns_record"),
|
|
284
|
+
}, async ({ confirmationId }) => {
|
|
285
|
+
const entry = popDelete(confirmationId);
|
|
286
|
+
if (!entry) {
|
|
287
|
+
return {
|
|
288
|
+
content: [
|
|
289
|
+
{
|
|
290
|
+
type: "text",
|
|
291
|
+
text: "Confirmation not found or expired. Call delete_dns_record again.",
|
|
292
|
+
},
|
|
293
|
+
],
|
|
294
|
+
isError: true,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
const res = await client.del(`/api/client/domains/${entry.domainId}/dns/${entry.recordId}`);
|
|
298
|
+
if (!res.success) {
|
|
299
|
+
return {
|
|
300
|
+
content: [{ type: "text", text: `Error: ${res.error}` }],
|
|
301
|
+
isError: true,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
content: [
|
|
306
|
+
{
|
|
307
|
+
type: "text",
|
|
308
|
+
text: `✅ DNS record ${entry.recordId} deleted from domain ID ${entry.domainId}. Propagation may take a few minutes.`,
|
|
309
|
+
},
|
|
310
|
+
],
|
|
311
|
+
};
|
|
312
|
+
});
|
|
313
|
+
}
|