import {
McpServer,
ResourceTemplate,
} from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { join } from "path";
import Baker from "cronbake";
import { BunSSEServerTransport } from "./transport.js";
import {
checkDomain,
checkDomainsBulk,
generateSuggestions,
getWhoisInfo,
findDomainsAcrossTLDs,
POPULAR_TLDS,
type DomainCheckResult,
} from "./services/domain.js";
import { updateWhoisServers } from "./cron/update-whois.js";
// Factory function to create a configured MCP server
function createServer(): McpServer {
const server = new McpServer({
name: "DomainFinder",
version: "1.0.0",
});
// Tool: Check single domain availability
server.registerTool(
"check_domain",
{
title: "Check Domain Availability",
description:
"Check if a single domain name is available for registration. " +
"Uses DNS lookup, RDAP, and WHOIS to verify availability. " +
"Returns the domain status: AVAILABLE, TAKEN, or UNKNOWN. " +
"Example: check_domain({ domain: 'myapp.com' })",
inputSchema: {
domain: z
.string()
.describe(
"Full domain name including TLD to check (e.g. 'example.com', 'startup.io', 'brand.ai')"
),
},
annotations: {
title: "Check Domain Availability",
readOnlyHint: true,
openWorldHint: true,
},
},
async ({ domain }) => {
const result = await checkDomain(domain);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
}
);
// Tool: Check multiple domains at once
server.registerTool(
"check_domains_bulk",
{
title: "Bulk Domain Availability Check",
description:
"Check availability of multiple domain names in parallel. " +
"Returns a summary with counts of available, taken, and unknown domains plus detailed results. " +
"Use this when you need to check several specific domains at once. " +
"Example: check_domains_bulk({ domains: ['startup.com', 'startup.io', 'startup.dev'] })",
inputSchema: {
domains: z
.array(z.string())
.describe(
"Array of full domain names to check (e.g. ['example.com', 'example.net', 'example.io'])"
),
},
annotations: {
title: "Bulk Domain Availability Check",
readOnlyHint: true,
openWorldHint: true,
},
},
async ({ domains }) => {
const results = await checkDomainsBulk(domains);
const summary = {
total: results.length,
available: results.filter((r) => r.status === "AVAILABLE").length,
taken: results.filter((r) => r.status === "TAKEN").length,
unknown: results.filter((r) => r.status === "UNKNOWN").length,
results,
};
return {
content: [
{
type: "text",
text: JSON.stringify(summary, null, 2),
},
],
};
}
);
// Tool: Generate domain suggestions
server.registerTool(
"suggest_domains",
{
title: "Generate Domain Name Suggestions",
description:
"Generate creative domain name suggestions based on a keyword or brand name. " +
"Produces variations using popular TLDs, common prefixes (get, my, try, use), " +
"common suffixes (app, hq, hub, pro, ai), and hyphenated forms. " +
"Optionally checks availability of all suggestions. " +
"Use this when the user needs domain name ideas for a project or brand. " +
"Example: suggest_domains({ keyword: 'coolstartup', checkAvailability: true })",
inputSchema: {
keyword: z
.string()
.describe(
"The keyword, brand name, or project name to generate domain suggestions for"
),
tlds: z
.array(z.string())
.optional()
.describe(
"Optional list of TLDs to use for suggestions (defaults to: com, net, org, io, co, ai, dev, app, tech, xyz)"
),
checkAvailability: z
.boolean()
.optional()
.describe(
"Whether to check availability of all generated suggestions (default: true). Set to false for faster results without availability checks."
),
},
annotations: {
title: "Generate Domain Name Suggestions",
readOnlyHint: true,
openWorldHint: true,
},
},
async ({ keyword, tlds, checkAvailability = true }) => {
const suggestions = generateSuggestions(keyword, tlds || POPULAR_TLDS);
if (!checkAvailability) {
return {
content: [
{
type: "text",
text: JSON.stringify({ keyword, suggestions }, null, 2),
},
],
};
}
// Check availability of all suggestions
const results = await checkDomainsBulk(suggestions);
const available = results.filter((r) => r.status === "AVAILABLE");
const taken = results.filter((r) => r.status === "TAKEN");
return {
content: [
{
type: "text",
text: JSON.stringify(
{
keyword,
totalSuggestions: suggestions.length,
availableCount: available.length,
takenCount: taken.length,
available: available.map((r) => r.domain),
taken: taken.map((r) => r.domain),
},
null,
2
),
},
],
};
}
);
// Tool: Get WHOIS information for a domain
server.registerTool(
"whois_lookup",
{
title: "WHOIS / RDAP Domain Lookup",
description:
"Retrieve WHOIS registration information for a domain name using RDAP (modern WHOIS). " +
"Returns registrar, creation date, expiration date, name servers, and domain status. " +
"Use this to find out who owns a domain, when it was registered, or when it expires. " +
"Example: whois_lookup({ domain: 'github.com' })",
inputSchema: {
domain: z
.string()
.describe(
"The registered domain name to look up WHOIS/RDAP information for (e.g. 'github.com')"
),
},
annotations: {
title: "WHOIS / RDAP Domain Lookup",
readOnlyHint: true,
openWorldHint: true,
},
},
async ({ domain }) => {
const info = await getWhoisInfo(domain);
if (!info) {
return {
content: [
{
type: "text",
text: JSON.stringify(
{
domain,
error:
"Could not retrieve WHOIS information. Domain may be available or WHOIS data is not accessible.",
},
null,
2
),
},
],
};
}
return {
content: [
{
type: "text",
text: JSON.stringify(info, null, 2),
},
],
};
}
);
// Tool: Find domain across multiple TLDs
server.registerTool(
"find_across_tlds",
{
title: "Find Domain Across TLDs",
description:
"Check availability of a domain name across multiple top-level domains (TLDs) simultaneously. " +
"Given a name like 'myapp', checks myapp.com, myapp.net, myapp.org, myapp.io, myapp.co, " +
"myapp.ai, myapp.dev, myapp.app, myapp.tech, myapp.xyz. " +
"Returns grouped results showing which TLDs are available and which are taken. " +
"Example: find_across_tlds({ name: 'myapp' })",
inputSchema: {
name: z
.string()
.describe(
"The domain name WITHOUT TLD to check across extensions (e.g. 'myapp' not 'myapp.com')"
),
tlds: z
.array(z.string())
.optional()
.describe(
"Optional list of TLDs to check (defaults to: com, net, org, io, co, ai, dev, app, tech, xyz)"
),
},
annotations: {
title: "Find Domain Across TLDs",
readOnlyHint: true,
openWorldHint: true,
},
},
async ({ name, tlds }) => {
const results = await findDomainsAcrossTLDs(name, tlds || POPULAR_TLDS);
const grouped = {
name,
tlds: tlds || POPULAR_TLDS,
available: results
.filter((r) => r.status === "AVAILABLE")
.map((r) => r.domain),
taken: results
.filter((r) => r.status === "TAKEN")
.map((r) => r.domain),
unknown: results
.filter((r) => r.status === "UNKNOWN")
.map((r) => r.domain),
};
return {
content: [
{
type: "text",
text: JSON.stringify(grouped, null, 2),
},
],
};
}
);
// Resource: Domain availability check result
server.registerResource(
"domain",
new ResourceTemplate("domain://{domain}", { list: undefined }),
{
title: "Domain Availability Resource",
description:
"Read domain availability status as a resource. Provide a domain name to get its registration status.",
mimeType: "application/json",
},
async (uri, { domain }) => {
const result = await checkDomain(domain as string);
return {
contents: [
{
uri: uri.href,
text: JSON.stringify(result, null, 2),
},
],
};
}
);
// Prompt: Domain search assistant
server.registerPrompt(
"find_domain",
{
title: "Find a Domain Name",
description:
"Interactive prompt to help find the perfect domain name for a keyword or brand. " +
"Checks .com availability, suggests alternatives, and scans popular TLDs.",
argsSchema: {
keyword: z
.string()
.describe("The keyword or brand name you want a domain for"),
preferences: z
.string()
.optional()
.describe(
"Any preferences like preferred TLDs (.io, .ai), naming style (short, brandable), budget, etc."
),
},
},
({ keyword, preferences }) => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `I'm looking for a domain name for "${keyword}".${
preferences ? ` My preferences: ${preferences}` : ""
}
Please help me:
1. Check if ${keyword}.com is available
2. Suggest alternative domain names if it's taken
3. Check availability across popular TLDs (.com, .io, .co, .ai, .dev)
4. Recommend the best available options`,
},
},
],
})
);
return server;
}
// Determine transport mode based on arguments
const args = process.argv.slice(2);
const useSSE = args.includes("--sse") || args.includes("-s");
const port = parseInt(process.env.PORT || args.find((a) => a.startsWith("--port="))?.split("=")[1] || "3000");
async function main() {
if (useSSE) {
// SSE mode - run HTTP server
const transports: Record<string, { transport: BunSSEServerTransport; server: McpServer }> = {};
Bun.serve({
port,
hostname: process.env.HOST || "0.0.0.0",
idleTimeout: 255,
routes: {
"/sse": {
GET: () => {
const server = createServer();
const transport = new BunSSEServerTransport("/messages");
server.connect(transport);
transport.onclose = () => {
delete transports[transport.sessionId];
};
transports[transport.sessionId] = { transport, server };
return transport.createResponse();
},
POST: () => new Response("Method Not Allowed", { status: 405 }),
},
"/messages": {
POST: (req) => {
const url = new URL(req.url);
const sessionId = url.searchParams.get("sessionId");
if (!sessionId || !transports[sessionId]) {
return new Response("Invalid session ID", { status: 400 });
}
return transports[sessionId].transport.handlePostMessage(req);
},
},
},
fetch(req) {
const url = new URL(req.url);
if (url.pathname === "/") {
return new Response(
JSON.stringify({
name: "DomainFinder MCP Server",
version: "1.0.0",
description:
"Domain name availability checking and intelligent suggestions",
endpoints: {
sse: "/sse",
messages: "/messages",
},
tools: [
"check_domain",
"check_domains_bulk",
"suggest_domains",
"whois_lookup",
"find_across_tlds",
],
}),
{
headers: { "Content-Type": "application/json" },
}
);
}
return new Response("Not Found", { status: 404 });
},
});
console.log(`DomainFinder MCP Server running on http://localhost:${port}`);
console.log("SSE endpoint: /sse");
console.log("Messages endpoint: /messages");
// Setup weekly cron job to update WHOIS servers
const baker = Baker.create();
baker.add({
name: "whois-update",
cron: "@weekly",
callback: async () => {
console.log("[CRON] Running weekly WHOIS servers update...");
try {
await updateWhoisServers();
} catch (error) {
console.error("[CRON] Failed to update WHOIS servers:", error);
}
},
});
baker.bakeAll();
console.log("WHOIS update cron: Every week (Sunday at midnight)");
// Run update on startup if file is older than 1 week
const whoisFilePath = join(import.meta.dir, "../whois-servers.json");
try {
const stat = await Bun.file(whoisFilePath).stat();
const fileAge = Date.now() - stat.mtime.getTime();
const oneWeek = 7 * 24 * 60 * 60 * 1000;
if (fileAge > oneWeek) {
console.log("[STARTUP] WHOIS servers file is older than 1 week, updating...");
await updateWhoisServers();
}
} catch {
console.log("[STARTUP] WHOIS servers file not found, fetching...");
await updateWhoisServers();
}
} else {
// stdio mode - default for MCP
const server = createServer();
const transport = new StdioServerTransport();
await server.connect(transport);
}
}
if (import.meta.main) {
main();
}