Claude Code f5e63c888d Initial commit: MCP Name Cheap server implementation
- 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>
2025-07-20 03:43:11 -06:00

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)}"