@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.)
65 lines (64 loc) • 3.83 kB
JavaScript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { tools } from "./tools.js";
export const SERVER_NAME = "porkbun-mcp";
export const SERVER_VERSION = "0.43.5";
// 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: 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;
}