All files / src/tools personal-nameservers.ts

100% Statements 28/28
100% Branches 6/6
100% Functions 6/6
100% Lines 28/28

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128            3x 31x                         4x 4x 4x   3x 1x     2x       3x           1x         31x                           3x 3x 3x 2x         1x         31x                             2x 2x 2x   1x       1x         31x                               2x 2x 2x   1x       1x          
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import * as z from "zod/v4";
import type { SpaceshipClient } from "../spaceship-client.js";
import { normalizeDomain } from "../dns-utils.js";
import { toTextResult, toErrorResult } from "../tool-result.js";
 
export const registerPersonalNameserverTools = (server: McpServer, client: SpaceshipClient): void => {
  server.registerTool(
    "list_personal_nameservers",
    {
      title: "List Personal Nameservers",
      description:
        "List personal (vanity) nameservers configured for a domain (e.g. ns1.yourdomain.com, ns2.yourdomain.com).",
      annotations: { readOnlyHint: true, openWorldHint: true },
 
      inputSchema: z.object({
        domain: z.string().min(4).max(255).describe("The domain name to list personal nameservers for."),
      }),
    },
    async ({ domain }) => {
      try {
        const normalizedDomain = normalizeDomain(domain);
        const nameservers = await client.listPersonalNameservers(normalizedDomain);
 
        if (nameservers.length === 0) {
          return toTextResult(`No personal nameservers configured for ${normalizedDomain}`);
        }
 
        return toTextResult(
          [
            `Personal nameservers for ${normalizedDomain}:`,
            ...nameservers.map(
              (ns) => `  - ${ns.host}${ns.ips?.length ? ` (${ns.ips.join(", ")})` : ""}`,
            ),
          ].join("\n"),
          { nameservers } as Record<string, unknown>,
        );
      } catch (error) {
        return toErrorResult(error);
      }
    },
  );
 
  server.registerTool(
    "get_personal_nameserver",
    {
      title: "Get Personal Nameserver",
      description:
        "Get details of a single personal (vanity) nameserver by hostname, including its IP addresses.",
      annotations: { readOnlyHint: true, openWorldHint: true },
 
      inputSchema: z.object({
        domain: z.string().min(4).max(255).describe("The parent domain name."),
        host: z.string().min(1).describe('The nameserver hostname (e.g. "ns1.yourdomain.com").'),
      }),
    },
    async ({ domain, host }) => {
      try {
        const normalizedDomain = normalizeDomain(domain);
        const ns = await client.getPersonalNameserver(normalizedDomain, host);
        return toTextResult(
          `Personal nameserver: ${ns.host}${ns.ips?.length ? ` (${ns.ips.join(", ")})` : ""}`,
          ns as unknown as Record<string, unknown>,
        );
      } catch (error) {
        return toErrorResult(error);
      }
    },
  );
 
  server.registerTool(
    "update_personal_nameserver",
    {
      title: "Update Personal Nameserver",
      description:
        "Create or update a personal nameserver host with its IP addresses. These are glue records that map a nameserver hostname (e.g. ns1.yourdomain.com) to IP addresses at the registry level.",
      annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
 
      inputSchema: z.object({
        domain: z.string().min(4).max(255).describe("The parent domain name."),
        host: z.string().min(1).describe("The nameserver hostname (e.g. \"ns1.yourdomain.com\")."),
        ips: z.array(z.string().min(1)).min(1).describe("Array of IP addresses for the nameserver glue record."),
      }),
    },
    async ({ domain, host, ips }) => {
      try {
        const normalizedDomain = normalizeDomain(domain);
        await client.updatePersonalNameserver(normalizedDomain, host, ips);
 
        return toTextResult(
          `Updated personal nameserver ${host} for ${normalizedDomain} with IPs: ${ips.join(", ")}`,
        );
      } catch (error) {
        return toErrorResult(error);
      }
    },
  );
 
  server.registerTool(
    "delete_personal_nameserver",
    {
      title: "Delete Personal Nameserver",
      description:
        "Delete a personal nameserver host from a domain. This removes the glue record at the registry. " +
        "WARNING: If this nameserver is actively used by any domain, those domains will lose DNS resolution and experience downtime. " +
        "Always confirm with the user before calling this tool.",
      annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
 
      inputSchema: z.object({
        domain: z.string().min(4).max(255).describe("The parent domain name."),
        host: z.string().min(1).describe("The nameserver hostname to delete (e.g. \"ns1.yourdomain.com\")."),
      }),
    },
    async ({ domain, host }) => {
      try {
        const normalizedDomain = normalizeDomain(domain);
        await client.deletePersonalNameserver(normalizedDomain, host);
 
        return toTextResult(
          `Deleted personal nameserver ${host} from ${normalizedDomain}`,
        );
      } catch (error) {
        return toErrorResult(error);
      }
    },
  );
};