UNPKG

@porkbunllc/mcp-server

Version:

Official Porkbun MCP server, built and maintained by Porkbun — exposes the Porkbun v3 API as tools for AI agents (ChatGPT, Claude, Cursor, etc.)

67 lines (66 loc) • 3.99 kB
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { tools } from "./tools.js"; export const SERVER_NAME = "porkbun-mcp"; export const SERVER_VERSION = "0.51.1"; // Human-readable display title for each tool, derived from its snake_case name // (domain acronyms kept upper-case). Every tool in the Connectors Directory must // carry a title alongside its read-only/destructive hint; deriving it here means // new tools get one automatically. A tool can still override via annotations.title. const TITLE_ACRONYMS = { dns: "DNS", dnssec: "DNSSEC", ssl: "SSL", url: "URL", api: "API", wp: "WP", tld: "TLD", ip: "IP", ns: "NS", id: "ID", mcp: "MCP", }; function deriveTitle(name) { return name .split("_") .map((w) => TITLE_ACRONYMS[w] ?? w.charAt(0).toUpperCase() + w.slice(1)) .join(" "); } // Sent on initialize by every entry point (stdio and each hosted path); a // profile's own instructions are appended. Kept to guidance about these tools: // the directories reject instructions that steer the assistant elsewhere. // Why the deploy line exists: an assistant that has just written a site for the // user tends to finish with manual "upload it in your control panel" steps, // never realising deploy_site is right there, so the user has to ask. export const BASE_INSTRUCTIONS = "Porkbun tools act on the signed-in user's Porkbun account: domains, DNS, hosting and related settings. " + "Building a website: if the user asks you to build or change a website for a domain in their Porkbun account, or mentions Porkbun hosting, the last step is publishing it with deploy_site. Offer that when the files are ready; do not end with steps for uploading through the Porkbun dashboard. " + "Check get_hosting first: it must be ACTIVE (if the domain has no active hosting, say so instead of uploading). Before uploading, use list_hosting_files to see what is already there, say which files will be replaced (a new site usually has a placeholder index.html), and wait for the user's OK."; /** * One MCP server with every tool registered against the given config source. * * Shared by both entry points so they cannot drift: the local stdio package * (index.ts, credentials from the environment) and the hosted connector * (http.ts, a fresh server per request bound to that request's bearer token). */ export function buildServer(getConfig, opts = {}) { const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION }, { instructions: opts.instructions ? `${BASE_INSTRUCTIONS}\n\n${opts.instructions}` : BASE_INSTRUCTIONS }); for (const tool of tools) { if (opts.exclude?.has(tool.name)) continue; const title = tool.annotations?.title ?? deriveTitle(tool.name); server.registerTool(tool.name, { title, description: (() => { const base = (opts.hosted && tool.hostedDescription) || tool.description; return opts.describe ? opts.describe(tool.name, base) : base; })(), inputSchema: opts.omitParams?.length ? Object.fromEntries(Object.entries(tool.inputSchema).filter(([k]) => !opts.omitParams.includes(k))) : tool.inputSchema, annotations: { ...(tool.annotations ?? {}), title }, }, async (args) => { try { const raw = await tool.handler(getConfig(), args); const result = opts.transformResult ? opts.transformResult(tool.name, raw) : raw; if (tool.toContent) return { content: tool.toContent(result) }; return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } catch (err) { const message = err instanceof Error ? err.message : String(err); return { isError: true, content: [{ type: "text", text: message }] }; } }); } return server; }