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)}
}
@mcp.resource("vultr://capabilities") @server.read_resource()
async def get_capabilities_resource(): async def read_resource(uri: str) -> str:
"""Resource describing server capabilities and supported record types.""" """Read a specific resource."""
return { if uri == "vultr://domains":
"uri": "vultr://capabilities", try:
"name": "Server Capabilities", domains = await vultr_client.list_domains()
"description": "Vultr DNS server capabilities and supported features", return str(domains)
"mimeType": "application/json", except Exception as e:
"content": { return f"Error loading domains: {str(e)}"
elif uri == "vultr://capabilities":
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(
name="list_dns_domains",
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"]
}
)
]
This tool retrieves all domains currently managed through Vultr DNS. @server.call_tool()
Each domain object includes domain name, creation date, and status information. 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() elif name == "get_dns_domain":
async def get_dns_domain(domain: str) -> Dict[str, Any]: domain = arguments["domain"]
""" result = await vultr_client.get_domain(domain)
Get detailed information for a specific DNS domain. return [TextContent(type="text", text=str(result))]
Args: elif name == "create_dns_domain":
domain: The domain name to retrieve (e.g., "example.com") domain = arguments["domain"]
""" ip = arguments["ip"]
try: result = await vultr_client.create_domain(domain, ip)
return await vultr_client.get_domain(domain) return [TextContent(type="text", text=str(result))]
except Exception as e:
return {"error": str(e)}
@mcp.tool() elif name == "delete_dns_domain":
async def create_dns_domain(domain: str, ip: str) -> Dict[str, Any]: domain = arguments["domain"]
""" await vultr_client.delete_domain(domain)
Create a new DNS domain with a default A record. return [TextContent(type="text", text=f"Domain {domain} deleted successfully")]
Args: elif name == "list_dns_records":
domain: The domain name to create (e.g., "newdomain.com") domain = arguments["domain"]
ip: IPv4 address for the default A record (e.g., "192.168.1.100") records = await vultr_client.list_records(domain)
""" return [TextContent(type="text", text=str(records))]
try:
return await vultr_client.create_domain(domain, ip)
except Exception as e:
return {"error": str(e)}
@mcp.tool() elif name == "get_dns_record":
async def delete_dns_domain(domain: str) -> Dict[str, Any]: domain = arguments["domain"]
""" record_id = arguments["record_id"]
Delete a DNS domain and ALL its associated records. result = await vultr_client.get_record(domain, record_id)
return [TextContent(type="text", text=str(result))]
WARNING: This permanently deletes the domain and all DNS records. 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))]
Args: elif name == "update_dns_record":
domain: The domain name to delete (e.g., "example.com") domain = arguments["domain"]
""" record_id = arguments["record_id"]
try: record_type = arguments["record_type"]
await vultr_client.delete_domain(domain) name = arguments["name"]
return {"success": f"Domain {domain} deleted successfully"} data = arguments["data"]
except Exception as e: ttl = arguments.get("ttl")
return {"error": str(e)} 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))]
@mcp.tool() elif name == "delete_dns_record":
async def list_dns_records(domain: str) -> List[Dict[str, Any]]: domain = arguments["domain"]
""" record_id = arguments["record_id"]
List all DNS records for a specific domain. await vultr_client.delete_record(domain, record_id)
return [TextContent(type="text", text=f"DNS record {record_id} deleted successfully")]
Args: elif name == "validate_dns_record":
domain: The domain name (e.g., "example.com") record_type = arguments["record_type"]
""" name = arguments["name"]
try: data = arguments["data"]
records = await vultr_client.list_records(domain) ttl = arguments.get("ttl")
return records priority = arguments.get("priority")
except Exception as e:
return [{"error": str(e)}]
@mcp.tool() validation_result = {
async def get_dns_record(domain: str, record_id: str) -> Dict[str, Any]: "valid": True,
""" "errors": [],
Get detailed information for a specific DNS record. "warnings": [],
"suggestions": []
}
Args: # Validate record type
domain: The domain name (e.g., "example.com") valid_types = ['A', 'AAAA', 'CNAME', 'MX', 'TXT', 'NS', 'SRV']
record_id: The unique record identifier if record_type.upper() not in valid_types:
""" validation_result["valid"] = False
try: validation_result["errors"].append(f"Invalid record type. Must be one of: {', '.join(valid_types)}")
return await vultr_client.get_record(domain, record_id)
except Exception as e:
return {"error": str(e)}
@mcp.tool() record_type = record_type.upper()
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: # Validate TTL
domain: The domain name (e.g., "example.com") if ttl is not None:
record_type: Record type (A, AAAA, CNAME, MX, TXT, NS, SRV) if ttl < 60 or ttl > 86400:
name: Record name/subdomain validation_result["warnings"].append("TTL should be between 60 and 86400 seconds")
data: Record value elif ttl < 300:
ttl: Time to live in seconds (60-86400, default: 300) validation_result["warnings"].append("Low TTL values may impact DNS performance")
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() # Record-specific validation
async def update_dns_record( if record_type == 'A':
domain: str, 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]?)$'
record_id: str, if not re.match(ipv4_pattern, data):
record_type: str, validation_result["valid"] = False
name: str, validation_result["errors"].append("Invalid IPv4 address format")
data: str,
ttl: Optional[int] = None,
priority: Optional[int] = None
) -> Dict[str, Any]:
"""
Update an existing DNS record with new configuration.
Args: elif record_type == 'AAAA':
domain: The domain name (e.g., "example.com") if '::' in data and data.count('::') > 1:
record_id: The unique identifier of the record to update validation_result["valid"] = False
record_type: New record type (A, AAAA, CNAME, MX, TXT, NS, SRV) validation_result["errors"].append("Invalid IPv6 address: multiple :: sequences")
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() elif record_type == 'CNAME':
async def delete_dns_record(domain: str, record_id: str) -> Dict[str, Any]: if name == '@' or name == '':
""" validation_result["valid"] = False
Delete a specific DNS record. validation_result["errors"].append("CNAME records cannot be used for root domain (@)")
Args: elif record_type == 'MX':
domain: The domain name (e.g., "example.com") if priority is None:
record_id: The unique identifier of the record to delete validation_result["valid"] = False
""" validation_result["errors"].append("MX records require a priority value")
try: elif priority < 0 or priority > 65535:
await vultr_client.delete_record(domain, record_id) validation_result["valid"] = False
return {"success": f"DNS record {record_id} deleted successfully"} validation_result["errors"].append("MX priority must be between 0 and 65535")
except Exception as e:
return {"error": str(e)}
@mcp.tool() elif record_type == 'SRV':
async def validate_dns_record( if priority is None:
record_type: str, validation_result["valid"] = False
name: str, validation_result["errors"].append("SRV records require a priority value")
data: str, srv_parts = data.split()
ttl: Optional[int] = None, if len(srv_parts) != 3:
priority: Optional[int] = None validation_result["valid"] = False
) -> Dict[str, Any]: validation_result["errors"].append("SRV data must be in format: 'weight port target'")
"""
Validate DNS record parameters before creation.
Args: result = {
record_type: The record type (A, AAAA, CNAME, MX, TXT, NS, SRV) "record_type": record_type,
name: The record name/subdomain "name": name,
data: The record data/value "data": data,
ttl: Time to live in seconds (optional) "ttl": ttl,
priority: Priority for MX/SRV records (optional) "priority": priority,
""" "validation": validation_result
validation_result = { }
"valid": True, return [TextContent(type="text", text=str(result))]
"errors": [],
"warnings": [],
"suggestions": []
}
# Validate record type elif name == "analyze_dns_records":
valid_types = ['A', 'AAAA', 'CNAME', 'MX', 'TXT', 'NS', 'SRV'] domain = arguments["domain"]
if record_type.upper() not in valid_types: records = await vultr_client.list_records(domain)
validation_result["valid"] = False
validation_result["errors"].append(f"Invalid record type. Must be one of: {', '.join(valid_types)}")
record_type = record_type.upper() # Analyze records
record_types = {}
total_records = len(records)
ttl_values = []
has_root_a = False
has_www = False
has_mx = False
has_spf = False
# Validate TTL for record in records:
if ttl is not None: record_type = record.get('type', 'UNKNOWN')
if ttl < 60 or ttl > 86400: record_name = record.get('name', '')
validation_result["warnings"].append("TTL should be between 60 and 86400 seconds") record_data = record.get('data', '')
elif ttl < 300: ttl = record.get('ttl', 300)
validation_result["warnings"].append("Low TTL values may impact DNS performance")
# Record-specific validation record_types[record_type] = record_types.get(record_type, 0) + 1
if record_type == 'A': ttl_values.append(ttl)
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 record_type == 'A' and record_name in ['@', domain]:
if '::' in data and data.count('::') > 1: has_root_a = True
validation_result["valid"] = False if record_name == 'www':
validation_result["errors"].append("Invalid IPv6 address: multiple :: sequences") has_www = True
if record_type == 'MX':
has_mx = True
if record_type == 'TXT' and 'spf1' in record_data.lower():
has_spf = True
elif record_type == 'CNAME': # Generate recommendations
if name == '@' or name == '': recommendations = []
validation_result["valid"] = False issues = []
validation_result["errors"].append("CNAME records cannot be used for root domain (@)")
elif record_type == 'MX': if not has_root_a:
if priority is None: recommendations.append("Consider adding an A record for the root domain (@)")
validation_result["valid"] = False if not has_www:
validation_result["errors"].append("MX records require a priority value") recommendations.append("Consider adding a www subdomain (A or CNAME record)")
elif priority < 0 or priority > 65535: if not has_mx and total_records > 1:
validation_result["valid"] = False recommendations.append("Consider adding MX records if you plan to use email")
validation_result["errors"].append("MX priority must be between 0 and 65535") if has_mx and not has_spf:
recommendations.append("Add SPF record (TXT) to prevent email spoofing")
elif record_type == 'SRV': avg_ttl = sum(ttl_values) / len(ttl_values) if ttl_values else 0
if priority is None: low_ttl_count = sum(1 for ttl in ttl_values if ttl < 300)
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 { if low_ttl_count > total_records * 0.5:
"record_type": record_type, issues.append("Many records have very low TTL values, which may impact performance")
"name": name,
"data": data,
"ttl": ttl,
"priority": priority,
"validation": validation_result
}
@mcp.tool() result = {
async def analyze_dns_records(domain: str) -> Dict[str, Any]: "domain": domain,
""" "analysis": {
Analyze DNS configuration for a domain and provide insights. "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))]
Args: else:
domain: The domain name to analyze (e.g., "example.com") return [TextContent(type="text", text=f"Unknown tool: {name}")]
"""
try:
records = await vultr_client.list_records(domain)
# Analyze records
record_types = {}
total_records = len(records)
ttl_values = []
has_root_a = False
has_www = False
has_mx = False
has_spf = False
for record in records:
record_type = record.get('type', 'UNKNOWN')
record_name = record.get('name', '')
record_data = record.get('data', '')
ttl = record.get('ttl', 300)
record_types[record_type] = record_types.get(record_type, 0) + 1
ttl_values.append(ttl)
if record_type == 'A' and record_name in ['@', domain]:
has_root_a = True
if record_name == 'www':
has_www = True
if record_type == 'MX':
has_mx = True
if record_type == 'TXT' and 'spf1' in record_data.lower():
has_spf = True
# Generate recommendations
recommendations = []
issues = []
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")
return {
"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
}
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)