import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod/v3";
import * as dotenv from "dotenv";
// Simple argument parsing for --dotenv-path
const args = process.argv.slice(2);
const dotenvPathIndex = args.indexOf("--dotenv-path");
const customDotenvPath =
dotenvPathIndex !== -1 ? args[dotenvPathIndex + 1] : undefined;
// Load environment variables from custom path if specified
if (customDotenvPath) {
dotenv.config({ path: customDotenvPath });
} else {
dotenv.config();
}
// Constants
const PORKBUN_API_BASE = "https://api.porkbun.com/api/json/v3";
const PORKBUN_API_KEY = process.env.PORKBUN_API_KEY;
const PORKBUN_SECRET_API_KEY = process.env.PORKBUN_SECRET_API_KEY;
const USER_AGENT = "porkbun-domain-availability-mcp-server/1.0.0";
if (!PORKBUN_API_KEY || !PORKBUN_SECRET_API_KEY) {
console.error(
"Error: PORKBUN_API_KEY and PORKBUN_SECRET_API_KEY environment variables are required",
);
console.error("Set them in your environment or create a .env file with:");
console.error("PORKBUN_API_KEY=your_api_key");
console.error("PORKBUN_SECRET_API_KEY=your_secret_key");
process.exit(1);
}
// Types
interface PorkbunResponse {
status: string;
message?: string;
yourIp?: string;
response?: {
avail: string;
type: string;
price: string;
firstYearPromo?: string;
regularPrice?: string;
premium: string;
additional?: {
renewal: {
type: string;
price: string;
regularPrice: string;
};
transfer: {
type: string;
price: string;
regularPrice: string;
};
};
};
limits?: {
TTL: string;
limit: string;
used: string;
naturalLanguage: string;
};
}
interface PorkbunRequestArgs {
[key: string]: any;
}
// API request function
async function porkbunRequest(
endpoint: string,
args: PorkbunRequestArgs = {},
authenticated: boolean = true,
): Promise<PorkbunResponse> {
const url = `${PORKBUN_API_BASE}/${endpoint}`;
let bodyData: PorkbunRequestArgs = { ...args };
if (authenticated) {
bodyData = {
apikey: PORKBUN_API_KEY,
secretapikey: PORKBUN_SECRET_API_KEY,
...bodyData,
};
}
try {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"User-Agent": USER_AGENT,
},
body: JSON.stringify(bodyData),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = (await response.json()) as PorkbunResponse;
return result;
} catch (error) {
console.error(`Error making request to ${endpoint}:`, error);
throw error;
}
}
// MCP Server setup
const server = new McpServer({
name: "porkbun-domain-availability",
version: "1.0.0",
});
// Check domain availability tool
server.registerTool(
"check_domain_availability",
{
title: "Check Domain Availability",
description:
"Checks the availability of a domain name and returns pricing information. Please note that domain checks are rate limited to 1 check per 10 seconds.",
inputSchema: z.object({
domain: z.string().describe("The domain name to check for availability"),
}),
outputSchema: z.object({
result: z.string(),
}),
},
async (args) => {
try {
const { domain } = args;
const response = await porkbunRequest(`domain/checkDomain/${domain}`);
if (response.status === "SUCCESS" && response.response) {
const domainInfo = response.response;
let availabilityText = `Domain: ${domain}\n`;
availabilityText += `Available: ${domainInfo.avail === "yes" ? "Yes" : "No"}\n`;
availabilityText += `Type: ${domainInfo.type}\n`;
availabilityText += `Premium: ${domainInfo.premium === "yes" ? "Yes" : "No"}\n`;
availabilityText += `Price: $${domainInfo.price}\n`;
if (domainInfo.firstYearPromo === "yes") {
availabilityText += `First Year Promotion: Yes\n`;
availabilityText += `Regular Price: $${domainInfo.regularPrice}\n`;
}
if (domainInfo.additional) {
availabilityText += `\nAdditional Pricing:\n`;
availabilityText += `Renewal: $${domainInfo.additional.renewal.price} (Regular: $${domainInfo.additional.renewal.regularPrice})\n`;
availabilityText += `Transfer: $${domainInfo.additional.transfer.price} (Regular: $${domainInfo.additional.transfer.regularPrice})\n`;
}
if (response.limits) {
availabilityText += `\nRate Limit Info:\n`;
availabilityText += `Limit: ${response.limits.naturalLanguage}\n`;
availabilityText += `TTL: ${response.limits.TTL} seconds\n`;
}
const output = { result: availabilityText };
return {
content: [{ type: "text", text: availabilityText }],
structuredContent: output,
};
} else {
const output = {
result: `Error checking domain availability: ${response.message || "Unknown error"}`,
};
return {
content: [{ type: "text", text: output.result }],
isError: true,
structuredContent: output,
};
}
} catch (error) {
const output = {
result: `Failed to check domain availability: ${error instanceof Error ? error.message : "Unknown error"}`,
};
return {
content: [{ type: "text", text: output.result }],
isError: true,
structuredContent: output,
};
}
},
);
// Bulk check domain availability tool
server.registerTool(
"bulk_check_domains_availability",
{
title: "Bulk Check Domains Availability",
description:
"Checks the availability of up to 10 domains names at once. WARNING: Due to Porkbun API rate limits (1 check per 10 seconds), this tool has a very long runtime. For example: 5 domains = ~50 seconds, 10 domains = ~100 seconds (1.7 minutes). The bulk tool provides better user experience and consolidated results compared to making multiple single domain check calls.",
inputSchema: z.object({
domains: z
.array(z.string())
.describe("Array of domain names to check for availability"),
}),
outputSchema: z.object({
result: z.string(),
}),
},
async (args) => {
try {
const { domains } = args;
if (domains.length === 0) {
const output = { result: "No domains provided for checking" };
return {
content: [{ type: "text", text: output.result }],
isError: true,
structuredContent: output,
};
}
if (domains.length > 10) {
const output = {
result:
"Maximum 10 domains can be checked at once due to rate limiting",
};
return {
content: [{ type: "text", text: output.result }],
isError: true,
structuredContent: output,
};
}
const results: string[] = [];
results.push(`Bulk Domain Availability Check Results:\n`);
// Check each domain (sequential to respect rate limits - 10 seconds per check)
for (let i = 0; i < domains.length; i++) {
const domain = domains[i];
try {
const response = await porkbunRequest(`domain/checkDomain/${domain}`);
if (response.status === "SUCCESS" && response.response) {
const domainInfo = response.response;
results.push(`${i + 1}. ${domain}:`);
results.push(
` Available: ${domainInfo.avail === "yes" ? "Yes" : "No"}`,
);
results.push(` Type: ${domainInfo.type}`);
results.push(
` Premium: ${domainInfo.premium === "yes" ? "Yes" : "No"}`,
);
results.push(` Price: $${domainInfo.price}`);
if (domainInfo.firstYearPromo === "yes") {
results.push(
` Regular Price: $${domainInfo.regularPrice} (First year promo)`,
);
}
results.push(""); // Empty line for readability
} else {
results.push(
`${i + 1}. ${domain}: Error - ${response.message || "Unknown error"}`,
);
results.push("");
}
// Add 10-second delay between requests to respect Porkbun API rate limits
if (i < domains.length - 1) {
await new Promise((resolve) => setTimeout(resolve, 10000));
}
} catch (error) {
results.push(
`${i + 1}. ${domain}: Error - ${error instanceof Error ? error.message : "Unknown error"}`,
);
results.push("");
}
}
const output = { result: results.join("\n") };
return {
content: [{ type: "text", text: output.result }],
structuredContent: output,
};
} catch (error) {
const output = {
result: `Failed to perform bulk domain check: ${error instanceof Error ? error.message : "Unknown error"}`,
};
return {
content: [{ type: "text", text: output.result }],
isError: true,
structuredContent: output,
};
}
},
);
// Main function
async function main() {
try {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Porkbun Domain Availability MCP Server running on stdio");
} catch (error) {
console.error(
"Fatal error initializing Porkbun Domain Availability MCP Server:",
error,
);
process.exit(1);
}
}
main();