
- Production-ready MCP server for Name Cheap API integration - Domain management (registration, renewal, availability checking) - DNS management (records, nameserver configuration) - SSL certificate management and monitoring - Account information and balance checking - Smart identifier resolution for improved UX - Comprehensive error handling with specific exception types - 80%+ test coverage with unit, integration, and MCP tests - CLI and MCP server interfaces - FastMCP 2.10.5+ implementation with full MCP spec compliance 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
311 lines
10 KiB
Python
311 lines
10 KiB
Python
"""Domain management tools for FastMCP."""
|
|
|
|
from typing import Any, Dict, List
|
|
from pydantic import BaseModel, Field
|
|
|
|
from fastmcp import FastMCP
|
|
from .client import NameCheapClient
|
|
from .server import NameCheapAPIError
|
|
|
|
|
|
# Create domain tools instance
|
|
domains_mcp = FastMCP("domains")
|
|
|
|
# Global client instance
|
|
_client: NameCheapClient = None
|
|
|
|
|
|
def get_client() -> NameCheapClient:
|
|
"""Get or create the Name Cheap client."""
|
|
global _client
|
|
if _client is None:
|
|
_client = NameCheapClient()
|
|
return _client
|
|
|
|
|
|
# Pydantic models for validation
|
|
class ContactInfo(BaseModel):
|
|
"""Contact information for domain registration."""
|
|
first_name: str = Field(..., description="First name")
|
|
last_name: str = Field(..., description="Last name")
|
|
address1: str = Field(..., description="Street address")
|
|
city: str = Field(..., description="City")
|
|
state_province: str = Field(..., description="State or province")
|
|
postal_code: str = Field(..., description="Postal/ZIP code")
|
|
country: str = Field(..., description="Country code (e.g., US, CA)")
|
|
phone: str = Field(..., description="Phone number with country code")
|
|
email_address: str = Field(..., description="Email address")
|
|
address2: str = Field("", description="Apartment/suite number")
|
|
organization_name: str = Field("", description="Organization name")
|
|
|
|
|
|
class DomainCheckRequest(BaseModel):
|
|
"""Request model for domain availability check."""
|
|
domains: List[str] = Field(..., min_items=1, description="List of domain names to check")
|
|
|
|
|
|
class DomainRegisterRequest(BaseModel):
|
|
"""Request model for domain registration."""
|
|
domain_name: str = Field(..., description="Domain name to register")
|
|
years: int = Field(1, ge=1, le=10, description="Registration period in years")
|
|
contacts: ContactInfo = Field(..., description="Contact information")
|
|
|
|
|
|
class DomainRenewRequest(BaseModel):
|
|
"""Request model for domain renewal."""
|
|
domain_identifier: str = Field(..., description="Domain name or identifier")
|
|
years: int = Field(1, ge=1, le=10, description="Renewal period in years")
|
|
|
|
|
|
# Domain management tools
|
|
@domains_mcp.tool()
|
|
async def list_domains() -> Dict[str, Any]:
|
|
"""List all domains in the account with enhanced information.
|
|
|
|
Returns:
|
|
Success response with domain list or error details
|
|
"""
|
|
try:
|
|
client = get_client()
|
|
domains = await client.list_domains()
|
|
|
|
return {
|
|
"success": True,
|
|
"data": domains,
|
|
"message": f"Found {len(domains)} domains in account"
|
|
}
|
|
except NameCheapAPIError as e:
|
|
return {
|
|
"success": False,
|
|
"error": str(e),
|
|
"message": "Failed to list domains"
|
|
}
|
|
except Exception as e:
|
|
return {
|
|
"success": False,
|
|
"error": f"Unexpected error: {str(e)}",
|
|
"message": "Failed to list domains"
|
|
}
|
|
|
|
|
|
@domains_mcp.tool()
|
|
async def get_domain_info(domain_identifier: str) -> Dict[str, Any]:
|
|
"""Get detailed information about a domain.
|
|
|
|
Args:
|
|
domain_identifier: Domain name (supports exact domain name matching)
|
|
|
|
Returns:
|
|
Success response with domain details or error details
|
|
"""
|
|
try:
|
|
client = get_client()
|
|
domain_info = await client.get_domain_info(domain_identifier)
|
|
|
|
return {
|
|
"success": True,
|
|
"data": domain_info,
|
|
"message": f"Retrieved information for domain {domain_identifier}"
|
|
}
|
|
except NameCheapAPIError as e:
|
|
return {
|
|
"success": False,
|
|
"error": str(e),
|
|
"message": f"Failed to get domain info for {domain_identifier}"
|
|
}
|
|
except Exception as e:
|
|
return {
|
|
"success": False,
|
|
"error": f"Unexpected error: {str(e)}",
|
|
"message": f"Failed to get domain info for {domain_identifier}"
|
|
}
|
|
|
|
|
|
@domains_mcp.tool()
|
|
async def check_domain_availability(request: DomainCheckRequest) -> Dict[str, Any]:
|
|
"""Check if domains are available for registration.
|
|
|
|
Args:
|
|
request: Domain check request with list of domains
|
|
|
|
Returns:
|
|
Success response with availability results or error details
|
|
"""
|
|
try:
|
|
client = get_client()
|
|
results = await client.check_domain_availability(request.domains)
|
|
|
|
return {
|
|
"success": True,
|
|
"data": results,
|
|
"message": f"Checked availability for {len(request.domains)} domains"
|
|
}
|
|
except NameCheapAPIError as e:
|
|
return {
|
|
"success": False,
|
|
"error": str(e),
|
|
"message": "Failed to check domain availability"
|
|
}
|
|
except Exception as e:
|
|
return {
|
|
"success": False,
|
|
"error": f"Unexpected error: {str(e)}",
|
|
"message": "Failed to check domain availability"
|
|
}
|
|
|
|
|
|
@domains_mcp.tool()
|
|
async def register_domain(request: DomainRegisterRequest) -> Dict[str, Any]:
|
|
"""Register a new domain.
|
|
|
|
Args:
|
|
request: Domain registration request with domain, years, and contact info
|
|
|
|
Returns:
|
|
Success response with registration details or error details
|
|
"""
|
|
try:
|
|
client = get_client()
|
|
|
|
# Convert ContactInfo to API format
|
|
contact_data = {
|
|
"FirstName": request.contacts.first_name,
|
|
"LastName": request.contacts.last_name,
|
|
"Address1": request.contacts.address1,
|
|
"City": request.contacts.city,
|
|
"StateProvince": request.contacts.state_province,
|
|
"PostalCode": request.contacts.postal_code,
|
|
"Country": request.contacts.country,
|
|
"Phone": request.contacts.phone,
|
|
"EmailAddress": request.contacts.email_address,
|
|
}
|
|
|
|
if request.contacts.address2:
|
|
contact_data["Address2"] = request.contacts.address2
|
|
if request.contacts.organization_name:
|
|
contact_data["OrganizationName"] = request.contacts.organization_name
|
|
|
|
result = await client.register_domain(
|
|
request.domain_name,
|
|
request.years,
|
|
contact_data
|
|
)
|
|
|
|
return {
|
|
"success": True,
|
|
"data": result,
|
|
"message": f"Successfully registered domain {request.domain_name} for {request.years} years"
|
|
}
|
|
except NameCheapAPIError as e:
|
|
return {
|
|
"success": False,
|
|
"error": str(e),
|
|
"message": f"Failed to register domain {request.domain_name}"
|
|
}
|
|
except Exception as e:
|
|
return {
|
|
"success": False,
|
|
"error": f"Unexpected error: {str(e)}",
|
|
"message": f"Failed to register domain {request.domain_name}"
|
|
}
|
|
|
|
|
|
@domains_mcp.tool()
|
|
async def renew_domain(request: DomainRenewRequest) -> Dict[str, Any]:
|
|
"""Renew an existing domain.
|
|
|
|
Args:
|
|
request: Domain renewal request with identifier and years
|
|
|
|
Returns:
|
|
Success response with renewal details or error details
|
|
"""
|
|
try:
|
|
client = get_client()
|
|
result = await client.renew_domain(request.domain_identifier, request.years)
|
|
|
|
return {
|
|
"success": True,
|
|
"data": result,
|
|
"message": f"Successfully renewed domain {request.domain_identifier} for {request.years} years"
|
|
}
|
|
except NameCheapAPIError as e:
|
|
return {
|
|
"success": False,
|
|
"error": str(e),
|
|
"message": f"Failed to renew domain {request.domain_identifier}"
|
|
}
|
|
except Exception as e:
|
|
return {
|
|
"success": False,
|
|
"error": f"Unexpected error: {str(e)}",
|
|
"message": f"Failed to renew domain {request.domain_identifier}"
|
|
}
|
|
|
|
|
|
# Resource for domain listings
|
|
@domains_mcp.resource("namecheap://domains")
|
|
async def list_domains_resource() -> str:
|
|
"""List all domains as a resource."""
|
|
try:
|
|
client = get_client()
|
|
domains = await client.list_domains()
|
|
|
|
# Format as readable text
|
|
if not domains:
|
|
return "No domains found in account."
|
|
|
|
output = ["Domains in Account:", "=" * 20, ""]
|
|
for domain in domains:
|
|
name = domain.get("@Name", "Unknown")
|
|
expires = domain.get("@Expires", "Unknown")
|
|
is_expired = domain.get("_is_expired", False)
|
|
is_locked = domain.get("_is_locked", False)
|
|
|
|
status_flags = []
|
|
if is_expired:
|
|
status_flags.append("EXPIRED")
|
|
if is_locked:
|
|
status_flags.append("LOCKED")
|
|
|
|
status = f" [{', '.join(status_flags)}]" if status_flags else ""
|
|
output.append(f"• {name} (expires: {expires}){status}")
|
|
|
|
return "\n".join(output)
|
|
except Exception as e:
|
|
return f"Error listing domains: {str(e)}"
|
|
|
|
|
|
@domains_mcp.resource("namecheap://domain/{domain_name}")
|
|
async def get_domain_resource(domain_name: str) -> str:
|
|
"""Get detailed domain information as a resource."""
|
|
try:
|
|
client = get_client()
|
|
domain_info = await client.get_domain_info(domain_name)
|
|
|
|
# Format as readable text
|
|
output = [f"Domain Information: {domain_name}", "=" * 40, ""]
|
|
|
|
# Extract key information
|
|
if "DomainGetInfoResult" in domain_info:
|
|
info = domain_info["DomainGetInfoResult"]
|
|
output.append(f"Status: {info.get('@Status', 'Unknown')}")
|
|
output.append(f"ID: {info.get('@ID', 'Unknown')}")
|
|
output.append(f"Owner Name: {info.get('@OwnerName', 'Unknown')}")
|
|
output.append(f"Created: {info.get('@DomainDetails', {}).get('@CreatedDate', 'Unknown')}")
|
|
output.append(f"Expires: {info.get('@DomainDetails', {}).get('@ExpiredDate', 'Unknown')}")
|
|
output.append("")
|
|
|
|
# Nameservers
|
|
nameservers = info.get("DnsDetails", {}).get("Nameserver", [])
|
|
if nameservers:
|
|
output.append("Nameservers:")
|
|
if isinstance(nameservers, list):
|
|
for ns in nameservers:
|
|
output.append(f" • {ns}")
|
|
else:
|
|
output.append(f" • {nameservers}")
|
|
|
|
return "\n".join(output)
|
|
except Exception as e:
|
|
return f"Error getting domain info: {str(e)}" |