#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { check } from 'domainstat';
const server = new Server(
{
name: 'mcp-domain-availability',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
// Constants
const MAX_BULK_DOMAINS = 50;
const DOMAIN_CHECK_TIMEOUT = 30000; // 30 seconds
const MAX_DOMAIN_LENGTH = 253; // RFC 1035
// Domain validation regex
const DOMAIN_REGEX = /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/i;
/**
* Creates a timeout promise that rejects after the specified milliseconds
*/
function createTimeout(ms: number): Promise<never> {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error(`Operation timed out after ${ms}ms`)), ms);
});
}
/**
* Validates a domain name format and length
*/
function validateDomain(domain: string): { valid: boolean; error?: string } {
if (!domain || typeof domain !== 'string') {
return { valid: false, error: 'Domain must be a non-empty string' };
}
if (domain.length > MAX_DOMAIN_LENGTH) {
return {
valid: false,
error: `Domain exceeds maximum length of ${MAX_DOMAIN_LENGTH} characters`,
};
}
if (!DOMAIN_REGEX.test(domain)) {
return {
valid: false,
error: 'Invalid domain format. Domain must be a valid hostname (e.g., example.com)',
};
}
return { valid: true };
}
/**
* Checks domain availability with timeout
*/
async function checkDomainWithTimeout(domain: string) {
return Promise.race([
check(domain),
createTimeout(DOMAIN_CHECK_TIMEOUT),
]);
}
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'check_domain_availability',
description:
'Check if a domain name is available for registration. Uses DNS, RDAP, and WHOIS lookups. Returns availability status and details.',
inputSchema: {
type: 'object',
properties: {
domain: {
type: 'string',
description: 'The domain name to check (e.g., "example.com")',
},
},
required: ['domain'],
},
},
{
name: 'check_multiple_domains',
description:
'Check availability for multiple domains at once (up to 50 domains). Returns results for all domains with summary statistics.',
inputSchema: {
type: 'object',
properties: {
domains: {
type: 'array',
items: {
type: 'string',
},
description:
'Array of domain names to check (e.g., ["example.com", "test.io"]). Maximum 50 domains.',
},
},
required: ['domains'],
},
},
],
};
});
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
if (name === 'check_domain_availability') {
const { domain } = args as { domain: string };
// Validate input
const validation = validateDomain(domain);
if (!validation.valid) {
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
error: validation.error,
domain,
},
null,
2
),
},
],
isError: true,
};
}
try {
const result = await checkDomainWithTimeout(domain);
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
domain,
available: result.availability === 'unregistered',
status: result.availability,
registered: result.availability === 'registered',
details: {
availability: result.availability,
...(result.registrar && { registrar: result.registrar }),
...(result.registrationDate && {
registrationDate: result.registrationDate,
}),
...(result.expirationDate && {
expirationDate: result.expirationDate,
}),
},
},
null,
2
),
},
],
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : 'Unknown error';
// Detect timeout
if (errorMessage.includes('timed out')) {
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
error: `Domain check timed out after ${DOMAIN_CHECK_TIMEOUT}ms. The domain check may be taking longer than expected.`,
domain,
suggestion: 'Please try again or check your network connection.',
},
null,
2
),
},
],
isError: true,
};
}
// Detect rate limiting (common error patterns)
if (
errorMessage.toLowerCase().includes('rate limit') ||
errorMessage.toLowerCase().includes('too many requests')
) {
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
error: 'Rate limit exceeded. Too many domain checks in a short period.',
domain,
suggestion:
'Please wait a few moments before checking again.',
},
null,
2
),
},
],
isError: true,
};
}
// Network errors
if (
errorMessage.toLowerCase().includes('network') ||
errorMessage.toLowerCase().includes('fetch') ||
errorMessage.toLowerCase().includes('connection')
) {
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
error: 'Network error occurred while checking domain.',
domain,
suggestion: 'Please check your internet connection and try again.',
},
null,
2
),
},
],
isError: true,
};
}
// Generic error
console.error('Domain check error:', error);
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
error: 'Failed to check domain availability.',
domain,
details: errorMessage,
},
null,
2
),
},
],
isError: true,
};
}
}
if (name === 'check_multiple_domains') {
const { domains } = args as { domains: string[] };
// Validate input
if (!Array.isArray(domains) || domains.length === 0) {
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
error: 'Domains parameter is required and must be a non-empty array',
},
null,
2
),
},
],
isError: true,
};
}
// Check bulk limit
if (domains.length > MAX_BULK_DOMAINS) {
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
error: `Too many domains. Maximum ${MAX_BULK_DOMAINS} domains allowed per request.`,
provided: domains.length,
max: MAX_BULK_DOMAINS,
},
null,
2
),
},
],
isError: true,
};
}
// Validate all domains
const validationResults = domains.map((domain) => ({
domain,
validation: validateDomain(domain),
}));
const invalidDomains = validationResults.filter(
(r) => !r.validation.valid
);
if (invalidDomains.length > 0) {
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
error: 'Invalid domain format(s)',
invalidDomains: invalidDomains.map((d) => ({
domain: d.domain,
error: d.validation.error,
})),
},
null,
2
),
},
],
isError: true,
};
}
// Check all domains concurrently with individual error handling
const results = await Promise.all(
domains.map(async (domain) => {
try {
const result = await checkDomainWithTimeout(domain);
return {
domain,
available: result.availability === 'unregistered',
status: result.availability,
registered: result.availability === 'registered',
error: null,
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : 'Unknown error';
// Categorize errors
let errorType = 'unknown';
if (errorMessage.includes('timed out')) {
errorType = 'timeout';
} else if (
errorMessage.toLowerCase().includes('rate limit') ||
errorMessage.toLowerCase().includes('too many requests')
) {
errorType = 'rate_limit';
} else if (
errorMessage.toLowerCase().includes('network') ||
errorMessage.toLowerCase().includes('fetch') ||
errorMessage.toLowerCase().includes('connection')
) {
errorType = 'network';
}
return {
domain,
available: false,
status: 'error',
registered: false,
error: errorMessage,
errorType,
};
}
})
);
// Calculate summary
const summary = {
total: results.length,
available: results.filter((r) => r.available).length,
registered: results.filter((r) => r.registered).length,
errors: results.filter((r) => r.error).length,
unknown: results.filter((r) => r.status === 'unknown').length,
};
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
results,
summary,
},
null,
2
),
},
],
};
}
throw new Error(`Unknown tool: ${name}`);
} catch (error) {
console.error('Tool execution error:', error);
return {
content: [
{
type: 'text',
text: JSON.stringify({
error:
error instanceof Error ? error.message : 'Unknown error occurred',
}),
},
],
isError: true,
};
}
});
// Start the server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('MCP Domain Availability server running on stdio');
}
main().catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});