Migrate from fastmcp to official MCP package

This commit is contained in:
Ryan Malloy 2025-06-11 17:53:43 -06:00
parent 5c87097158
commit b8fd6e4632

View File

@ -10,7 +10,9 @@ import re
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
import httpx import httpx
from fastmcp import FastMCP from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Resource, Tool, TextContent
from pydantic import BaseModel from pydantic import BaseModel
@ -143,15 +145,15 @@ class VultrDNSServer:
return await self._make_request("DELETE", f"/domains/{domain}/records/{record_id}") return await self._make_request("DELETE", f"/domains/{domain}/records/{record_id}")
def create_mcp_server(api_key: Optional[str] = None) -> FastMCP: def create_mcp_server(api_key: Optional[str] = None) -> Server:
""" """
Create and configure a FastMCP server for Vultr DNS management. Create and configure an MCP server for Vultr DNS management.
Args: Args:
api_key: Vultr API key. If not provided, will read from VULTR_API_KEY env var. api_key: Vultr API key. If not provided, will read from VULTR_API_KEY env var.
Returns: Returns:
Configured FastMCP server instance Configured MCP server instance
Raises: Raises:
ValueError: If API key is not provided and not found in environment ValueError: If API key is not provided and not found in environment
@ -164,43 +166,43 @@ def create_mcp_server(api_key: Optional[str] = None) -> FastMCP:
"VULTR_API_KEY must be provided either as parameter or environment variable" "VULTR_API_KEY must be provided either as parameter or environment variable"
) )
# Initialize FastMCP server # Initialize MCP server
mcp = FastMCP("Vultr DNS Manager") server = Server("vultr-dns-mcp")
# Initialize Vultr client # Initialize Vultr client
vultr_client = VultrDNSServer(api_key) vultr_client = VultrDNSServer(api_key)
# Add resources for client discovery # Add resources for client discovery
@mcp.resource("vultr://domains") @server.list_resources()
async def get_domains_resource(): async def list_resources() -> List[Resource]:
"""Resource listing all available domains.""" """List available resources."""
try: return [
domains = await vultr_client.list_domains() Resource(
return { uri="vultr://domains",
"uri": "vultr://domains", name="DNS Domains",
"name": "DNS Domains", description="All DNS domains in your Vultr account",
"description": "All DNS domains in your Vultr account", mimeType="application/json"
"mimeType": "application/json", ),
"content": domains Resource(
} uri="vultr://capabilities",
except Exception as e: name="Server Capabilities",
return { description="Vultr DNS server capabilities and supported features",
"uri": "vultr://domains", mimeType="application/json"
"name": "DNS Domains", )
"description": f"Error loading domains: {str(e)}", ]
"mimeType": "application/json",
"content": {"error": str(e)} @server.read_resource()
} async def read_resource(uri: str) -> str:
"""Read a specific resource."""
@mcp.resource("vultr://capabilities") if uri == "vultr://domains":
async def get_capabilities_resource(): try:
"""Resource describing server capabilities and supported record types.""" domains = await vultr_client.list_domains()
return { return str(domains)
"uri": "vultr://capabilities", except Exception as e:
"name": "Server Capabilities", return f"Error loading domains: {str(e)}"
"description": "Vultr DNS server capabilities and supported features",
"mimeType": "application/json", elif uri == "vultr://capabilities":
"content": { capabilities = {
"supported_record_types": [ "supported_record_types": [
{ {
"type": "A", "type": "A",
@ -253,356 +255,470 @@ def create_mcp_server(api_key: Optional[str] = None) -> FastMCP:
"min_ttl": 60, "min_ttl": 60,
"max_ttl": 86400 "max_ttl": 86400
} }
} return str(capabilities)
@mcp.resource("vultr://records/{domain}") elif uri.startswith("vultr://records/"):
async def get_domain_records_resource(domain: str): domain = uri.replace("vultr://records/", "")
"""Resource listing all records for a specific domain.""" try:
try: records = await vultr_client.list_records(domain)
records = await vultr_client.list_records(domain) return str({
return {
"uri": f"vultr://records/{domain}",
"name": f"DNS Records for {domain}",
"description": f"All DNS records configured for domain {domain}",
"mimeType": "application/json",
"content": {
"domain": domain, "domain": domain,
"records": records, "records": records,
"record_count": len(records) "record_count": len(records)
} })
} except Exception as e:
except Exception as e: return f"Error loading records for {domain}: {str(e)}"
return {
"uri": f"vultr://records/{domain}", return "Resource not found"
"name": f"DNS Records for {domain}",
"description": f"Error loading records for {domain}: {str(e)}",
"mimeType": "application/json",
"content": {"error": str(e), "domain": domain}
}
# Define MCP tools # Define MCP tools
@mcp.tool() @server.list_tools()
async def list_dns_domains() -> List[Dict[str, Any]]: async def list_tools() -> List[Tool]:
""" """List available tools."""
List all DNS domains in your Vultr account. return [
Tool(
This tool retrieves all domains currently managed through Vultr DNS. name="list_dns_domains",
Each domain object includes domain name, creation date, and status information. description="List all DNS domains in your Vultr account",
""" inputSchema={
"type": "object",
"properties": {},
"required": []
}
),
Tool(
name="get_dns_domain",
description="Get detailed information for a specific DNS domain",
inputSchema={
"type": "object",
"properties": {
"domain": {
"type": "string",
"description": "The domain name to retrieve (e.g., 'example.com')"
}
},
"required": ["domain"]
}
),
Tool(
name="create_dns_domain",
description="Create a new DNS domain with a default A record",
inputSchema={
"type": "object",
"properties": {
"domain": {
"type": "string",
"description": "The domain name to create (e.g., 'newdomain.com')"
},
"ip": {
"type": "string",
"description": "IPv4 address for the default A record (e.g., '192.168.1.100')"
}
},
"required": ["domain", "ip"]
}
),
Tool(
name="delete_dns_domain",
description="Delete a DNS domain and ALL its associated records",
inputSchema={
"type": "object",
"properties": {
"domain": {
"type": "string",
"description": "The domain name to delete (e.g., 'example.com')"
}
},
"required": ["domain"]
}
),
Tool(
name="list_dns_records",
description="List all DNS records for a specific domain",
inputSchema={
"type": "object",
"properties": {
"domain": {
"type": "string",
"description": "The domain name (e.g., 'example.com')"
}
},
"required": ["domain"]
}
),
Tool(
name="get_dns_record",
description="Get detailed information for a specific DNS record",
inputSchema={
"type": "object",
"properties": {
"domain": {
"type": "string",
"description": "The domain name (e.g., 'example.com')"
},
"record_id": {
"type": "string",
"description": "The unique record identifier"
}
},
"required": ["domain", "record_id"]
}
),
Tool(
name="create_dns_record",
description="Create a new DNS record for a domain",
inputSchema={
"type": "object",
"properties": {
"domain": {
"type": "string",
"description": "The domain name (e.g., 'example.com')"
},
"record_type": {
"type": "string",
"description": "Record type (A, AAAA, CNAME, MX, TXT, NS, SRV)"
},
"name": {
"type": "string",
"description": "Record name/subdomain"
},
"data": {
"type": "string",
"description": "Record value"
},
"ttl": {
"type": "integer",
"description": "Time to live in seconds (60-86400, default: 300)"
},
"priority": {
"type": "integer",
"description": "Priority for MX/SRV records (0-65535)"
}
},
"required": ["domain", "record_type", "name", "data"]
}
),
Tool(
name="update_dns_record",
description="Update an existing DNS record with new configuration",
inputSchema={
"type": "object",
"properties": {
"domain": {
"type": "string",
"description": "The domain name (e.g., 'example.com')"
},
"record_id": {
"type": "string",
"description": "The unique identifier of the record to update"
},
"record_type": {
"type": "string",
"description": "New record type (A, AAAA, CNAME, MX, TXT, NS, SRV)"
},
"name": {
"type": "string",
"description": "New record name/subdomain"
},
"data": {
"type": "string",
"description": "New record value"
},
"ttl": {
"type": "integer",
"description": "New TTL in seconds (60-86400, optional)"
},
"priority": {
"type": "integer",
"description": "New priority for MX/SRV records (optional)"
}
},
"required": ["domain", "record_id", "record_type", "name", "data"]
}
),
Tool(
name="delete_dns_record",
description="Delete a specific DNS record",
inputSchema={
"type": "object",
"properties": {
"domain": {
"type": "string",
"description": "The domain name (e.g., 'example.com')"
},
"record_id": {
"type": "string",
"description": "The unique identifier of the record to delete"
}
},
"required": ["domain", "record_id"]
}
),
Tool(
name="validate_dns_record",
description="Validate DNS record parameters before creation",
inputSchema={
"type": "object",
"properties": {
"record_type": {
"type": "string",
"description": "The record type (A, AAAA, CNAME, MX, TXT, NS, SRV)"
},
"name": {
"type": "string",
"description": "The record name/subdomain"
},
"data": {
"type": "string",
"description": "The record data/value"
},
"ttl": {
"type": "integer",
"description": "Time to live in seconds (optional)"
},
"priority": {
"type": "integer",
"description": "Priority for MX/SRV records (optional)"
}
},
"required": ["record_type", "name", "data"]
}
),
Tool(
name="analyze_dns_records",
description="Analyze DNS configuration for a domain and provide insights",
inputSchema={
"type": "object",
"properties": {
"domain": {
"type": "string",
"description": "The domain name to analyze (e.g., 'example.com')"
}
},
"required": ["domain"]
}
)
]
@server.call_tool()
async def call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]:
"""Handle tool calls."""
try: try:
domains = await vultr_client.list_domains() if name == "list_dns_domains":
return domains domains = await vultr_client.list_domains()
except Exception as e: return [TextContent(type="text", text=str(domains))]
return [{"error": str(e)}]
@mcp.tool()
async def get_dns_domain(domain: str) -> Dict[str, Any]:
"""
Get detailed information for a specific DNS domain.
Args:
domain: The domain name to retrieve (e.g., "example.com")
"""
try:
return await vultr_client.get_domain(domain)
except Exception as e:
return {"error": str(e)}
@mcp.tool()
async def create_dns_domain(domain: str, ip: str) -> Dict[str, Any]:
"""
Create a new DNS domain with a default A record.
Args:
domain: The domain name to create (e.g., "newdomain.com")
ip: IPv4 address for the default A record (e.g., "192.168.1.100")
"""
try:
return await vultr_client.create_domain(domain, ip)
except Exception as e:
return {"error": str(e)}
@mcp.tool()
async def delete_dns_domain(domain: str) -> Dict[str, Any]:
"""
Delete a DNS domain and ALL its associated records.
WARNING: This permanently deletes the domain and all DNS records.
Args:
domain: The domain name to delete (e.g., "example.com")
"""
try:
await vultr_client.delete_domain(domain)
return {"success": f"Domain {domain} deleted successfully"}
except Exception as e:
return {"error": str(e)}
@mcp.tool()
async def list_dns_records(domain: str) -> List[Dict[str, Any]]:
"""
List all DNS records for a specific domain.
Args:
domain: The domain name (e.g., "example.com")
"""
try:
records = await vultr_client.list_records(domain)
return records
except Exception as e:
return [{"error": str(e)}]
@mcp.tool()
async def get_dns_record(domain: str, record_id: str) -> Dict[str, Any]:
"""
Get detailed information for a specific DNS record.
Args:
domain: The domain name (e.g., "example.com")
record_id: The unique record identifier
"""
try:
return await vultr_client.get_record(domain, record_id)
except Exception as e:
return {"error": str(e)}
@mcp.tool()
async def create_dns_record(
domain: str,
record_type: str,
name: str,
data: str,
ttl: Optional[int] = None,
priority: Optional[int] = None
) -> Dict[str, Any]:
"""
Create a new DNS record for a domain.
Args:
domain: The domain name (e.g., "example.com")
record_type: Record type (A, AAAA, CNAME, MX, TXT, NS, SRV)
name: Record name/subdomain
data: Record value
ttl: Time to live in seconds (60-86400, default: 300)
priority: Priority for MX/SRV records (0-65535)
"""
try:
return await vultr_client.create_record(domain, record_type, name, data, ttl, priority)
except Exception as e:
return {"error": str(e)}
@mcp.tool()
async def update_dns_record(
domain: str,
record_id: str,
record_type: str,
name: str,
data: str,
ttl: Optional[int] = None,
priority: Optional[int] = None
) -> Dict[str, Any]:
"""
Update an existing DNS record with new configuration.
Args:
domain: The domain name (e.g., "example.com")
record_id: The unique identifier of the record to update
record_type: New record type (A, AAAA, CNAME, MX, TXT, NS, SRV)
name: New record name/subdomain
data: New record value
ttl: New TTL in seconds (60-86400, optional)
priority: New priority for MX/SRV records (optional)
"""
try:
return await vultr_client.update_record(domain, record_id, record_type, name, data, ttl, priority)
except Exception as e:
return {"error": str(e)}
@mcp.tool()
async def delete_dns_record(domain: str, record_id: str) -> Dict[str, Any]:
"""
Delete a specific DNS record.
Args:
domain: The domain name (e.g., "example.com")
record_id: The unique identifier of the record to delete
"""
try:
await vultr_client.delete_record(domain, record_id)
return {"success": f"DNS record {record_id} deleted successfully"}
except Exception as e:
return {"error": str(e)}
@mcp.tool()
async def validate_dns_record(
record_type: str,
name: str,
data: str,
ttl: Optional[int] = None,
priority: Optional[int] = None
) -> Dict[str, Any]:
"""
Validate DNS record parameters before creation.
Args:
record_type: The record type (A, AAAA, CNAME, MX, TXT, NS, SRV)
name: The record name/subdomain
data: The record data/value
ttl: Time to live in seconds (optional)
priority: Priority for MX/SRV records (optional)
"""
validation_result = {
"valid": True,
"errors": [],
"warnings": [],
"suggestions": []
}
# Validate record type
valid_types = ['A', 'AAAA', 'CNAME', 'MX', 'TXT', 'NS', 'SRV']
if record_type.upper() not in valid_types:
validation_result["valid"] = False
validation_result["errors"].append(f"Invalid record type. Must be one of: {', '.join(valid_types)}")
record_type = record_type.upper()
# Validate TTL
if ttl is not None:
if ttl < 60 or ttl > 86400:
validation_result["warnings"].append("TTL should be between 60 and 86400 seconds")
elif ttl < 300:
validation_result["warnings"].append("Low TTL values may impact DNS performance")
# Record-specific validation
if record_type == 'A':
ipv4_pattern = r'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'
if not re.match(ipv4_pattern, data):
validation_result["valid"] = False
validation_result["errors"].append("Invalid IPv4 address format")
elif record_type == 'AAAA':
if '::' in data and data.count('::') > 1:
validation_result["valid"] = False
validation_result["errors"].append("Invalid IPv6 address: multiple :: sequences")
elif record_type == 'CNAME':
if name == '@' or name == '':
validation_result["valid"] = False
validation_result["errors"].append("CNAME records cannot be used for root domain (@)")
elif record_type == 'MX':
if priority is None:
validation_result["valid"] = False
validation_result["errors"].append("MX records require a priority value")
elif priority < 0 or priority > 65535:
validation_result["valid"] = False
validation_result["errors"].append("MX priority must be between 0 and 65535")
elif record_type == 'SRV':
if priority is None:
validation_result["valid"] = False
validation_result["errors"].append("SRV records require a priority value")
srv_parts = data.split()
if len(srv_parts) != 3:
validation_result["valid"] = False
validation_result["errors"].append("SRV data must be in format: 'weight port target'")
return {
"record_type": record_type,
"name": name,
"data": data,
"ttl": ttl,
"priority": priority,
"validation": validation_result
}
@mcp.tool()
async def analyze_dns_records(domain: str) -> Dict[str, Any]:
"""
Analyze DNS configuration for a domain and provide insights.
Args:
domain: The domain name to analyze (e.g., "example.com")
"""
try:
records = await vultr_client.list_records(domain)
# Analyze records elif name == "get_dns_domain":
record_types = {} domain = arguments["domain"]
total_records = len(records) result = await vultr_client.get_domain(domain)
ttl_values = [] return [TextContent(type="text", text=str(result))]
has_root_a = False
has_www = False
has_mx = False
has_spf = False
for record in records: elif name == "create_dns_domain":
record_type = record.get('type', 'UNKNOWN') domain = arguments["domain"]
record_name = record.get('name', '') ip = arguments["ip"]
record_data = record.get('data', '') result = await vultr_client.create_domain(domain, ip)
ttl = record.get('ttl', 300) return [TextContent(type="text", text=str(result))]
elif name == "delete_dns_domain":
domain = arguments["domain"]
await vultr_client.delete_domain(domain)
return [TextContent(type="text", text=f"Domain {domain} deleted successfully")]
elif name == "list_dns_records":
domain = arguments["domain"]
records = await vultr_client.list_records(domain)
return [TextContent(type="text", text=str(records))]
elif name == "get_dns_record":
domain = arguments["domain"]
record_id = arguments["record_id"]
result = await vultr_client.get_record(domain, record_id)
return [TextContent(type="text", text=str(result))]
elif name == "create_dns_record":
domain = arguments["domain"]
record_type = arguments["record_type"]
name = arguments["name"]
data = arguments["data"]
ttl = arguments.get("ttl")
priority = arguments.get("priority")
result = await vultr_client.create_record(domain, record_type, name, data, ttl, priority)
return [TextContent(type="text", text=str(result))]
elif name == "update_dns_record":
domain = arguments["domain"]
record_id = arguments["record_id"]
record_type = arguments["record_type"]
name = arguments["name"]
data = arguments["data"]
ttl = arguments.get("ttl")
priority = arguments.get("priority")
result = await vultr_client.update_record(domain, record_id, record_type, name, data, ttl, priority)
return [TextContent(type="text", text=str(result))]
elif name == "delete_dns_record":
domain = arguments["domain"]
record_id = arguments["record_id"]
await vultr_client.delete_record(domain, record_id)
return [TextContent(type="text", text=f"DNS record {record_id} deleted successfully")]
elif name == "validate_dns_record":
record_type = arguments["record_type"]
name = arguments["name"]
data = arguments["data"]
ttl = arguments.get("ttl")
priority = arguments.get("priority")
record_types[record_type] = record_types.get(record_type, 0) + 1 validation_result = {
ttl_values.append(ttl) "valid": True,
"errors": [],
"warnings": [],
"suggestions": []
}
if record_type == 'A' and record_name in ['@', domain]: # Validate record type
has_root_a = True valid_types = ['A', 'AAAA', 'CNAME', 'MX', 'TXT', 'NS', 'SRV']
if record_name == 'www': if record_type.upper() not in valid_types:
has_www = True validation_result["valid"] = False
if record_type == 'MX': validation_result["errors"].append(f"Invalid record type. Must be one of: {', '.join(valid_types)}")
has_mx = True
if record_type == 'TXT' and 'spf1' in record_data.lower(): record_type = record_type.upper()
has_spf = True
# Validate TTL
if ttl is not None:
if ttl < 60 or ttl > 86400:
validation_result["warnings"].append("TTL should be between 60 and 86400 seconds")
elif ttl < 300:
validation_result["warnings"].append("Low TTL values may impact DNS performance")
# Record-specific validation
if record_type == 'A':
ipv4_pattern = r'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'
if not re.match(ipv4_pattern, data):
validation_result["valid"] = False
validation_result["errors"].append("Invalid IPv4 address format")
elif record_type == 'AAAA':
if '::' in data and data.count('::') > 1:
validation_result["valid"] = False
validation_result["errors"].append("Invalid IPv6 address: multiple :: sequences")
elif record_type == 'CNAME':
if name == '@' or name == '':
validation_result["valid"] = False
validation_result["errors"].append("CNAME records cannot be used for root domain (@)")
elif record_type == 'MX':
if priority is None:
validation_result["valid"] = False
validation_result["errors"].append("MX records require a priority value")
elif priority < 0 or priority > 65535:
validation_result["valid"] = False
validation_result["errors"].append("MX priority must be between 0 and 65535")
elif record_type == 'SRV':
if priority is None:
validation_result["valid"] = False
validation_result["errors"].append("SRV records require a priority value")
srv_parts = data.split()
if len(srv_parts) != 3:
validation_result["valid"] = False
validation_result["errors"].append("SRV data must be in format: 'weight port target'")
result = {
"record_type": record_type,
"name": name,
"data": data,
"ttl": ttl,
"priority": priority,
"validation": validation_result
}
return [TextContent(type="text", text=str(result))]
# Generate recommendations elif name == "analyze_dns_records":
recommendations = [] domain = arguments["domain"]
issues = [] records = await vultr_client.list_records(domain)
if not has_root_a: # Analyze records
recommendations.append("Consider adding an A record for the root domain (@)") record_types = {}
if not has_www: total_records = len(records)
recommendations.append("Consider adding a www subdomain (A or CNAME record)") ttl_values = []
if not has_mx and total_records > 1: has_root_a = False
recommendations.append("Consider adding MX records if you plan to use email") has_www = False
if has_mx and not has_spf: has_mx = False
recommendations.append("Add SPF record (TXT) to prevent email spoofing") has_spf = False
avg_ttl = sum(ttl_values) / len(ttl_values) if ttl_values else 0 for record in records:
low_ttl_count = sum(1 for ttl in ttl_values if ttl < 300) record_type = record.get('type', 'UNKNOWN')
record_name = record.get('name', '')
if low_ttl_count > total_records * 0.5: record_data = record.get('data', '')
issues.append("Many records have very low TTL values, which may impact performance") ttl = record.get('ttl', 300)
return { record_types[record_type] = record_types.get(record_type, 0) + 1
"domain": domain, ttl_values.append(ttl)
"analysis": {
"total_records": total_records, if record_type == 'A' and record_name in ['@', domain]:
"record_types": record_types, has_root_a = True
"average_ttl": round(avg_ttl), if record_name == 'www':
"configuration_status": { has_www = True
"has_root_domain": has_root_a, if record_type == 'MX':
"has_www_subdomain": has_www, has_mx = True
"has_email_mx": has_mx, if record_type == 'TXT' and 'spf1' in record_data.lower():
"has_spf_protection": has_spf has_spf = True
}
}, # Generate recommendations
"recommendations": recommendations, recommendations = []
"potential_issues": issues, issues = []
"records_detail": records
} if not has_root_a:
recommendations.append("Consider adding an A record for the root domain (@)")
if not has_www:
recommendations.append("Consider adding a www subdomain (A or CNAME record)")
if not has_mx and total_records > 1:
recommendations.append("Consider adding MX records if you plan to use email")
if has_mx and not has_spf:
recommendations.append("Add SPF record (TXT) to prevent email spoofing")
avg_ttl = sum(ttl_values) / len(ttl_values) if ttl_values else 0
low_ttl_count = sum(1 for ttl in ttl_values if ttl < 300)
if low_ttl_count > total_records * 0.5:
issues.append("Many records have very low TTL values, which may impact performance")
result = {
"domain": domain,
"analysis": {
"total_records": total_records,
"record_types": record_types,
"average_ttl": round(avg_ttl),
"configuration_status": {
"has_root_domain": has_root_a,
"has_www_subdomain": has_www,
"has_email_mx": has_mx,
"has_spf_protection": has_spf
}
},
"recommendations": recommendations,
"potential_issues": issues,
"records_detail": records
}
return [TextContent(type="text", text=str(result))]
else:
return [TextContent(type="text", text=f"Unknown tool: {name}")]
except Exception as e: except Exception as e:
return {"error": str(e), "domain": domain} return [TextContent(type="text", text=f"Error: {str(e)}")]
return mcp return server
def run_server(api_key: Optional[str] = None) -> None: async def run_server(api_key: Optional[str] = None) -> None:
""" """
Create and run a Vultr DNS MCP server. Create and run a Vultr DNS MCP server.
Args: Args:
api_key: Vultr API key. If not provided, will read from VULTR_API_KEY env var. api_key: Vultr API key. If not provided, will read from VULTR_API_KEY env var.
""" """
mcp = create_mcp_server(api_key) server = create_mcp_server(api_key)
mcp.run() async with stdio_server() as (read_stream, write_stream):
await server.run(read_stream, write_stream, None)