@porkbunllc/mcp-server 0.2.0 → 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 CHANGED
@@ -2,38 +2,59 @@
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.2full domain lifecycle (register, renew, transfer) plus DNS and nameserver writes. All write operations attach an `Idempotency-Key` automatically, so retries within 24 hours don't double-charge.
5
+ > **Status:** v0.3.1covers 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.2)
7
+ ## What's included (v0.3.1 — 30 tools)
8
8
 
9
- **Read tools**
9
+ **Read tools (free, no spend, no state changes)**
10
10
 
11
11
  | Tool | Description |
12
12
  |---|---|
13
13
  | `ping` | Verify API connectivity and credentials |
14
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 |
15
17
  | `list_domains` | Paginate through domains in the authenticated account |
16
18
  | `get_balance` | Get account credit balance |
17
- | `get_pricing` | Get registration/renewal/transfer pricing for all TLDs |
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 |
18
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 |
19
27
  | `get_ssl_bundle` | Retrieve the free Porkbun-issued SSL bundle for a domain |
20
28
 
21
- **Write tools (spend account credit — register/renew/transfer)**
29
+ **Domain lifecycle writes (spend account credit)**
22
30
 
23
31
  | Tool | Description |
24
32
  |---|---|
25
- | `register_domain` | Register a new domain — workflow: `check_domain` first to confirm price |
33
+ | `register_domain` | Register a new domain — call `check_domain` first to confirm price |
26
34
  | `renew_domain` | Renew an existing domain |
27
35
  | `transfer_domain` | Initiate an inbound transfer (returns transferId; takes 5-7 days) |
28
36
 
29
- **DNS and nameserver writes (free)**
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)**
30
45
 
31
46
  | Tool | Description |
32
47
  |---|---|
33
48
  | `create_dns_record` | Create a new DNS record (A, AAAA, CNAME, MX, TXT, etc.) |
34
49
  | `update_dns_record` | Update an existing DNS record by its ID |
35
50
  | `delete_dns_record` | Delete a DNS record by its ID |
36
- | `update_nameservers` | Replace the nameserver list for a domain (full replace, not append) |
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 |
37
58
 
38
59
  ## Install
39
60
 
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.2.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.
package/dist/tools.js CHANGED
@@ -101,6 +101,190 @@ const get_ssl_bundle = {
101
101
  return await call(config, `/ssl/retrieve/${encodeURIComponent(domain)}`, { method: "GET" });
102
102
  },
103
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
+ };
104
288
  // ─── Domain lifecycle (write — these spend account credit) ──────────────────
105
289
  const DNS_RECORD_TYPES = [
106
290
  "A",
@@ -192,6 +376,25 @@ const transfer_domain = {
192
376
  });
193
377
  },
194
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
+ };
195
398
  // ─── DNS writes ─────────────────────────────────────────────────────────────
196
399
  const create_dns_record = {
197
400
  name: "create_dns_record",
@@ -289,6 +492,117 @@ const delete_dns_record = {
289
492
  return await call(config, `/dns/delete/${encodeURIComponent(domain)}/${encodeURIComponent(recordId)}`, { method: "POST", idempotent: true });
290
493
  },
291
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
+ };
292
606
  // ─── Nameservers ────────────────────────────────────────────────────────────
293
607
  const update_nameservers = {
294
608
  name: "update_nameservers",
@@ -312,22 +626,42 @@ const update_nameservers = {
312
626
  },
313
627
  };
314
628
  export const tools = [
315
- // read
629
+ // read — global / account
316
630
  ping,
317
631
  check_domain,
632
+ get_pricing,
633
+ list_marketplace,
318
634
  list_domains,
319
635
  get_balance,
320
- get_pricing,
636
+ get_api_settings,
637
+ // read — per-domain
638
+ get_nameservers,
321
639
  list_dns_records,
640
+ list_dnssec_records,
641
+ list_url_forwards,
642
+ list_glue_records,
643
+ list_transfers,
644
+ get_transfer_status,
322
645
  get_ssl_bundle,
323
- // write — domain lifecycle
646
+ // write — domain lifecycle (spend account credit)
324
647
  register_domain,
325
648
  renew_domain,
326
649
  transfer_domain,
650
+ // write — domain settings
651
+ update_auto_renew,
652
+ update_nameservers,
327
653
  // write — DNS
328
654
  create_dns_record,
329
655
  update_dns_record,
330
656
  delete_dns_record,
331
- // write — nameservers
332
- update_nameservers,
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,
333
667
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@porkbunllc/mcp-server",
3
- "version": "0.2.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": {