@porkbunllc/mcp-server 0.1.1 → 0.3.2
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 +43 -3
- package/dist/index.js +2 -1
- package/dist/tools.js +564 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -2,20 +2,60 @@
|
|
|
2
2
|
|
|
3
3
|
A [Model Context Protocol](https://modelcontextprotocol.io) server that exposes the [Porkbun v3 API](https://porkbun.com/api/json/v3/documentation) as native tools for AI agents — Claude Desktop, Cursor, Cline, and any other MCP-compatible client.
|
|
4
4
|
|
|
5
|
-
> **Status:** v0.1 —
|
|
5
|
+
> **Status:** v0.3.1 — covers everything you can do in the Porkbun web UI. All write operations attach an `Idempotency-Key` automatically, so retries within 24 hours don't double-charge.
|
|
6
6
|
|
|
7
|
-
## What's included (v0.1)
|
|
7
|
+
## What's included (v0.3.1 — 30 tools)
|
|
8
|
+
|
|
9
|
+
**Read tools (free, no spend, no state changes)**
|
|
8
10
|
|
|
9
11
|
| Tool | Description |
|
|
10
12
|
|---|---|
|
|
11
13
|
| `ping` | Verify API connectivity and credentials |
|
|
12
14
|
| `check_domain` | Check availability and pricing for a single domain |
|
|
15
|
+
| `get_pricing` | Get registration/renewal/transfer pricing for all TLDs (no auth needed) |
|
|
16
|
+
| `list_marketplace` | Browse the Porkbun aftermarket — filter by TLD, max price, name substring |
|
|
13
17
|
| `list_domains` | Paginate through domains in the authenticated account |
|
|
14
18
|
| `get_balance` | Get account credit balance |
|
|
15
|
-
| `
|
|
19
|
+
| `get_api_settings` | Get monthly spend limit, low-balance alert, auto top-up config, MTD spend |
|
|
20
|
+
| `get_nameservers` | Get current nameservers for a domain |
|
|
16
21
|
| `list_dns_records` | List DNS records for a domain |
|
|
22
|
+
| `list_dnssec_records` | List DNSSEC DS records published at the registry |
|
|
23
|
+
| `list_url_forwards` | List URL forwarding rules for a domain |
|
|
24
|
+
| `list_glue_records` | List glue records (host-to-IP mappings) for a domain |
|
|
25
|
+
| `list_transfers` | List in-progress and recent inbound transfers |
|
|
26
|
+
| `get_transfer_status` | Get status of a specific inbound transfer |
|
|
17
27
|
| `get_ssl_bundle` | Retrieve the free Porkbun-issued SSL bundle for a domain |
|
|
18
28
|
|
|
29
|
+
**Domain lifecycle writes (spend account credit)**
|
|
30
|
+
|
|
31
|
+
| Tool | Description |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `register_domain` | Register a new domain — call `check_domain` first to confirm price |
|
|
34
|
+
| `renew_domain` | Renew an existing domain |
|
|
35
|
+
| `transfer_domain` | Initiate an inbound transfer (returns transferId; takes 5-7 days) |
|
|
36
|
+
|
|
37
|
+
**Domain settings writes (free)**
|
|
38
|
+
|
|
39
|
+
| Tool | Description |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `update_auto_renew` | Turn auto-renewal on or off |
|
|
42
|
+
| `update_nameservers` | Replace the nameserver list for a domain (full replace, not append) |
|
|
43
|
+
|
|
44
|
+
**DNS / DNSSEC / URL-forwarding / glue writes (free)**
|
|
45
|
+
|
|
46
|
+
| Tool | Description |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `create_dns_record` | Create a new DNS record (A, AAAA, CNAME, MX, TXT, etc.) |
|
|
49
|
+
| `update_dns_record` | Update an existing DNS record by its ID |
|
|
50
|
+
| `delete_dns_record` | Delete a DNS record by its ID |
|
|
51
|
+
| `create_dnssec_record` | Submit a DNSSEC DS record to the registry |
|
|
52
|
+
| `delete_dnssec_record` | Remove a DNSSEC DS record by key tag |
|
|
53
|
+
| `create_url_forward` | Create a URL forwarding rule |
|
|
54
|
+
| `delete_url_forward` | Delete a URL forwarding rule by ID |
|
|
55
|
+
| `create_glue_record` | Create a glue record (host-to-IP mapping at the registry) |
|
|
56
|
+
| `update_glue_record` | Replace the IP list for a glue record |
|
|
57
|
+
| `delete_glue_record` | Delete a glue record by host |
|
|
58
|
+
|
|
19
59
|
## Install
|
|
20
60
|
|
|
21
61
|
You'll need [Node.js](https://nodejs.org) 18 or newer.
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { loadConfig } from "./api.js";
|
|
|
5
5
|
import { tools } from "./tools.js";
|
|
6
6
|
const server = new McpServer({
|
|
7
7
|
name: "porkbun-mcp",
|
|
8
|
-
version: "0.
|
|
8
|
+
version: "0.3.2",
|
|
9
9
|
});
|
|
10
10
|
// Defer config loading until the first tool call. tools/list works without
|
|
11
11
|
// credentials so MCP clients can still discover what's available.
|
|
@@ -19,6 +19,7 @@ for (const tool of tools) {
|
|
|
19
19
|
server.registerTool(tool.name, {
|
|
20
20
|
description: tool.description,
|
|
21
21
|
inputSchema: tool.inputSchema,
|
|
22
|
+
...(tool.annotations ? { annotations: tool.annotations } : {}),
|
|
22
23
|
}, async (args) => {
|
|
23
24
|
try {
|
|
24
25
|
const result = await tool.handler(getConfig(), args);
|
package/dist/tools.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { call } from "./api.js";
|
|
3
|
+
// ─── Read-only tools ────────────────────────────────────────────────────────
|
|
3
4
|
const ping = {
|
|
4
5
|
name: "ping",
|
|
5
6
|
description: "Verify the Porkbun API connection and credentials. Returns the caller's public IP and whether the API key is valid. Use this as a first sanity check before making other calls.",
|
|
6
7
|
inputSchema: {},
|
|
8
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
7
9
|
handler: async (config) => {
|
|
8
10
|
return await call(config, "/ping", { method: "POST" });
|
|
9
11
|
},
|
|
@@ -17,6 +19,7 @@ const check_domain = {
|
|
|
17
19
|
.min(3)
|
|
18
20
|
.describe("Fully qualified domain name to check, e.g. `example.com`"),
|
|
19
21
|
},
|
|
22
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
20
23
|
handler: async (config, args) => {
|
|
21
24
|
const domain = String(args.domain).toLowerCase();
|
|
22
25
|
return await call(config, `/domain/checkDomain/${encodeURIComponent(domain)}`, {
|
|
@@ -39,6 +42,7 @@ const list_domains = {
|
|
|
39
42
|
.optional()
|
|
40
43
|
.describe("If true, include user-defined domain labels in the response."),
|
|
41
44
|
},
|
|
45
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
42
46
|
handler: async (config, args) => {
|
|
43
47
|
const params = new URLSearchParams();
|
|
44
48
|
if (args.start !== undefined)
|
|
@@ -53,6 +57,7 @@ const get_balance = {
|
|
|
53
57
|
name: "get_balance",
|
|
54
58
|
description: "Get the available account credit balance for the authenticated Porkbun account. Returns the balance in cents (integer) and a human-readable display string (e.g. `$12.34`). Use this to check spend headroom before initiating registrations or renewals.",
|
|
55
59
|
inputSchema: {},
|
|
60
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
56
61
|
handler: async (config) => {
|
|
57
62
|
return await call(config, "/account/balance", { method: "GET" });
|
|
58
63
|
},
|
|
@@ -61,6 +66,7 @@ const get_pricing = {
|
|
|
61
66
|
name: "get_pricing",
|
|
62
67
|
description: "Get current Porkbun pricing for all supported TLDs. Returns registration, renewal, and transfer prices per TLD in USD. No authentication required. Useful when an agent needs to compare TLD costs before registering. Note: this returns standard pricing only — premium domains have their own per-domain pricing reported by check_domain.",
|
|
63
68
|
inputSchema: {},
|
|
69
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
64
70
|
handler: async (config) => {
|
|
65
71
|
return await call(config, "/pricing/get", { method: "POST" });
|
|
66
72
|
},
|
|
@@ -74,6 +80,7 @@ const list_dns_records = {
|
|
|
74
80
|
.min(3)
|
|
75
81
|
.describe("Fully qualified domain name registered at Porkbun, e.g. `example.com`"),
|
|
76
82
|
},
|
|
83
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
77
84
|
handler: async (config, args) => {
|
|
78
85
|
const domain = String(args.domain).toLowerCase();
|
|
79
86
|
return await call(config, `/dns/retrieve/${encodeURIComponent(domain)}`, { method: "GET" });
|
|
@@ -88,17 +95,573 @@ const get_ssl_bundle = {
|
|
|
88
95
|
.min(3)
|
|
89
96
|
.describe("Fully qualified domain name registered at Porkbun, e.g. `example.com`"),
|
|
90
97
|
},
|
|
98
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
91
99
|
handler: async (config, args) => {
|
|
92
100
|
const domain = String(args.domain).toLowerCase();
|
|
93
101
|
return await call(config, `/ssl/retrieve/${encodeURIComponent(domain)}`, { method: "GET" });
|
|
94
102
|
},
|
|
95
103
|
};
|
|
104
|
+
const get_nameservers = {
|
|
105
|
+
name: "get_nameservers",
|
|
106
|
+
description: "Get the current nameservers configured for a domain in the authenticated account. Returns an array of nameserver hostnames. Read-only complement to `update_nameservers`.",
|
|
107
|
+
inputSchema: {
|
|
108
|
+
domain: z.string().min(3).describe("Fully qualified domain name, e.g. `example.com`"),
|
|
109
|
+
},
|
|
110
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
111
|
+
handler: async (config, args) => {
|
|
112
|
+
const domain = String(args.domain).toLowerCase();
|
|
113
|
+
return await call(config, `/domain/getNs/${encodeURIComponent(domain)}`, { method: "GET" });
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
const list_url_forwards = {
|
|
117
|
+
name: "list_url_forwards",
|
|
118
|
+
description: "List all URL forwarding rules configured for a domain. Each entry includes its `id` (used by `delete_url_forward`), the source subdomain, the destination URL, the redirect type (permanent/temporary), and whether the request path and wildcards are forwarded.",
|
|
119
|
+
inputSchema: {
|
|
120
|
+
domain: z.string().min(3).describe("Fully qualified domain name, e.g. `example.com`"),
|
|
121
|
+
},
|
|
122
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
123
|
+
handler: async (config, args) => {
|
|
124
|
+
const domain = String(args.domain).toLowerCase();
|
|
125
|
+
return await call(config, `/domain/getUrlForwarding/${encodeURIComponent(domain)}`, {
|
|
126
|
+
method: "GET",
|
|
127
|
+
});
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
const list_dnssec_records = {
|
|
131
|
+
name: "list_dnssec_records",
|
|
132
|
+
description: "List the DNSSEC DS records currently submitted to the registry for a domain. Returns key tag, algorithm, digest type, and digest. Use this to verify DNSSEC chain-of-trust setup. Empty array = DNSSEC not configured.",
|
|
133
|
+
inputSchema: {
|
|
134
|
+
domain: z.string().min(3).describe("Fully qualified domain name, e.g. `example.com`"),
|
|
135
|
+
},
|
|
136
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
137
|
+
handler: async (config, args) => {
|
|
138
|
+
const domain = String(args.domain).toLowerCase();
|
|
139
|
+
return await call(config, `/dns/getDnssecRecords/${encodeURIComponent(domain)}`, {
|
|
140
|
+
method: "GET",
|
|
141
|
+
});
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
const list_transfers = {
|
|
145
|
+
name: "list_transfers",
|
|
146
|
+
description: "List all in-progress and recent inbound domain transfers for the authenticated account. Returns each transfer's domain, status (`NEW`, `PENDINGAUTH`, `PENDINGSUBMIT`, `PENDINGTRANSFER`, `DONE`, `CANCELED`, etc.), and create date. Use this to monitor transfers initiated by `transfer_domain`.",
|
|
147
|
+
inputSchema: {},
|
|
148
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
149
|
+
handler: async (config) => {
|
|
150
|
+
return await call(config, "/domain/listTransfers", { method: "GET" });
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
const get_transfer_status = {
|
|
154
|
+
name: "get_transfer_status",
|
|
155
|
+
description: "Get the status of a specific inbound transfer for a domain. Useful for polling after `transfer_domain` to know when the transfer completes (typical window: 5-7 days). Returns the same status values as `list_transfers`, plus a human-readable description.",
|
|
156
|
+
inputSchema: {
|
|
157
|
+
domain: z.string().min(3).describe("Domain whose transfer status to check, e.g. `example.com`"),
|
|
158
|
+
},
|
|
159
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
160
|
+
handler: async (config, args) => {
|
|
161
|
+
const domain = String(args.domain).toLowerCase();
|
|
162
|
+
return await call(config, `/domain/getTransfer/${encodeURIComponent(domain)}`, {
|
|
163
|
+
method: "GET",
|
|
164
|
+
});
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
const list_marketplace = {
|
|
168
|
+
name: "list_marketplace",
|
|
169
|
+
description: "Browse domains for sale on the Porkbun marketplace (aftermarket — domains owned by other users, not new registrations). Returns each listing's domain, TLD, SLD length, price (in USD), and listing date.\n\nFilters (all optional, server-side, mirroring the porkbun.com/marketplace UI):\n- `query`: SLD substring match. Multi-word queries: prefix a word with `-` to exclude it (e.g. `\"ai -test\"` matches SLDs containing 'ai' but not 'test').\n- `tlds`: limit to a list of TLDs (without the leading dot).\n- `sld_length_min`, `sld_length_max`: SLD character length bounds.\n- `sort_name`: `domain` | `tld` | `price` | `sld_length`.\n- `sort_direction`: `asc` | `desc`.\n\nWhen any filter is set, server returns up to 1000 matching listings. With no filters, supports raw pagination via `start` / `limit` (max 5000).",
|
|
170
|
+
inputSchema: {
|
|
171
|
+
query: z.string().optional().describe("SLD substring search. Use `-word` to exclude. Example: `'ai -test'`."),
|
|
172
|
+
tlds: z
|
|
173
|
+
.array(z.string())
|
|
174
|
+
.optional()
|
|
175
|
+
.describe("Limit to these TLDs (no leading dot). Example: `['com', 'io', 'ai']`."),
|
|
176
|
+
sld_length_min: z.number().int().min(1).optional().describe("Minimum SLD character length."),
|
|
177
|
+
sld_length_max: z.number().int().min(1).optional().describe("Maximum SLD character length."),
|
|
178
|
+
sort_name: z
|
|
179
|
+
.enum(["domain", "tld", "price", "sld_length"])
|
|
180
|
+
.optional()
|
|
181
|
+
.describe("Sort field. Default: `sld_length` asc when query is set, else `create_date` desc."),
|
|
182
|
+
sort_direction: z.enum(["asc", "desc"]).optional().describe("Sort direction."),
|
|
183
|
+
start: z.number().int().min(0).optional().describe("Pagination offset (no-filter mode only). Default 0."),
|
|
184
|
+
limit: z
|
|
185
|
+
.number()
|
|
186
|
+
.int()
|
|
187
|
+
.min(1)
|
|
188
|
+
.max(5000)
|
|
189
|
+
.optional()
|
|
190
|
+
.describe("Page size (no-filter mode only). Default 1000, max 5000."),
|
|
191
|
+
},
|
|
192
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
193
|
+
handler: async (config, args) => {
|
|
194
|
+
const body = {};
|
|
195
|
+
if (args.query !== undefined)
|
|
196
|
+
body.query = args.query;
|
|
197
|
+
if (Array.isArray(args.tlds) && args.tlds.length)
|
|
198
|
+
body.tlds = args.tlds;
|
|
199
|
+
if (args.sld_length_min !== undefined)
|
|
200
|
+
body.sldLengthMin = args.sld_length_min;
|
|
201
|
+
if (args.sld_length_max !== undefined)
|
|
202
|
+
body.sldLengthMax = args.sld_length_max;
|
|
203
|
+
if (args.sort_name !== undefined)
|
|
204
|
+
body.sortName = args.sort_name;
|
|
205
|
+
if (args.sort_direction !== undefined)
|
|
206
|
+
body.sortDirection = args.sort_direction;
|
|
207
|
+
if (args.start !== undefined)
|
|
208
|
+
body.start = args.start;
|
|
209
|
+
if (args.limit !== undefined)
|
|
210
|
+
body.limit = args.limit;
|
|
211
|
+
return await call(config, "/marketplace/getAll", { method: "POST", body });
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
const get_api_settings = {
|
|
215
|
+
name: "get_api_settings",
|
|
216
|
+
description: "Get the authenticated account's API spend control configuration: monthly spend limit, low-balance alert threshold, auto top-up settings, and current month's API spend total. All amounts are in cents. Useful for an agent to check budget headroom before initiating expensive operations — `register_domain` will be hard-blocked if it would push monthly spend over the configured limit.",
|
|
217
|
+
inputSchema: {},
|
|
218
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
219
|
+
handler: async (config) => {
|
|
220
|
+
return await call(config, "/account/apiSettings", { method: "GET" });
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
const list_glue_records = {
|
|
224
|
+
name: "list_glue_records",
|
|
225
|
+
description: "List glue records for a domain. Glue records associate a host (e.g. `ns1.example.com`) with one or more IP addresses at the registry, used when running your own nameservers on the same domain they serve. Returns the host, IPv4 addresses, and IPv6 addresses for each glue record.",
|
|
226
|
+
inputSchema: {
|
|
227
|
+
domain: z.string().min(3).describe("Domain to list glue records for, e.g. `example.com`"),
|
|
228
|
+
},
|
|
229
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
230
|
+
handler: async (config, args) => {
|
|
231
|
+
const domain = String(args.domain).toLowerCase();
|
|
232
|
+
return await call(config, `/domain/getGlue/${encodeURIComponent(domain)}`, { method: "GET" });
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
const create_glue_record = {
|
|
236
|
+
name: "create_glue_record",
|
|
237
|
+
description: "Create a glue record for a host on a domain. Used when running your own nameservers on the same domain they serve (e.g. `ns1.example.com` serving `example.com`). The `subdomain` is just the host part (e.g. `ns1`), not the full FQDN. Provide IPs as an array of IPv4 and/or IPv6 addresses. Idempotent.",
|
|
238
|
+
inputSchema: {
|
|
239
|
+
domain: z.string().min(3).describe("Parent domain, e.g. `example.com`"),
|
|
240
|
+
subdomain: z
|
|
241
|
+
.string()
|
|
242
|
+
.min(1)
|
|
243
|
+
.describe("Host portion only (no domain), e.g. `ns1`."),
|
|
244
|
+
ips: z
|
|
245
|
+
.array(z.string().min(7))
|
|
246
|
+
.min(1)
|
|
247
|
+
.describe("Array of IPv4 and/or IPv6 addresses to associate with the host."),
|
|
248
|
+
},
|
|
249
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
250
|
+
handler: async (config, args) => {
|
|
251
|
+
const domain = String(args.domain).toLowerCase();
|
|
252
|
+
const subdomain = String(args.subdomain).toLowerCase();
|
|
253
|
+
return await call(config, `/domain/createGlue/${encodeURIComponent(domain)}/${encodeURIComponent(subdomain)}`, { method: "POST", idempotent: true, body: { ips: args.ips } });
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
const update_glue_record = {
|
|
257
|
+
name: "update_glue_record",
|
|
258
|
+
description: "Update the IP addresses associated with an existing glue record. Replaces the full IP list — pass all IPs you want set, not just additions. Idempotent.",
|
|
259
|
+
inputSchema: {
|
|
260
|
+
domain: z.string().min(3).describe("Parent domain, e.g. `example.com`"),
|
|
261
|
+
subdomain: z.string().min(1).describe("Host portion only, e.g. `ns1`."),
|
|
262
|
+
ips: z
|
|
263
|
+
.array(z.string().min(7))
|
|
264
|
+
.min(1)
|
|
265
|
+
.describe("Full replacement set of IPv4/IPv6 addresses for the host."),
|
|
266
|
+
},
|
|
267
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
268
|
+
handler: async (config, args) => {
|
|
269
|
+
const domain = String(args.domain).toLowerCase();
|
|
270
|
+
const subdomain = String(args.subdomain).toLowerCase();
|
|
271
|
+
return await call(config, `/domain/updateGlue/${encodeURIComponent(domain)}/${encodeURIComponent(subdomain)}`, { method: "POST", idempotent: true, body: { ips: args.ips } });
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
const delete_glue_record = {
|
|
275
|
+
name: "delete_glue_record",
|
|
276
|
+
description: "Delete a glue record by host on a domain. Idempotent: deleting a non-existent glue record returns success.",
|
|
277
|
+
inputSchema: {
|
|
278
|
+
domain: z.string().min(3).describe("Parent domain, e.g. `example.com`"),
|
|
279
|
+
subdomain: z.string().min(1).describe("Host portion only, e.g. `ns1`."),
|
|
280
|
+
},
|
|
281
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
282
|
+
handler: async (config, args) => {
|
|
283
|
+
const domain = String(args.domain).toLowerCase();
|
|
284
|
+
const subdomain = String(args.subdomain).toLowerCase();
|
|
285
|
+
return await call(config, `/domain/deleteGlue/${encodeURIComponent(domain)}/${encodeURIComponent(subdomain)}`, { method: "POST", idempotent: true });
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
// ─── Domain lifecycle (write — these spend account credit) ──────────────────
|
|
289
|
+
const DNS_RECORD_TYPES = [
|
|
290
|
+
"A",
|
|
291
|
+
"AAAA",
|
|
292
|
+
"CNAME",
|
|
293
|
+
"MX",
|
|
294
|
+
"TXT",
|
|
295
|
+
"NS",
|
|
296
|
+
"ALIAS",
|
|
297
|
+
"SRV",
|
|
298
|
+
"TLSA",
|
|
299
|
+
"CAA",
|
|
300
|
+
"HTTPS",
|
|
301
|
+
"SVCB",
|
|
302
|
+
];
|
|
303
|
+
const register_domain = {
|
|
304
|
+
name: "register_domain",
|
|
305
|
+
description: "**Spends account credit.** Registers a new domain on the authenticated Porkbun account. The `cost` parameter must exactly match the current registration price returned by `check_domain` (in cents) — Porkbun rejects mismatched quotes. Workflow: call `check_domain` first to get availability + price, confirm the spend with the user, then call this. The order is idempotency-safe: retries within 24 hours via the same Idempotency-Key return the original response without re-charging. Premium domains, .uk, and a handful of registry-specific TLDs cannot be registered via API and must be done on the website. The account's email and phone number must be verified, and the account must have at least one prior registration order before this works.",
|
|
306
|
+
inputSchema: {
|
|
307
|
+
domain: z
|
|
308
|
+
.string()
|
|
309
|
+
.min(3)
|
|
310
|
+
.describe("Fully qualified domain name to register, e.g. `example.com`"),
|
|
311
|
+
cost: z
|
|
312
|
+
.number()
|
|
313
|
+
.int()
|
|
314
|
+
.positive()
|
|
315
|
+
.describe("Registration price in cents. Must match the value returned by `check_domain` for this domain (multiplied by years if duration > 1)."),
|
|
316
|
+
},
|
|
317
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
318
|
+
handler: async (config, args) => {
|
|
319
|
+
const domain = String(args.domain).toLowerCase();
|
|
320
|
+
return await call(config, `/domain/create/${encodeURIComponent(domain)}`, {
|
|
321
|
+
method: "POST",
|
|
322
|
+
idempotent: true,
|
|
323
|
+
body: {
|
|
324
|
+
cost: Number(args.cost),
|
|
325
|
+
agreeToTerms: "yes",
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
const renew_domain = {
|
|
331
|
+
name: "renew_domain",
|
|
332
|
+
description: "**Spends account credit.** Renews an existing domain in the authenticated account. The `cost` parameter must exactly match the current renewal price returned by `check_domain` (in cents). The domain must be opted in to API access (per-domain or global toggle in account settings). Domains registered within the last 30 days, or already renewed within the last 30 days, cannot be renewed yet — the API returns `RENEWAL_TOO_SOON`. Premium domain renewals are not supported via API. Idempotency-safe: retries within 24 hours don't double-charge.",
|
|
333
|
+
inputSchema: {
|
|
334
|
+
domain: z.string().min(3).describe("Domain name to renew, e.g. `example.com`. Must already be in your account."),
|
|
335
|
+
cost: z
|
|
336
|
+
.number()
|
|
337
|
+
.int()
|
|
338
|
+
.positive()
|
|
339
|
+
.describe("Renewal price in cents. Must match the value returned by `check_domain`."),
|
|
340
|
+
},
|
|
341
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
342
|
+
handler: async (config, args) => {
|
|
343
|
+
const domain = String(args.domain).toLowerCase();
|
|
344
|
+
return await call(config, `/domain/renew/${encodeURIComponent(domain)}`, {
|
|
345
|
+
method: "POST",
|
|
346
|
+
idempotent: true,
|
|
347
|
+
body: { cost: Number(args.cost) },
|
|
348
|
+
});
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
const transfer_domain = {
|
|
352
|
+
name: "transfer_domain",
|
|
353
|
+
description: "**Spends account credit.** Initiates a transfer of an external domain into Porkbun. Returns immediately with a `transferId`; the actual registry transfer takes 5-7 days for most TLDs. Use `get_transfer_status` to poll. Requires the auth/EPP code from the losing registrar. The `cost` must match the current transfer price from `check_domain`. .uk domains and a few TLDs do not support inbound API transfers. Idempotency-safe.",
|
|
354
|
+
inputSchema: {
|
|
355
|
+
domain: z.string().min(3).describe("Domain to transfer in, e.g. `example.com`"),
|
|
356
|
+
cost: z
|
|
357
|
+
.number()
|
|
358
|
+
.int()
|
|
359
|
+
.positive()
|
|
360
|
+
.describe("Transfer price in cents. Must match the value returned by `check_domain`."),
|
|
361
|
+
auth_code: z
|
|
362
|
+
.string()
|
|
363
|
+
.min(1)
|
|
364
|
+
.describe("Authorization (EPP) code from the losing registrar."),
|
|
365
|
+
},
|
|
366
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
367
|
+
handler: async (config, args) => {
|
|
368
|
+
const domain = String(args.domain).toLowerCase();
|
|
369
|
+
return await call(config, `/domain/transfer/${encodeURIComponent(domain)}`, {
|
|
370
|
+
method: "POST",
|
|
371
|
+
idempotent: true,
|
|
372
|
+
body: {
|
|
373
|
+
cost: Number(args.cost),
|
|
374
|
+
authCode: String(args.auth_code),
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
const update_auto_renew = {
|
|
380
|
+
name: "update_auto_renew",
|
|
381
|
+
description: "Turn auto-renewal on or off for a domain in the authenticated account. When auto-renew is on, Porkbun automatically charges your account credit at expiration. When off, you must manually renew or the domain expires. Idempotent.",
|
|
382
|
+
inputSchema: {
|
|
383
|
+
domain: z.string().min(3).describe("Domain to update, e.g. `example.com`"),
|
|
384
|
+
status: z
|
|
385
|
+
.enum(["on", "off"])
|
|
386
|
+
.describe("`on` enables auto-renew, `off` disables it."),
|
|
387
|
+
},
|
|
388
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
389
|
+
handler: async (config, args) => {
|
|
390
|
+
const domain = String(args.domain).toLowerCase();
|
|
391
|
+
return await call(config, `/domain/updateAutoRenew/${encodeURIComponent(domain)}`, {
|
|
392
|
+
method: "POST",
|
|
393
|
+
idempotent: true,
|
|
394
|
+
body: { status: args.status },
|
|
395
|
+
});
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
// ─── DNS writes ─────────────────────────────────────────────────────────────
|
|
399
|
+
const create_dns_record = {
|
|
400
|
+
name: "create_dns_record",
|
|
401
|
+
description: "Create a DNS record on a domain in the authenticated account. Returns the new record's `id` so it can be referenced by `update_dns_record` and `delete_dns_record`. For the `name` field: omit or pass empty string for the apex/root, otherwise pass the subdomain prefix only (e.g. `www`, not `www.example.com`). For MX and SRV records, set `prio` (priority). Free, doesn't spend account credit.",
|
|
402
|
+
inputSchema: {
|
|
403
|
+
domain: z.string().min(3).describe("Domain to add the record to, e.g. `example.com`"),
|
|
404
|
+
type: z
|
|
405
|
+
.enum(DNS_RECORD_TYPES)
|
|
406
|
+
.describe("Record type. Common: A, AAAA, CNAME, MX, TXT."),
|
|
407
|
+
content: z
|
|
408
|
+
.string()
|
|
409
|
+
.min(1)
|
|
410
|
+
.describe("Record value (e.g. an IP for A, a hostname for CNAME, the text body for TXT)."),
|
|
411
|
+
name: z
|
|
412
|
+
.string()
|
|
413
|
+
.optional()
|
|
414
|
+
.describe("Subdomain prefix (no domain). Empty string or omitted = apex. Examples: `www`, `mail`, `api.staging`."),
|
|
415
|
+
ttl: z
|
|
416
|
+
.number()
|
|
417
|
+
.int()
|
|
418
|
+
.min(60)
|
|
419
|
+
.optional()
|
|
420
|
+
.describe("Time-to-live in seconds. Minimum 60. Defaults to 600 if omitted."),
|
|
421
|
+
prio: z
|
|
422
|
+
.number()
|
|
423
|
+
.int()
|
|
424
|
+
.min(0)
|
|
425
|
+
.optional()
|
|
426
|
+
.describe("Priority — required for MX and SRV records, ignored otherwise."),
|
|
427
|
+
},
|
|
428
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
429
|
+
handler: async (config, args) => {
|
|
430
|
+
const domain = String(args.domain).toLowerCase();
|
|
431
|
+
const body = {
|
|
432
|
+
type: args.type,
|
|
433
|
+
content: args.content,
|
|
434
|
+
};
|
|
435
|
+
if (args.name !== undefined)
|
|
436
|
+
body.name = args.name;
|
|
437
|
+
if (args.ttl !== undefined)
|
|
438
|
+
body.ttl = String(args.ttl);
|
|
439
|
+
if (args.prio !== undefined)
|
|
440
|
+
body.prio = String(args.prio);
|
|
441
|
+
return await call(config, `/dns/create/${encodeURIComponent(domain)}`, {
|
|
442
|
+
method: "POST",
|
|
443
|
+
idempotent: true,
|
|
444
|
+
body,
|
|
445
|
+
});
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
const update_dns_record = {
|
|
449
|
+
name: "update_dns_record",
|
|
450
|
+
description: "Update an existing DNS record by its numeric `record_id` (obtained from `list_dns_records`). All fields except `record_id` and `domain` are optional — pass only the ones you want to change. Idempotent: applying the same update twice is a no-op.",
|
|
451
|
+
inputSchema: {
|
|
452
|
+
domain: z.string().min(3).describe("Domain the record belongs to, e.g. `example.com`"),
|
|
453
|
+
record_id: z
|
|
454
|
+
.string()
|
|
455
|
+
.min(1)
|
|
456
|
+
.describe("Numeric record ID (as a string). Get this from `list_dns_records`."),
|
|
457
|
+
type: z.enum(DNS_RECORD_TYPES).optional().describe("New record type (rarely changed)."),
|
|
458
|
+
content: z.string().min(1).optional().describe("New record value."),
|
|
459
|
+
name: z.string().optional().describe("New subdomain prefix (empty string = apex)."),
|
|
460
|
+
ttl: z.number().int().min(60).optional().describe("New TTL in seconds."),
|
|
461
|
+
prio: z.number().int().min(0).optional().describe("New priority (MX/SRV only)."),
|
|
462
|
+
},
|
|
463
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
464
|
+
handler: async (config, args) => {
|
|
465
|
+
const domain = String(args.domain).toLowerCase();
|
|
466
|
+
const recordId = String(args.record_id);
|
|
467
|
+
const body = {};
|
|
468
|
+
if (args.type !== undefined)
|
|
469
|
+
body.type = args.type;
|
|
470
|
+
if (args.content !== undefined)
|
|
471
|
+
body.content = args.content;
|
|
472
|
+
if (args.name !== undefined)
|
|
473
|
+
body.name = args.name;
|
|
474
|
+
if (args.ttl !== undefined)
|
|
475
|
+
body.ttl = String(args.ttl);
|
|
476
|
+
if (args.prio !== undefined)
|
|
477
|
+
body.prio = String(args.prio);
|
|
478
|
+
return await call(config, `/dns/edit/${encodeURIComponent(domain)}/${encodeURIComponent(recordId)}`, { method: "POST", idempotent: true, body });
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
const delete_dns_record = {
|
|
482
|
+
name: "delete_dns_record",
|
|
483
|
+
description: "Delete a single DNS record by its numeric `record_id` (obtained from `list_dns_records`). Idempotent: deleting an already-deleted record returns success. Free.",
|
|
484
|
+
inputSchema: {
|
|
485
|
+
domain: z.string().min(3).describe("Domain the record belongs to, e.g. `example.com`"),
|
|
486
|
+
record_id: z.string().min(1).describe("Numeric record ID (as a string)."),
|
|
487
|
+
},
|
|
488
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
489
|
+
handler: async (config, args) => {
|
|
490
|
+
const domain = String(args.domain).toLowerCase();
|
|
491
|
+
const recordId = String(args.record_id);
|
|
492
|
+
return await call(config, `/dns/delete/${encodeURIComponent(domain)}/${encodeURIComponent(recordId)}`, { method: "POST", idempotent: true });
|
|
493
|
+
},
|
|
494
|
+
};
|
|
495
|
+
// ─── DNSSEC writes ──────────────────────────────────────────────────────────
|
|
496
|
+
const create_dnssec_record = {
|
|
497
|
+
name: "create_dnssec_record",
|
|
498
|
+
description: "Submit a DNSSEC DS record to the registry for a domain. Use when you sign DNS yourself (custom nameservers running BIND/Knot/PowerDNS/etc.) and need to publish the chain-of-trust at the parent zone. Required: keyTag, algorithm, digestType, digest. Optional key-data fields for registries that require full DNSKEY (rare).",
|
|
499
|
+
inputSchema: {
|
|
500
|
+
domain: z.string().min(3).describe("Domain to add the DS record to."),
|
|
501
|
+
keyTag: z.string().describe("DNSSEC key tag (16-bit identifier of the key)."),
|
|
502
|
+
alg: z
|
|
503
|
+
.string()
|
|
504
|
+
.describe("Algorithm number, e.g. `13` for ECDSA P-256 SHA-256, `8` for RSA SHA-256."),
|
|
505
|
+
digestType: z.string().describe("Digest type, e.g. `2` for SHA-256, `4` for SHA-384."),
|
|
506
|
+
digest: z.string().describe("Hex-encoded DS digest value."),
|
|
507
|
+
maxSigLife: z.string().optional().describe("Maximum signature lifetime in seconds (registry-specific, optional)."),
|
|
508
|
+
keyDataFlags: z.string().optional().describe("DNSKEY flags (optional — typically 256 or 257)."),
|
|
509
|
+
keyDataProtocol: z.string().optional().describe("DNSKEY protocol (optional — almost always 3)."),
|
|
510
|
+
keyDataAlgo: z.string().optional().describe("DNSKEY algorithm (optional)."),
|
|
511
|
+
keyDataPubKey: z.string().optional().describe("Base64-encoded public key (optional)."),
|
|
512
|
+
},
|
|
513
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
514
|
+
handler: async (config, args) => {
|
|
515
|
+
const domain = String(args.domain).toLowerCase();
|
|
516
|
+
const body = {
|
|
517
|
+
keyTag: args.keyTag,
|
|
518
|
+
alg: args.alg,
|
|
519
|
+
digestType: args.digestType,
|
|
520
|
+
digest: args.digest,
|
|
521
|
+
};
|
|
522
|
+
for (const k of ["maxSigLife", "keyDataFlags", "keyDataProtocol", "keyDataAlgo", "keyDataPubKey"]) {
|
|
523
|
+
if (args[k] !== undefined)
|
|
524
|
+
body[k] = args[k];
|
|
525
|
+
}
|
|
526
|
+
return await call(config, `/dns/createDnssecRecord/${encodeURIComponent(domain)}`, {
|
|
527
|
+
method: "POST",
|
|
528
|
+
idempotent: true,
|
|
529
|
+
body,
|
|
530
|
+
});
|
|
531
|
+
},
|
|
532
|
+
};
|
|
533
|
+
const delete_dnssec_record = {
|
|
534
|
+
name: "delete_dnssec_record",
|
|
535
|
+
description: "Remove a DNSSEC DS record from the registry for a domain, identified by key tag. Use when retiring a key. Idempotent: deleting a non-existent key tag returns success.",
|
|
536
|
+
inputSchema: {
|
|
537
|
+
domain: z.string().min(3).describe("Domain to remove the DS record from."),
|
|
538
|
+
keyTag: z.string().describe("Key tag of the DS record to remove (from `list_dnssec_records`)."),
|
|
539
|
+
},
|
|
540
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
541
|
+
handler: async (config, args) => {
|
|
542
|
+
const domain = String(args.domain).toLowerCase();
|
|
543
|
+
const keyTag = String(args.keyTag);
|
|
544
|
+
return await call(config, `/dns/deleteDnssecRecord/${encodeURIComponent(domain)}/${encodeURIComponent(keyTag)}`, { method: "POST", idempotent: true });
|
|
545
|
+
},
|
|
546
|
+
};
|
|
547
|
+
// ─── URL forwarding writes ──────────────────────────────────────────────────
|
|
548
|
+
const create_url_forward = {
|
|
549
|
+
name: "create_url_forward",
|
|
550
|
+
description: "Add a URL forwarding rule for a domain. Forwards a subdomain (or apex if `subdomain` is empty/omitted) to an arbitrary destination URL. Useful for redirects without setting up a web server. Free.",
|
|
551
|
+
inputSchema: {
|
|
552
|
+
domain: z.string().min(3).describe("Domain to add the forward to, e.g. `example.com`"),
|
|
553
|
+
location: z
|
|
554
|
+
.string()
|
|
555
|
+
.url()
|
|
556
|
+
.describe("Destination URL to forward visitors to, e.g. `https://newsite.example.com`"),
|
|
557
|
+
type: z
|
|
558
|
+
.enum(["permanent", "temporary"])
|
|
559
|
+
.describe("`permanent` sends HTTP 301 (browsers cache); `temporary` is the configurable default redirect."),
|
|
560
|
+
includePath: z
|
|
561
|
+
.enum(["yes", "no"])
|
|
562
|
+
.describe("`yes` appends the request URI path to the forward target; `no` always sends to the bare destination."),
|
|
563
|
+
wildcard: z
|
|
564
|
+
.enum(["yes", "no"])
|
|
565
|
+
.describe("`yes` also forwards all sub-subdomains; `no` forwards only the exact subdomain."),
|
|
566
|
+
subdomain: z
|
|
567
|
+
.string()
|
|
568
|
+
.optional()
|
|
569
|
+
.describe("Subdomain prefix to forward. Empty/omitted = the apex (root domain). Examples: `www`, `shop`."),
|
|
570
|
+
},
|
|
571
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
572
|
+
handler: async (config, args) => {
|
|
573
|
+
const domain = String(args.domain).toLowerCase();
|
|
574
|
+
const body = {
|
|
575
|
+
location: args.location,
|
|
576
|
+
type: args.type,
|
|
577
|
+
includePath: args.includePath,
|
|
578
|
+
wildcard: args.wildcard,
|
|
579
|
+
};
|
|
580
|
+
if (args.subdomain !== undefined)
|
|
581
|
+
body.subdomain = args.subdomain;
|
|
582
|
+
return await call(config, `/domain/addUrlForward/${encodeURIComponent(domain)}`, {
|
|
583
|
+
method: "POST",
|
|
584
|
+
idempotent: true,
|
|
585
|
+
body,
|
|
586
|
+
});
|
|
587
|
+
},
|
|
588
|
+
};
|
|
589
|
+
const delete_url_forward = {
|
|
590
|
+
name: "delete_url_forward",
|
|
591
|
+
description: "Delete a URL forwarding rule by its `id` (obtained from `list_url_forwards`). Idempotent.",
|
|
592
|
+
inputSchema: {
|
|
593
|
+
domain: z.string().min(3).describe("Domain the forward belongs to."),
|
|
594
|
+
record_id: z
|
|
595
|
+
.string()
|
|
596
|
+
.min(1)
|
|
597
|
+
.describe("Numeric forward record ID from `list_url_forwards`."),
|
|
598
|
+
},
|
|
599
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
600
|
+
handler: async (config, args) => {
|
|
601
|
+
const domain = String(args.domain).toLowerCase();
|
|
602
|
+
const recordId = String(args.record_id);
|
|
603
|
+
return await call(config, `/domain/deleteUrlForward/${encodeURIComponent(domain)}/${encodeURIComponent(recordId)}`, { method: "POST", idempotent: true });
|
|
604
|
+
},
|
|
605
|
+
};
|
|
606
|
+
// ─── Nameservers ────────────────────────────────────────────────────────────
|
|
607
|
+
const update_nameservers = {
|
|
608
|
+
name: "update_nameservers",
|
|
609
|
+
description: "Replace the nameservers for a domain in the authenticated account. **This is a full replacement, not an append** — the supplied list becomes the complete set of nameservers. Most TLDs require 2-13 entries. Setting custom nameservers disables Porkbun's free DNS hosting for the domain. Idempotent: applying the same NS list twice is a no-op.",
|
|
610
|
+
inputSchema: {
|
|
611
|
+
domain: z.string().min(3).describe("Domain to update, e.g. `example.com`"),
|
|
612
|
+
nameservers: z
|
|
613
|
+
.array(z.string().min(3))
|
|
614
|
+
.min(2)
|
|
615
|
+
.max(13)
|
|
616
|
+
.describe("Full list of nameservers (e.g. `['ns1.example.com', 'ns2.example.com']`). Minimum 2, maximum 13."),
|
|
617
|
+
},
|
|
618
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
619
|
+
handler: async (config, args) => {
|
|
620
|
+
const domain = String(args.domain).toLowerCase();
|
|
621
|
+
return await call(config, `/domain/updateNs/${encodeURIComponent(domain)}`, {
|
|
622
|
+
method: "POST",
|
|
623
|
+
idempotent: true,
|
|
624
|
+
body: { ns: args.nameservers },
|
|
625
|
+
});
|
|
626
|
+
},
|
|
627
|
+
};
|
|
96
628
|
export const tools = [
|
|
629
|
+
// read — global / account
|
|
97
630
|
ping,
|
|
98
631
|
check_domain,
|
|
632
|
+
get_pricing,
|
|
633
|
+
list_marketplace,
|
|
99
634
|
list_domains,
|
|
100
635
|
get_balance,
|
|
101
|
-
|
|
636
|
+
get_api_settings,
|
|
637
|
+
// read — per-domain
|
|
638
|
+
get_nameservers,
|
|
102
639
|
list_dns_records,
|
|
640
|
+
list_dnssec_records,
|
|
641
|
+
list_url_forwards,
|
|
642
|
+
list_glue_records,
|
|
643
|
+
list_transfers,
|
|
644
|
+
get_transfer_status,
|
|
103
645
|
get_ssl_bundle,
|
|
646
|
+
// write — domain lifecycle (spend account credit)
|
|
647
|
+
register_domain,
|
|
648
|
+
renew_domain,
|
|
649
|
+
transfer_domain,
|
|
650
|
+
// write — domain settings
|
|
651
|
+
update_auto_renew,
|
|
652
|
+
update_nameservers,
|
|
653
|
+
// write — DNS
|
|
654
|
+
create_dns_record,
|
|
655
|
+
update_dns_record,
|
|
656
|
+
delete_dns_record,
|
|
657
|
+
// write — DNSSEC
|
|
658
|
+
create_dnssec_record,
|
|
659
|
+
delete_dnssec_record,
|
|
660
|
+
// write — URL forwarding
|
|
661
|
+
create_url_forward,
|
|
662
|
+
delete_url_forward,
|
|
663
|
+
// write — glue records
|
|
664
|
+
create_glue_record,
|
|
665
|
+
update_glue_record,
|
|
666
|
+
delete_glue_record,
|
|
104
667
|
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@porkbunllc/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Porkbun MCP server — exposes the Porkbun v3 API as tools for AI agents (Claude Desktop, Cursor, etc.)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"build": "tsc",
|
|
16
16
|
"start": "node dist/index.js",
|
|
17
17
|
"dev": "tsc --watch",
|
|
18
|
+
"smoke": "node scripts/smoke.mjs",
|
|
18
19
|
"prepublishOnly": "rm -rf dist && tsc"
|
|
19
20
|
},
|
|
20
21
|
"keywords": [
|