← back to Cloudflare Dns Mcp
index.js
168 lines
#!/usr/bin/env node
// cloudflare-dns-mcp — owned local MCP for Cloudflare DNS record CRUD.
// Token source of truth: env CLOUDFLARE_API_TOKEN, else the secrets master
// (~/Projects/secrets-manager/.env). The token is never written into MCP config.
import { readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
ListToolsRequestSchema,
CallToolRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
// ---- token loading ---------------------------------------------------------
function loadToken() {
if (process.env.CLOUDFLARE_API_TOKEN) return process.env.CLOUDFLARE_API_TOKEN.trim();
try {
const envPath = join(homedir(), "Projects", "secrets-manager", ".env");
const line = readFileSync(envPath, "utf8")
.split("\n")
.find((l) => l.startsWith("CLOUDFLARE_API_TOKEN="));
if (line) return line.slice("CLOUDFLARE_API_TOKEN=".length).trim().replace(/^["']|["']$/g, "");
} catch { /* fall through */ }
return null;
}
const API = "https://api.cloudflare.com/client/v4";
async function cf(path, { method = "GET", body } = {}) {
// Load per-request so a rotated token is picked up without restarting the server.
const token = loadToken();
if (!token) throw new Error("CLOUDFLARE_API_TOKEN not found (env or secrets master).");
const res = await fetch(API + path, {
method,
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
body: body ? JSON.stringify(body) : undefined,
});
const data = await res.json().catch(() => ({}));
if (!data.success) {
const errs = (data.errors || []).map((e) => `${e.code}: ${e.message}`).join("; ");
throw new Error(`CF API ${res.status} — ${errs || "unknown error"}`);
}
return data.result;
}
// Accept a zone id (32 hex) or a zone name; resolve name → id.
async function resolveZoneId(zone) {
if (/^[0-9a-f]{32}$/i.test(zone)) return zone;
const zones = await cf(`/zones?name=${encodeURIComponent(zone)}&per_page=1`);
if (!zones.length) throw new Error(`Zone not found: ${zone}`);
return zones[0].id;
}
// ---- tool definitions ------------------------------------------------------
const TOOLS = [
{
name: "cf_list_zones",
description: "List Cloudflare zones (optionally filter by exact name). Returns id, name, status.",
inputSchema: {
type: "object",
properties: { name: { type: "string", description: "Exact zone name filter, e.g. designerwallcoverings.com" } },
},
},
{
name: "cf_list_dns_records",
description: "List DNS records in a zone. zone = zone name or id. Optional type/name filters.",
inputSchema: {
type: "object",
properties: {
zone: { type: "string", description: "Zone name or zone id" },
type: { type: "string", description: "Record type filter (A, CNAME, TXT, MX…)" },
name: { type: "string", description: "Record name filter (exact FQDN)" },
},
required: ["zone"],
},
},
{
name: "cf_create_dns_record",
description: "Create a DNS record. Returns the created record incl. its id.",
inputSchema: {
type: "object",
properties: {
zone: { type: "string", description: "Zone name or zone id" },
type: { type: "string", description: "A, AAAA, CNAME, TXT, MX, NS, SRV, CAA…" },
name: { type: "string", description: "Record name (FQDN or @ for apex)" },
content: { type: "string", description: "Record value (IP, target, text…)" },
ttl: { type: "number", description: "TTL seconds; 1 = auto (default 1)" },
proxied: { type: "boolean", description: "Cloudflare proxy (orange cloud); default false" },
priority: { type: "number", description: "Priority (MX/SRV)" },
},
required: ["zone", "type", "name", "content"],
},
},
{
name: "cf_update_dns_record",
description: "Update an existing DNS record by record_id. Only pass fields you want changed.",
inputSchema: {
type: "object",
properties: {
zone: { type: "string", description: "Zone name or zone id" },
record_id: { type: "string", description: "DNS record id (from cf_list_dns_records)" },
type: { type: "string" }, name: { type: "string" }, content: { type: "string" },
ttl: { type: "number" }, proxied: { type: "boolean" }, priority: { type: "number" },
},
required: ["zone", "record_id"],
},
},
{
name: "cf_delete_dns_record",
description: "DELETE a DNS record by record_id. Destructive — deletes the record from the live zone.",
inputSchema: {
type: "object",
properties: {
zone: { type: "string", description: "Zone name or zone id" },
record_id: { type: "string", description: "DNS record id to delete" },
},
required: ["zone", "record_id"],
},
},
];
// ---- handlers --------------------------------------------------------------
const server = new Server(
{ name: "cloudflare-dns-mcp", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
server.setRequestHandler(CallToolRequestSchema, async (req) => {
const { name, arguments: a = {} } = req.params;
try {
let result;
if (name === "cf_list_zones") {
const q = a.name ? `?name=${encodeURIComponent(a.name)}` : "?per_page=50";
const zones = await cf(`/zones${q}`);
result = zones.map((z) => ({ id: z.id, name: z.name, status: z.status }));
} else if (name === "cf_list_dns_records") {
const zid = await resolveZoneId(a.zone);
const params = new URLSearchParams({ per_page: "100" });
if (a.type) params.set("type", a.type);
if (a.name) params.set("name", a.name);
const recs = await cf(`/zones/${zid}/dns_records?${params}`);
result = recs.map((r) => ({ id: r.id, type: r.type, name: r.name, content: r.content, ttl: r.ttl, proxied: r.proxied }));
} else if (name === "cf_create_dns_record") {
const zid = await resolveZoneId(a.zone);
const body = { type: a.type, name: a.name, content: a.content, ttl: a.ttl ?? 1, proxied: a.proxied ?? false };
if (a.priority != null) body.priority = a.priority;
result = await cf(`/zones/${zid}/dns_records`, { method: "POST", body });
} else if (name === "cf_update_dns_record") {
const zid = await resolveZoneId(a.zone);
const body = {};
for (const k of ["type", "name", "content", "ttl", "proxied", "priority"]) if (a[k] != null) body[k] = a[k];
result = await cf(`/zones/${zid}/dns_records/${a.record_id}`, { method: "PATCH", body });
} else if (name === "cf_delete_dns_record") {
const zid = await resolveZoneId(a.zone);
result = await cf(`/zones/${zid}/dns_records/${a.record_id}`, { method: "DELETE" });
} else {
throw new Error(`Unknown tool: ${name}`);
}
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
} catch (err) {
return { content: [{ type: "text", text: `ERROR: ${err.message}` }], isError: true };
}
});
await server.connect(new StdioServerTransport());