Migrate to FastMCP and add comprehensive improvements
Major changes: - Migrate from low-level MCP to FastMCP framework for better compatibility - Add custom exception hierarchy (VultrAPIError, VultrAuthError, etc.) - Replace basic IPv6 validation with Python's ipaddress module - Add HTTP request timeouts (30s total, 10s connect) - Modernize development workflow with uv package manager - Create FastMCP server with proper async/await patterns New features: - FastMCP server implementation with 12 DNS management tools - Comprehensive Claude Desktop integration guide - Enhanced error handling with specific exception types - Professional README with badges and examples - Complete testing suite with improvement validation Documentation: - CLAUDE.md: Consolidated project documentation - CLAUDE_DESKTOP_SETUP.md: Step-by-step Claude Desktop setup guide - Updated README.md with modern structure and uv-first approach - Enhanced TESTING.md with FastMCP testing patterns Development improvements: - Updated all scripts to use uv run commands - Smart development setup with uv/pip fallback - Added comprehensive test coverage for new features - PyPI-ready package configuration 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
7273fe8539
commit
75ffe33008
3
.gitignore
vendored
3
.gitignore
vendored
@ -91,6 +91,9 @@ ipython_config.py
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# uv
|
||||
uv.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
|
240
CLAUDE.md
Normal file
240
CLAUDE.md
Normal file
@ -0,0 +1,240 @@
|
||||
# Vultr DNS MCP Package
|
||||
|
||||
A Model Context Protocol (MCP) server for managing Vultr DNS domains and records through natural language interfaces.
|
||||
|
||||
## Project Overview
|
||||
|
||||
This package provides both an MCP server and a standalone Python client for Vultr DNS management. It supports all major DNS record types (A, AAAA, CNAME, MX, TXT, NS, SRV) with validation, analysis, and batch operations.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Installation
|
||||
```bash
|
||||
# Using uv (recommended)
|
||||
uv add vultr-dns-mcp
|
||||
|
||||
# Or using pip
|
||||
pip install vultr-dns-mcp
|
||||
```
|
||||
|
||||
### Basic Usage
|
||||
```bash
|
||||
# CLI - requires VULTR_API_KEY environment variable
|
||||
vultr-dns-mcp domains list
|
||||
vultr-dns-mcp records list example.com
|
||||
|
||||
# As MCP server
|
||||
uv run python -m vultr_dns_mcp.server --api-key YOUR_API_KEY
|
||||
```
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Dependencies
|
||||
```bash
|
||||
# Using uv (recommended)
|
||||
uv sync --extra dev
|
||||
|
||||
# Or using pip
|
||||
pip install -e .[dev]
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
```bash
|
||||
# All tests (using uv - recommended)
|
||||
uv run pytest
|
||||
|
||||
# Specific categories
|
||||
uv run pytest -m unit
|
||||
uv run pytest -m mcp
|
||||
uv run pytest -m integration
|
||||
|
||||
# With coverage
|
||||
uv run pytest --cov=vultr_dns_mcp --cov-report=html
|
||||
|
||||
# Comprehensive test runner
|
||||
uv run python run_tests.py --all-checks
|
||||
|
||||
# Traditional approach (fallback)
|
||||
pytest
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
```bash
|
||||
# Using uv (recommended)
|
||||
uv run black src tests
|
||||
uv run isort src tests
|
||||
uv run mypy src
|
||||
uv run flake8 src tests
|
||||
|
||||
# Traditional approach
|
||||
black src tests
|
||||
isort src tests
|
||||
mypy src
|
||||
flake8 src tests
|
||||
```
|
||||
|
||||
### Build & Publishing
|
||||
```bash
|
||||
# Using uv (recommended)
|
||||
uv build
|
||||
uv run twine check dist/*
|
||||
uv run twine upload --repository testpypi dist/*
|
||||
uv run twine upload dist/*
|
||||
|
||||
# Traditional approach
|
||||
python -m build
|
||||
python -m twine check dist/*
|
||||
python -m twine upload --repository testpypi dist/*
|
||||
python -m twine upload dist/*
|
||||
```
|
||||
|
||||
## Test Structure
|
||||
|
||||
Following FastMCP testing best practices:
|
||||
|
||||
- `tests/conftest.py` - Test configuration and fixtures
|
||||
- `tests/test_mcp_server.py` - MCP server functionality tests using in-memory pattern
|
||||
- `tests/test_client.py` - VultrDNSClient tests
|
||||
- `tests/test_cli.py` - CLI interface tests
|
||||
- `tests/test_vultr_server.py` - Core VultrDNSServer tests
|
||||
- `tests/test_package_validation.py` - Package integrity tests
|
||||
|
||||
### Test Markers
|
||||
- `@pytest.mark.unit` - Individual component testing
|
||||
- `@pytest.mark.integration` - Component interaction testing
|
||||
- `@pytest.mark.mcp` - MCP-specific functionality
|
||||
- `@pytest.mark.slow` - Performance and timeout tests
|
||||
|
||||
### Coverage Goals
|
||||
- Overall: 80%+ (enforced)
|
||||
- MCP Tools: 100% (critical functionality)
|
||||
- API Client: 95%+ (core functionality)
|
||||
- CLI Commands: 90%+ (user interface)
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
vultr-dns-mcp/
|
||||
├── src/vultr_dns_mcp/
|
||||
│ ├── client.py # High-level DNS client
|
||||
│ ├── server.py # Core Vultr API client
|
||||
│ ├── mcp_server.py # MCP server implementation
|
||||
│ └── cli.py # Command-line interface
|
||||
├── tests/ # Comprehensive test suite
|
||||
├── docs/ # Documentation
|
||||
└── pyproject.toml # Project configuration
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
### MCP Tools (12 total)
|
||||
- Domain management: list, create, delete, get details
|
||||
- DNS record operations: CRUD for all record types
|
||||
- Validation: Pre-creation validation with suggestions
|
||||
- Analysis: Configuration analysis with security recommendations
|
||||
|
||||
### MCP Resources
|
||||
- Domain discovery endpoints
|
||||
- DNS record resources
|
||||
- Configuration capabilities
|
||||
|
||||
### CLI Commands
|
||||
- Domain operations: `domains list|create|delete|get`
|
||||
- Record operations: `records list|create|update|delete`
|
||||
- Setup utilities: `setup website|email`
|
||||
|
||||
## Version History
|
||||
|
||||
### Current: 1.0.1
|
||||
- Fixed FastMCP server initialization
|
||||
- Corrected MCP server creation for current FastMCP version
|
||||
- Simplified initialization to use only name parameter
|
||||
|
||||
### 1.0.0 - Initial Release
|
||||
- Complete MCP server implementation
|
||||
- Python client library
|
||||
- CLI interface
|
||||
- Support for all DNS record types
|
||||
- Validation and analysis features
|
||||
- Comprehensive test suite
|
||||
|
||||
## CI/CD Pipeline
|
||||
|
||||
### GitHub Actions
|
||||
- Multi-Python testing (3.8-3.12)
|
||||
- Progressive test execution: validation → unit → integration → mcp
|
||||
- Code quality gates: black, isort, flake8, mypy
|
||||
- Security scanning: safety, bandit
|
||||
- Package validation: build, install, test CLI
|
||||
|
||||
### Quality Gates
|
||||
1. All tests pass on all Python versions
|
||||
2. Code coverage meets 80% threshold
|
||||
3. Code quality checks pass
|
||||
4. Security scans clean
|
||||
5. Package builds and installs correctly
|
||||
|
||||
## Publishing
|
||||
|
||||
### Automated (Recommended)
|
||||
1. Update version in `pyproject.toml`
|
||||
2. Commit and push changes
|
||||
3. Create and push version tag: `git tag v1.0.2 && git push origin v1.0.2`
|
||||
4. Workflow automatically publishes to PyPI
|
||||
|
||||
### Manual
|
||||
Use the GitHub Actions "Publish to PyPI" workflow with manual trigger.
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
### Testing
|
||||
- Use FastMCP in-memory testing pattern for MCP functionality
|
||||
- Mock external dependencies (Vultr API)
|
||||
- Maintain high coverage on critical paths
|
||||
- Follow pytest best practices
|
||||
|
||||
### Code Style
|
||||
- Black formatting
|
||||
- isort import sorting
|
||||
- Type hints with mypy
|
||||
- Comprehensive docstrings
|
||||
- Security best practices
|
||||
|
||||
### Release Process
|
||||
1. Update version in `pyproject.toml`
|
||||
2. Update `CHANGELOG.md`
|
||||
3. Run full test suite
|
||||
4. Create git tag
|
||||
5. Automated publishing via GitHub Actions
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
- **ImportError**: Run `pip install -e .` from repository root
|
||||
- **AsyncioError**: Ensure `asyncio_mode = "auto"` in pyproject.toml
|
||||
- **MockError**: Check that test fixtures are properly configured
|
||||
- **API Errors**: Verify VULTR_API_KEY environment variable
|
||||
|
||||
### Test Debugging
|
||||
```bash
|
||||
# Single test with verbose output (using uv)
|
||||
uv run pytest tests/test_mcp_server.py::TestMCPTools::test_list_dns_domains_tool -vvv
|
||||
|
||||
# Check pytest configuration
|
||||
uv run pytest --collect-only tests/
|
||||
|
||||
# Validate imports
|
||||
uv run python -c "from vultr_dns_mcp.server import create_mcp_server"
|
||||
|
||||
# Traditional approach
|
||||
pytest tests/test_mcp_server.py::TestMCPTools::test_list_dns_domains_tool -vvv
|
||||
pytest --collect-only tests/
|
||||
python -c "from vultr_dns_mcp.server import create_mcp_server"
|
||||
```
|
||||
|
||||
## Support & Documentation
|
||||
|
||||
- **GitHub**: https://github.com/rsp2k/vultr-dns-mcp
|
||||
- **PyPI**: https://pypi.org/project/vultr-dns-mcp/
|
||||
- **Documentation**: Complete API documentation and examples in package
|
||||
- **Issues**: Use GitHub Issues for bug reports and feature requests
|
248
CLAUDE_DESKTOP_SETUP.md
Normal file
248
CLAUDE_DESKTOP_SETUP.md
Normal file
@ -0,0 +1,248 @@
|
||||
# Testing Vultr DNS MCP in Claude Desktop
|
||||
|
||||
This guide shows how to set up and test the Vultr DNS MCP package locally with Claude Desktop.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Claude Desktop** installed on your machine
|
||||
2. **Vultr API Key** - Get from [Vultr Account > API](https://my.vultr.com/settings/#settingsapi)
|
||||
3. **Python 3.10+** with the package installed
|
||||
|
||||
## Installation Options
|
||||
|
||||
### Option 1: Install from PyPI (Recommended)
|
||||
```bash
|
||||
pip install vultr-dns-mcp
|
||||
```
|
||||
|
||||
### Option 2: Install from Local Development
|
||||
```bash
|
||||
# From this project directory
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
### Option 3: Using uv (Fastest)
|
||||
```bash
|
||||
uv add vultr-dns-mcp
|
||||
```
|
||||
|
||||
## Claude Desktop Configuration
|
||||
|
||||
### 1. Locate Claude Desktop Config
|
||||
|
||||
The configuration file location depends on your OS:
|
||||
|
||||
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||
- **Windows**: `%APPDATA%/Claude/claude_desktop_config.json`
|
||||
- **Linux**: `~/.config/Claude/claude_desktop_config.json`
|
||||
|
||||
### 2. Add MCP Server Configuration
|
||||
|
||||
Add this to your `claude_desktop_config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"vultr-dns": {
|
||||
"command": "python",
|
||||
"args": ["-m", "vultr_dns_mcp.server"],
|
||||
"env": {
|
||||
"VULTR_API_KEY": "YOUR_VULTR_API_KEY_HERE"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Alternative: Using uv (if you have it)
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"vultr-dns": {
|
||||
"command": "uv",
|
||||
"args": ["run", "python", "-m", "vultr_dns_mcp.server"],
|
||||
"env": {
|
||||
"VULTR_API_KEY": "YOUR_VULTR_API_KEY_HERE"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Using Absolute Path (Most Reliable)
|
||||
|
||||
If you have issues, use the absolute path to your Python installation:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"vultr-dns": {
|
||||
"command": "/usr/bin/python3",
|
||||
"args": ["-m", "vultr_dns_mcp.server"],
|
||||
"env": {
|
||||
"VULTR_API_KEY": "YOUR_VULTR_API_KEY_HERE"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing the Setup
|
||||
|
||||
### 1. Restart Claude Desktop
|
||||
After saving the configuration file, completely restart Claude Desktop.
|
||||
|
||||
### 2. Test MCP Connection
|
||||
In Claude Desktop, you should see:
|
||||
- An indicator that the MCP server is connected
|
||||
- Access to Vultr DNS tools in the interface
|
||||
|
||||
### 3. Example Prompts to Try
|
||||
|
||||
Once connected, try these prompts in Claude Desktop:
|
||||
|
||||
```
|
||||
"List all my DNS domains"
|
||||
```
|
||||
|
||||
```
|
||||
"Show me the DNS records for example.com"
|
||||
```
|
||||
|
||||
```
|
||||
"Create an A record for www.example.com pointing to 192.168.1.100"
|
||||
```
|
||||
|
||||
```
|
||||
"Analyze the DNS configuration for my domain and suggest improvements"
|
||||
```
|
||||
|
||||
```
|
||||
"Set up basic website DNS for newdomain.com with IP 203.0.113.10"
|
||||
```
|
||||
|
||||
## Available MCP Tools
|
||||
|
||||
The server provides these tools that Claude Desktop can use:
|
||||
|
||||
1. **list_dns_domains** - List all your DNS domains
|
||||
2. **get_dns_domain** - Get details for a specific domain
|
||||
3. **create_dns_domain** - Create a new DNS domain
|
||||
4. **delete_dns_domain** - Delete a domain and all its records
|
||||
5. **list_dns_records** - List DNS records for a domain
|
||||
6. **get_dns_record** - Get details for a specific record
|
||||
7. **create_dns_record** - Create a new DNS record
|
||||
8. **update_dns_record** - Update an existing record
|
||||
9. **delete_dns_record** - Delete a DNS record
|
||||
10. **validate_dns_record** - Validate a record before creation
|
||||
11. **analyze_dns_records** - Analyze domain DNS configuration
|
||||
12. **setup_website_dns** - Quick setup for website DNS
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### MCP Server Won't Start
|
||||
|
||||
1. **Check Python installation**:
|
||||
```bash
|
||||
python -c "import vultr_dns_mcp; print('✅ Package installed')"
|
||||
```
|
||||
|
||||
2. **Test server manually**:
|
||||
```bash
|
||||
export VULTR_API_KEY="your-key"
|
||||
python -m vultr_dns_mcp.server
|
||||
```
|
||||
|
||||
3. **Check API key**:
|
||||
```bash
|
||||
export VULTR_API_KEY="your-key"
|
||||
python -c "
|
||||
from vultr_dns_mcp.client import VultrDNSClient
|
||||
import asyncio
|
||||
async def test():
|
||||
client = VultrDNSClient()
|
||||
domains = await client.domains()
|
||||
print(f'✅ Found {len(domains)} domains')
|
||||
asyncio.run(test())
|
||||
"
|
||||
```
|
||||
|
||||
### Path Issues
|
||||
|
||||
If Claude Desktop can't find Python:
|
||||
|
||||
1. **Find your Python path**:
|
||||
```bash
|
||||
which python3
|
||||
# or
|
||||
which python
|
||||
```
|
||||
|
||||
2. **Update config with full path**:
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"vultr-dns": {
|
||||
"command": "/full/path/to/python3",
|
||||
"args": ["-m", "vultr_dns_mcp.server"],
|
||||
"env": {
|
||||
"VULTR_API_KEY": "YOUR_KEY"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Virtual Environment Issues
|
||||
|
||||
If using a virtual environment:
|
||||
|
||||
1. **Activate and find Python**:
|
||||
```bash
|
||||
source your-venv/bin/activate # or your-venv\Scripts\activate on Windows
|
||||
which python
|
||||
```
|
||||
|
||||
2. **Use that path in config**:
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"vultr-dns": {
|
||||
"command": "/path/to/your-venv/bin/python",
|
||||
"args": ["-m", "vultr_dns_mcp.server"],
|
||||
"env": {
|
||||
"VULTR_API_KEY": "YOUR_KEY"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Security Notes
|
||||
|
||||
- Keep your Vultr API key secure
|
||||
- Consider using environment variables instead of hardcoding in config
|
||||
- The API key has access to modify your DNS records
|
||||
|
||||
## Example Natural Language Interactions
|
||||
|
||||
Once set up, you can use natural language with Claude Desktop:
|
||||
|
||||
- "What domains do I have in Vultr?"
|
||||
- "Add a CNAME record for blog.example.com pointing to example.com"
|
||||
- "Delete the old MX record for example.com"
|
||||
- "Set up email DNS for my domain with mail.example.com as the mail server"
|
||||
- "Check if my domain has proper SPF and DMARC records"
|
||||
- "Create an IPv6 AAAA record for www pointing to 2001:db8::1"
|
||||
|
||||
Claude Desktop will use the MCP tools to perform these operations on your behalf!
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Try the natural language prompts above
|
||||
- Explore the comprehensive DNS management capabilities
|
||||
- Use the analysis features to improve your DNS setup
|
||||
- Set up automation for common DNS tasks
|
||||
|
||||
Happy DNS managing! 🎉
|
346
README.md
346
README.md
@ -1,177 +1,253 @@
|
||||
# Vultr DNS MCP Test Suite - Complete Fix Package
|
||||
# Vultr DNS MCP
|
||||
|
||||
## 🎯 Quick Solution
|
||||
A comprehensive Model Context Protocol (MCP) server for managing Vultr DNS records through natural language interfaces.
|
||||
|
||||
I've analyzed the broken tests in the vultr-dns-mcp repository and created a complete fix package. Here's how to apply it:
|
||||
[](https://www.python.org/downloads/)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://modelcontextprotocol.io/)
|
||||
|
||||
## Features
|
||||
|
||||
- **Complete MCP Server**: Full Model Context Protocol implementation with 12 tools and 3 resources
|
||||
- **Comprehensive DNS Management**: Support for all major record types (A, AAAA, CNAME, MX, TXT, NS, SRV)
|
||||
- **Intelligent Validation**: Pre-creation validation with helpful suggestions and warnings
|
||||
- **Configuration Analysis**: DNS setup analysis with security recommendations
|
||||
- **CLI Interface**: Complete command-line tool for direct DNS operations
|
||||
- **High-Level Client**: Convenient Python API for common operations
|
||||
- **Modern Development**: Fast development workflow with uv support
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Installation
|
||||
|
||||
### One-Command Fix (if you have access to this directory):
|
||||
```bash
|
||||
# From your vultr-dns-mcp repository root:
|
||||
bash /home/rpm/claude/vultr-dns-mcp-fix/fix_tests.sh
|
||||
# Using uv (recommended - fast and modern)
|
||||
uv add vultr-dns-mcp
|
||||
|
||||
# Or using pip
|
||||
pip install vultr-dns-mcp
|
||||
```
|
||||
|
||||
### Manual Fix (recommended):
|
||||
### Basic Usage
|
||||
|
||||
```bash
|
||||
# 1. Navigate to your repository
|
||||
cd /path/to/vultr-dns-mcp
|
||||
# Set your Vultr API key
|
||||
export VULTR_API_KEY="your-api-key"
|
||||
|
||||
# 2. Backup current files
|
||||
cp tests/conftest.py tests/conftest.py.backup
|
||||
cp tests/test_mcp_server.py tests/test_mcp_server.py.backup
|
||||
# List domains
|
||||
vultr-dns-mcp domains list
|
||||
|
||||
# 3. Copy fixed files
|
||||
cp /home/rpm/claude/vultr-dns-mcp-fix/fixed_conftest.py tests/conftest.py
|
||||
cp /home/rpm/claude/vultr-dns-mcp-fix/fixed_test_mcp_server.py tests/test_mcp_server.py
|
||||
# List DNS records
|
||||
vultr-dns-mcp records list example.com
|
||||
|
||||
# 4. Install dependencies
|
||||
# Set up basic website DNS
|
||||
vultr-dns-mcp setup-website example.com 192.168.1.100
|
||||
|
||||
# Run as MCP server
|
||||
uv run python -m vultr_dns_mcp.server
|
||||
```
|
||||
|
||||
### Python API
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from vultr_dns_mcp import VultrDNSClient
|
||||
|
||||
async def main():
|
||||
client = VultrDNSClient("your-api-key")
|
||||
|
||||
# List domains
|
||||
domains = await client.domains()
|
||||
|
||||
# Add DNS records
|
||||
await client.add_a_record("example.com", "www", "192.168.1.100")
|
||||
await client.add_mx_record("example.com", "@", "mail.example.com", 10)
|
||||
|
||||
# Get domain summary
|
||||
summary = await client.get_domain_summary("example.com")
|
||||
print(f"Domain has {summary['total_records']} records")
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
### MCP Integration
|
||||
|
||||
This package provides a complete MCP server that can be integrated with MCP-compatible clients:
|
||||
|
||||
```python
|
||||
from vultr_dns_mcp import create_mcp_server, run_server
|
||||
|
||||
# Create server
|
||||
server = create_mcp_server("your-api-key")
|
||||
|
||||
# Run server
|
||||
await run_server("your-api-key")
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.10+
|
||||
- [uv](https://docs.astral.sh/uv/) (recommended) or pip
|
||||
- Vultr API key
|
||||
|
||||
### Setup with uv (Recommended)
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/rsp2k/vultr-dns-mcp.git
|
||||
cd vultr-dns-mcp
|
||||
|
||||
# Install dependencies
|
||||
uv sync --extra dev
|
||||
|
||||
# Run tests
|
||||
uv run pytest
|
||||
|
||||
# Run comprehensive test suite
|
||||
uv run python run_tests.py --all-checks
|
||||
|
||||
# Format code
|
||||
uv run black src tests
|
||||
uv run isort src tests
|
||||
|
||||
# Type checking
|
||||
uv run mypy src
|
||||
```
|
||||
|
||||
### Setup with pip (Traditional)
|
||||
|
||||
```bash
|
||||
# Create virtual environment
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
||||
|
||||
# Install in development mode
|
||||
pip install -e .[dev]
|
||||
|
||||
# 5. Run tests
|
||||
pytest tests/ -v
|
||||
# Run tests
|
||||
pytest
|
||||
|
||||
# Run comprehensive test suite
|
||||
python run_tests.py --all-checks
|
||||
```
|
||||
|
||||
## 🔍 Problems Identified & Fixed
|
||||
## MCP Tools Available
|
||||
|
||||
| Issue | Severity | Status | Fix Applied |
|
||||
|-------|----------|--------|------------|
|
||||
| Import path problems | 🔴 Critical | ✅ Fixed | Updated all import statements |
|
||||
| Async/await patterns | 🔴 Critical | ✅ Fixed | Fixed FastMCP Client usage |
|
||||
| Mock configuration | 🟡 Medium | ✅ Fixed | Complete API response mocks |
|
||||
| Test data structure | 🟡 Medium | ✅ Fixed | Updated fixtures to match API |
|
||||
| Error handling gaps | 🟢 Low | ✅ Fixed | Added comprehensive error tests |
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `list_dns_domains` | List all DNS domains |
|
||||
| `get_dns_domain` | Get domain details |
|
||||
| `create_dns_domain` | Create new domain |
|
||||
| `delete_dns_domain` | Delete domain and all records |
|
||||
| `list_dns_records` | List records for a domain |
|
||||
| `get_dns_record` | Get specific record details |
|
||||
| `create_dns_record` | Create new DNS record |
|
||||
| `update_dns_record` | Update existing record |
|
||||
| `delete_dns_record` | Delete DNS record |
|
||||
| `validate_dns_record` | Validate record before creation |
|
||||
| `analyze_dns_records` | Analyze domain configuration |
|
||||
|
||||
## 📁 Files in This Fix Package
|
||||
|
||||
### Core Fixes
|
||||
- **`fixed_conftest.py`** - Updated test configuration with proper mocks
|
||||
- **`fixed_test_mcp_server.py`** - All MCP server tests with correct async patterns
|
||||
- **`fix_tests.sh`** - Automated installer script
|
||||
|
||||
### Documentation
|
||||
- **`FINAL_SOLUTION.md`** - Complete solution overview
|
||||
- **`COMPLETE_FIX_GUIDE.md`** - Detailed fix documentation
|
||||
|
||||
### Utilities
|
||||
- **`analyze_test_issues.py`** - Issue analysis script
|
||||
- **`comprehensive_test_fix.py`** - Complete fix generator
|
||||
- **`create_fixes.py`** - Simple fix creator
|
||||
|
||||
## 🚀 What Gets Fixed
|
||||
|
||||
### Before (Broken):
|
||||
```python
|
||||
# Incorrect async pattern
|
||||
async def test_tool(self, mcp_server):
|
||||
result = await client.call_tool("tool_name", {})
|
||||
# ❌ Missing proper async context
|
||||
# ❌ No mock configuration
|
||||
# ❌ Incomplete error handling
|
||||
```
|
||||
|
||||
### After (Fixed):
|
||||
```python
|
||||
@pytest.mark.asyncio
|
||||
async def test_tool(self, mock_vultr_client):
|
||||
with patch('vultr_dns_mcp.server.VultrDNSServer', return_value=mock_vultr_client):
|
||||
server = create_mcp_server("test-api-key")
|
||||
async with Client(server) as client: # ✅ Proper context manager
|
||||
result = await client.call_tool("tool_name", {})
|
||||
assert result is not None # ✅ Proper assertions
|
||||
mock_vultr_client.method.assert_called_once() # ✅ Mock verification
|
||||
```
|
||||
|
||||
## 🧪 Expected Test Results
|
||||
|
||||
After applying the fixes, you should see:
|
||||
## CLI Commands
|
||||
|
||||
```bash
|
||||
$ pytest tests/test_mcp_server.py -v
|
||||
# Domain management
|
||||
vultr-dns-mcp domains list
|
||||
vultr-dns-mcp domains info example.com
|
||||
vultr-dns-mcp domains create newdomain.com 192.168.1.100
|
||||
|
||||
tests/test_mcp_server.py::TestMCPServerBasics::test_server_creation PASSED
|
||||
tests/test_mcp_server.py::TestMCPTools::test_list_dns_domains_tool PASSED
|
||||
tests/test_mcp_server.py::TestMCPTools::test_get_dns_domain_tool PASSED
|
||||
tests/test_mcp_server.py::TestMCPTools::test_create_dns_domain_tool PASSED
|
||||
tests/test_mcp_server.py::TestMCPResources::test_domains_resource PASSED
|
||||
tests/test_mcp_server.py::TestMCPIntegration::test_complete_domain_workflow PASSED
|
||||
tests/test_mcp_server.py::TestValidationLogic::test_a_record_validation PASSED
|
||||
# Record management
|
||||
vultr-dns-mcp records list example.com
|
||||
vultr-dns-mcp records add example.com A www 192.168.1.100
|
||||
vultr-dns-mcp records delete example.com record-id
|
||||
|
||||
========================== 25 passed in 2.34s ==========================
|
||||
# Setup utilities
|
||||
vultr-dns-mcp setup-website example.com 192.168.1.100
|
||||
vultr-dns-mcp setup-email example.com mail.example.com
|
||||
|
||||
# Start MCP server
|
||||
vultr-dns-mcp server
|
||||
```
|
||||
|
||||
## 🔧 Key Technical Fixes
|
||||
## Testing
|
||||
|
||||
### 1. Fixed Async Patterns
|
||||
- Proper `@pytest.mark.asyncio` usage
|
||||
- Correct `async with Client(server) as client:` context managers
|
||||
- Fixed await patterns throughout
|
||||
This project follows FastMCP testing best practices with comprehensive test coverage:
|
||||
|
||||
### 2. Improved Mock Configuration
|
||||
- Complete `AsyncMock` setup with proper specs
|
||||
- All Vultr API methods properly mocked
|
||||
- Realistic API response structures
|
||||
```bash
|
||||
# Run all tests (uv)
|
||||
uv run pytest
|
||||
|
||||
### 3. Better Error Handling
|
||||
- Comprehensive error scenario testing
|
||||
- Graceful handling of API failures
|
||||
- Proper exception testing patterns
|
||||
# Run specific test categories
|
||||
uv run pytest -m unit # Unit tests
|
||||
uv run pytest -m integration # Integration tests
|
||||
uv run pytest -m mcp # MCP-specific tests
|
||||
|
||||
### 4. Updated Dependencies
|
||||
- Fixed pytest-asyncio configuration
|
||||
- Proper FastMCP version requirements
|
||||
- Added missing test dependencies
|
||||
# With coverage
|
||||
uv run pytest --cov=vultr_dns_mcp --cov-report=html
|
||||
|
||||
## 🆘 Troubleshooting
|
||||
# Full validation suite
|
||||
uv run python run_tests.py --all-checks
|
||||
```
|
||||
|
||||
### If tests still fail:
|
||||
## Contributing
|
||||
|
||||
1. **Check installation**:
|
||||
```bash
|
||||
pip list | grep -E "(pytest|fastmcp|httpx)"
|
||||
```
|
||||
1. Fork the repository
|
||||
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
||||
3. Make your changes
|
||||
4. Run the test suite (`uv run python run_tests.py --all-checks`)
|
||||
5. Commit your changes (`git commit -m 'Add amazing feature'`)
|
||||
6. Push to the branch (`git push origin feature/amazing-feature`)
|
||||
7. Open a Pull Request
|
||||
|
||||
2. **Verify imports**:
|
||||
```bash
|
||||
python -c "from vultr_dns_mcp.server import create_mcp_server"
|
||||
```
|
||||
## Error Handling
|
||||
|
||||
3. **Run single test**:
|
||||
```bash
|
||||
pytest tests/test_mcp_server.py::TestMCPTools::test_list_dns_domains_tool -vvv
|
||||
```
|
||||
The package provides specific exception types for better error handling:
|
||||
|
||||
4. **Check pytest config**:
|
||||
```bash
|
||||
pytest --collect-only tests/
|
||||
```
|
||||
```python
|
||||
from vultr_dns_mcp import (
|
||||
VultrAPIError,
|
||||
VultrAuthError,
|
||||
VultrRateLimitError,
|
||||
VultrResourceNotFoundError,
|
||||
VultrValidationError
|
||||
)
|
||||
|
||||
### Common Issues:
|
||||
- **ImportError**: Run `pip install -e .` from repository root
|
||||
- **AsyncioError**: Ensure `asyncio_mode = "auto"` in pyproject.toml
|
||||
- **MockError**: Check that fixed_conftest.py was properly copied
|
||||
try:
|
||||
await client.get_domain("example.com")
|
||||
except VultrAuthError:
|
||||
print("Invalid API key or insufficient permissions")
|
||||
except VultrResourceNotFoundError:
|
||||
print("Domain not found")
|
||||
except VultrRateLimitError:
|
||||
print("Rate limit exceeded, please try again later")
|
||||
except VultrAPIError as e:
|
||||
print(f"API error {e.status_code}: {e.message}")
|
||||
```
|
||||
|
||||
## 📊 Success Metrics
|
||||
## Configuration
|
||||
|
||||
You'll know the fix worked when:
|
||||
- ✅ Zero test failures in MCP test suite
|
||||
- ✅ All async tests run without warnings
|
||||
- ✅ Mock verification passes
|
||||
- ✅ Coverage >80% on core modules
|
||||
- ✅ Integration tests complete end-to-end
|
||||
Set your Vultr API key via environment variable:
|
||||
|
||||
## 🎉 Summary
|
||||
```bash
|
||||
export VULTR_API_KEY="your-vultr-api-key"
|
||||
```
|
||||
|
||||
This fix package addresses all the major issues in the vultr-dns-mcp test suite:
|
||||
Or pass directly to the client:
|
||||
|
||||
1. **Fixes critical async/await patterns** that were causing test failures
|
||||
2. **Provides comprehensive mock configuration** matching the Vultr API
|
||||
3. **Adds proper error handling tests** for robustness
|
||||
4. **Updates all import statements** to work correctly
|
||||
5. **Includes complete documentation** for maintenance
|
||||
```python
|
||||
client = VultrDNSClient("your-api-key")
|
||||
server = create_mcp_server("your-api-key")
|
||||
```
|
||||
|
||||
The fixed test suite follows FastMCP best practices and provides reliable, maintainable tests for the Vultr DNS MCP server functionality.
|
||||
## License
|
||||
|
||||
---
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
**Quick Start**: Copy `fixed_conftest.py` and `fixed_test_mcp_server.py` to your `tests/` directory, install dependencies with `pip install -e .[dev]`, and run `pytest tests/ -v`.
|
||||
## Links
|
||||
|
||||
**Need Help?** Check `FINAL_SOLUTION.md` for detailed instructions or `COMPLETE_FIX_GUIDE.md` for comprehensive documentation.
|
||||
- [GitHub Repository](https://github.com/rsp2k/vultr-dns-mcp)
|
||||
- [PyPI Package](https://pypi.org/project/vultr-dns-mcp/)
|
||||
- [Vultr API Documentation](https://www.vultr.com/api/)
|
||||
- [Model Context Protocol](https://modelcontextprotocol.io/)
|
||||
- [uv Package Manager](https://docs.astral.sh/uv/)
|
33
TESTING.md
33
TESTING.md
@ -104,32 +104,41 @@ def mock_vultr_client():
|
||||
|
||||
## 🚀 Running Tests
|
||||
|
||||
### Using pytest directly:
|
||||
### Using uv (recommended):
|
||||
```bash
|
||||
# All tests
|
||||
pytest
|
||||
uv run pytest
|
||||
|
||||
# Specific categories
|
||||
pytest -m unit
|
||||
pytest -m integration
|
||||
pytest -m mcp
|
||||
pytest -m "not slow"
|
||||
uv run pytest -m unit
|
||||
uv run pytest -m integration
|
||||
uv run pytest -m mcp
|
||||
uv run pytest -m "not slow"
|
||||
|
||||
# With coverage
|
||||
pytest --cov=vultr_dns_mcp --cov-report=html
|
||||
uv run pytest --cov=vultr_dns_mcp --cov-report=html
|
||||
```
|
||||
|
||||
### Using the test runner:
|
||||
```bash
|
||||
# Comprehensive test runner
|
||||
python run_tests.py
|
||||
# Comprehensive test runner (uv)
|
||||
uv run python run_tests.py
|
||||
|
||||
# Specific test types
|
||||
python run_tests.py --type unit --verbose
|
||||
python run_tests.py --type mcp --coverage
|
||||
python run_tests.py --fast # Skip slow tests
|
||||
uv run python run_tests.py --type unit --verbose
|
||||
uv run python run_tests.py --type mcp --coverage
|
||||
uv run python run_tests.py --fast # Skip slow tests
|
||||
|
||||
# Full validation
|
||||
uv run python run_tests.py --all-checks
|
||||
```
|
||||
|
||||
### Traditional approach (fallback):
|
||||
```bash
|
||||
# All tests
|
||||
pytest
|
||||
|
||||
# Test runner
|
||||
python run_tests.py --all-checks
|
||||
```
|
||||
|
||||
|
53
debug_server.py
Normal file
53
debug_server.py
Normal file
@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
from mcp.server import Server
|
||||
from mcp.server.stdio import stdio_server
|
||||
from mcp.types import Resource, Tool, TextContent
|
||||
|
||||
async def main():
|
||||
# Set up API key
|
||||
api_key = os.getenv("VULTR_API_KEY")
|
||||
if not api_key:
|
||||
print("VULTR_API_KEY not set", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Starting server with API key: {api_key[:8]}...", file=sys.stderr)
|
||||
|
||||
# Create minimal server
|
||||
server = Server("vultr-dns-debug")
|
||||
|
||||
@server.list_tools()
|
||||
async def list_tools() -> list[Tool]:
|
||||
return [
|
||||
Tool(
|
||||
name="test_tool",
|
||||
description="A simple test tool",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
@server.call_tool()
|
||||
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
|
||||
if name == "test_tool":
|
||||
return [TextContent(type="text", text="Test tool working!")]
|
||||
return [TextContent(type="text", text=f"Unknown tool: {name}")]
|
||||
|
||||
print("Server configured, starting stdio...", file=sys.stderr)
|
||||
|
||||
try:
|
||||
async with stdio_server() as (read_stream, write_stream):
|
||||
print("Running server...", file=sys.stderr)
|
||||
await server.run(read_stream, write_stream, None)
|
||||
except Exception as e:
|
||||
print(f"Server error: {e}", file=sys.stderr)
|
||||
raise
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
@ -10,29 +10,59 @@ echo "🔧 Installing vultr-dns-mcp in development mode..."
|
||||
# Change to package directory
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Check if we're in a virtual environment
|
||||
if [[ -z "$VIRTUAL_ENV" ]]; then
|
||||
# Check for uv first, fall back to pip
|
||||
if command -v uv &> /dev/null; then
|
||||
echo "📦 Using uv for fast, modern dependency management..."
|
||||
|
||||
# Sync dependencies with dev extras
|
||||
echo "🔄 Syncing dependencies..."
|
||||
uv sync --extra dev
|
||||
|
||||
echo "✅ Installation complete!"
|
||||
echo ""
|
||||
echo "🚀 You can now run:"
|
||||
echo " vultr-dns-mcp --help"
|
||||
echo " vultr-dns-mcp server"
|
||||
echo ""
|
||||
echo "🧪 Run tests with:"
|
||||
echo " uv run pytest"
|
||||
echo " uv run python run_tests.py --all-checks"
|
||||
echo ""
|
||||
echo "🔧 Code quality tools:"
|
||||
echo " uv run black src tests"
|
||||
echo " uv run mypy src"
|
||||
echo ""
|
||||
|
||||
else
|
||||
echo "📦 Using pip (consider installing uv for faster dependency management)..."
|
||||
echo " Install uv: curl -LsSf https://astral.sh/uv/install.sh | sh"
|
||||
echo ""
|
||||
|
||||
# Check if we're in a virtual environment
|
||||
if [[ -z "$VIRTUAL_ENV" ]]; then
|
||||
echo "⚠️ Warning: Not in a virtual environment"
|
||||
echo " Consider running: python -m venv .venv && source .venv/bin/activate"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Install in development mode
|
||||
echo "📦 Installing package dependencies..."
|
||||
pip install -e .
|
||||
|
||||
echo "🧪 Installing development dependencies..."
|
||||
pip install -e .[dev]
|
||||
|
||||
echo "✅ Installation complete!"
|
||||
echo ""
|
||||
echo "🚀 You can now run:"
|
||||
echo " vultr-dns-mcp --help"
|
||||
echo " vultr-dns-mcp server"
|
||||
echo ""
|
||||
echo "🧪 Run tests with:"
|
||||
echo " pytest"
|
||||
echo " python run_tests.py --all-checks"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Install in development mode
|
||||
echo "📦 Installing package dependencies..."
|
||||
pip install -e .
|
||||
|
||||
echo "🧪 Installing development dependencies..."
|
||||
pip install -e .[dev]
|
||||
|
||||
echo "✅ Installation complete!"
|
||||
echo ""
|
||||
echo "🚀 You can now run:"
|
||||
echo " vultr-dns-mcp --help"
|
||||
echo " vultr-dns-mcp server"
|
||||
echo ""
|
||||
echo "🧪 Run tests with:"
|
||||
echo " python test_fix.py"
|
||||
echo " pytest"
|
||||
echo ""
|
||||
echo "📝 Set your API key:"
|
||||
echo " export VULTR_API_KEY='your-api-key-here'"
|
||||
|
@ -43,7 +43,7 @@ classifiers = [
|
||||
]
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"mcp>=1.0.0",
|
||||
"fastmcp>=0.1.0",
|
||||
"httpx>=0.24.0",
|
||||
"pydantic>=2.0.0",
|
||||
"click>=8.0.0"
|
||||
@ -180,3 +180,16 @@ exclude_lines = [
|
||||
"if 0:",
|
||||
"if __name__ == .__main__.:"
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
dev-dependencies = [
|
||||
"pytest>=7.0.0",
|
||||
"pytest-asyncio>=0.21.0",
|
||||
"pytest-cov>=4.0.0",
|
||||
"black>=23.0.0",
|
||||
"isort>=5.12.0",
|
||||
"flake8>=6.0.0",
|
||||
"mypy>=1.0.0",
|
||||
"pre-commit>=3.0.0",
|
||||
"twine>=4.0.0"
|
||||
]
|
||||
|
20
run_tests.py
20
run_tests.py
@ -18,8 +18,8 @@ def run_tests(test_type="all", verbose=False, coverage=False, fast=False):
|
||||
# Change to package directory
|
||||
package_dir = Path(__file__).parent
|
||||
|
||||
# Base pytest command
|
||||
cmd = ["python", "-m", "pytest"]
|
||||
# Base pytest command using uv run
|
||||
cmd = ["uv", "run", "pytest"]
|
||||
|
||||
# Add verbosity
|
||||
if verbose:
|
||||
@ -73,7 +73,7 @@ def run_tests(test_type="all", verbose=False, coverage=False, fast=False):
|
||||
return result.returncode == 0
|
||||
|
||||
except FileNotFoundError:
|
||||
print("❌ Error: pytest not found. Install with: pip install pytest")
|
||||
print("❌ Error: pytest not found. Install with: uv add pytest")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ Error running tests: {e}")
|
||||
@ -86,10 +86,10 @@ def run_linting():
|
||||
print("=" * 50)
|
||||
|
||||
checks = [
|
||||
(["python", "-m", "black", "--check", "src", "tests"], "Black formatting"),
|
||||
(["python", "-m", "isort", "--check", "src", "tests"], "Import sorting"),
|
||||
(["python", "-m", "flake8", "src", "tests"], "Flake8 linting"),
|
||||
(["python", "-m", "mypy", "src"], "Type checking")
|
||||
(["uv", "run", "black", "--check", "src", "tests"], "Black formatting"),
|
||||
(["uv", "run", "isort", "--check", "src", "tests"], "Import sorting"),
|
||||
(["uv", "run", "flake8", "src", "tests"], "Flake8 linting"),
|
||||
(["uv", "run", "mypy", "src"], "Type checking")
|
||||
]
|
||||
|
||||
all_passed = True
|
||||
@ -209,9 +209,9 @@ def main():
|
||||
if success:
|
||||
print("🎉 All checks passed!")
|
||||
print("\n📚 Next steps:")
|
||||
print(" • Run 'python -m build' to build the package")
|
||||
print(" • Run 'python -m twine check dist/*' to validate")
|
||||
print(" • Upload to PyPI with 'python -m twine upload dist/*'")
|
||||
print(" • Run 'uv build' to build the package")
|
||||
print(" • Run 'uv run twine check dist/*' to validate")
|
||||
print(" • Upload to PyPI with 'uv run twine upload dist/*'")
|
||||
else:
|
||||
print("❌ Some checks failed. Please fix the issues above.")
|
||||
sys.exit(1)
|
||||
|
26
simple_fastmcp.py
Normal file
26
simple_fastmcp.py
Normal file
@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import os
|
||||
from fastmcp import FastMCP
|
||||
|
||||
print("SIMPLE FASTMCP SERVER STARTING", file=sys.stderr)
|
||||
|
||||
# Create FastMCP server
|
||||
mcp = FastMCP(name="vultr-dns-simple")
|
||||
|
||||
@mcp.tool
|
||||
def test_tool() -> str:
|
||||
"""A simple test tool"""
|
||||
return "Hello from Vultr DNS MCP!"
|
||||
|
||||
@mcp.tool
|
||||
def list_domains() -> str:
|
||||
"""List DNS domains"""
|
||||
return "This would list your DNS domains"
|
||||
|
||||
print("TOOLS REGISTERED, STARTING SERVER", file=sys.stderr)
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("RUNNING MCP SERVER", file=sys.stderr)
|
||||
mcp.run()
|
@ -23,7 +23,16 @@ Main functions:
|
||||
run_server: Convenience function to run the MCP server
|
||||
"""
|
||||
|
||||
from .server import VultrDNSServer, create_mcp_server, run_server
|
||||
from .server import (
|
||||
VultrDNSServer,
|
||||
create_mcp_server,
|
||||
run_server,
|
||||
VultrAPIError,
|
||||
VultrAuthError,
|
||||
VultrRateLimitError,
|
||||
VultrResourceNotFoundError,
|
||||
VultrValidationError
|
||||
)
|
||||
from .client import VultrDNSClient
|
||||
from ._version import __version__, __version_info__
|
||||
|
||||
@ -32,6 +41,11 @@ __all__ = [
|
||||
"VultrDNSClient",
|
||||
"create_mcp_server",
|
||||
"run_server",
|
||||
"VultrAPIError",
|
||||
"VultrAuthError",
|
||||
"VultrRateLimitError",
|
||||
"VultrResourceNotFoundError",
|
||||
"VultrValidationError",
|
||||
"__version__",
|
||||
"__version_info__"
|
||||
]
|
||||
|
17
src/vultr_dns_mcp/__main__.py
Normal file
17
src/vultr_dns_mcp/__main__.py
Normal file
@ -0,0 +1,17 @@
|
||||
"""
|
||||
Main entry point for running the Vultr DNS FastMCP server.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from .fastmcp_server import run_server
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
run_server()
|
||||
except KeyboardInterrupt:
|
||||
print("Server stopped by user", file=sys.stderr)
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(f"Server error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
208
src/vultr_dns_mcp/fastmcp_server.py
Normal file
208
src/vultr_dns_mcp/fastmcp_server.py
Normal file
@ -0,0 +1,208 @@
|
||||
"""
|
||||
Vultr DNS FastMCP Server Implementation.
|
||||
|
||||
This module contains the FastMCP server implementation for managing DNS records
|
||||
through the Vultr API using the FastMCP framework.
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Optional, List, Dict, Any
|
||||
from fastmcp import FastMCP
|
||||
from .server import VultrDNSServer
|
||||
|
||||
|
||||
def create_vultr_mcp_server(api_key: Optional[str] = None) -> FastMCP:
|
||||
"""
|
||||
Create a FastMCP server for Vultr DNS management.
|
||||
|
||||
Args:
|
||||
api_key: Vultr API key. If not provided, will read from VULTR_API_KEY env var.
|
||||
|
||||
Returns:
|
||||
Configured FastMCP server instance
|
||||
"""
|
||||
if not api_key:
|
||||
api_key = os.getenv("VULTR_API_KEY")
|
||||
|
||||
if not api_key:
|
||||
raise ValueError(
|
||||
"VULTR_API_KEY must be provided either as parameter or environment variable"
|
||||
)
|
||||
|
||||
# Create FastMCP server
|
||||
mcp = FastMCP(name="vultr-dns-mcp")
|
||||
|
||||
# Initialize Vultr client
|
||||
vultr_client = VultrDNSServer(api_key)
|
||||
|
||||
@mcp.tool
|
||||
async def list_dns_domains() -> List[Dict[str, Any]]:
|
||||
"""List all DNS domains in your Vultr account."""
|
||||
return await vultr_client.list_domains()
|
||||
|
||||
@mcp.tool
|
||||
async def get_dns_domain(domain: str) -> Dict[str, Any]:
|
||||
"""Get details for a specific DNS domain.
|
||||
|
||||
Args:
|
||||
domain: The domain name to get details for
|
||||
"""
|
||||
return await vultr_client.get_domain(domain)
|
||||
|
||||
@mcp.tool
|
||||
async def create_dns_domain(domain: str, ip: str, dns_sec: str = "disabled") -> Dict[str, Any]:
|
||||
"""Create a new DNS domain.
|
||||
|
||||
Args:
|
||||
domain: The domain name to create
|
||||
ip: The default IP address for the domain
|
||||
dns_sec: Enable DNSSEC (enabled/disabled, default: disabled)
|
||||
"""
|
||||
return await vultr_client.create_domain(domain, ip, dns_sec)
|
||||
|
||||
@mcp.tool
|
||||
async def delete_dns_domain(domain: str) -> Dict[str, str]:
|
||||
"""Delete a DNS domain and all its records.
|
||||
|
||||
Args:
|
||||
domain: The domain name to delete
|
||||
"""
|
||||
await vultr_client.delete_domain(domain)
|
||||
return {"status": "success", "message": f"Domain {domain} deleted successfully"}
|
||||
|
||||
@mcp.tool
|
||||
async def list_dns_records(domain: str) -> List[Dict[str, Any]]:
|
||||
"""List all DNS records for a domain.
|
||||
|
||||
Args:
|
||||
domain: The domain name to list records for
|
||||
"""
|
||||
return await vultr_client.list_records(domain)
|
||||
|
||||
@mcp.tool
|
||||
async def get_dns_record(domain: str, record_id: str) -> Dict[str, Any]:
|
||||
"""Get details for a specific DNS record.
|
||||
|
||||
Args:
|
||||
domain: The domain name
|
||||
record_id: The record ID to get details for
|
||||
"""
|
||||
return await vultr_client.get_record(domain, record_id)
|
||||
|
||||
@mcp.tool
|
||||
async def create_dns_record(
|
||||
domain: str,
|
||||
record_type: str,
|
||||
name: str,
|
||||
data: str,
|
||||
ttl: int = 300,
|
||||
priority: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a new DNS record.
|
||||
|
||||
Args:
|
||||
domain: The domain name
|
||||
record_type: Record type (A, AAAA, CNAME, MX, TXT, NS, SRV)
|
||||
name: Record name/subdomain
|
||||
data: Record data/value
|
||||
ttl: Time to live in seconds (default: 300)
|
||||
priority: Priority for MX/SRV records
|
||||
"""
|
||||
return await vultr_client.create_record(domain, record_type, name, data, ttl, priority)
|
||||
|
||||
@mcp.tool
|
||||
async def update_dns_record(
|
||||
domain: str,
|
||||
record_id: str,
|
||||
name: Optional[str] = None,
|
||||
data: Optional[str] = None,
|
||||
ttl: Optional[int] = None,
|
||||
priority: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Update an existing DNS record.
|
||||
|
||||
Args:
|
||||
domain: The domain name
|
||||
record_id: The record ID to update
|
||||
name: New record name (optional)
|
||||
data: New record data (optional)
|
||||
ttl: New TTL value (optional)
|
||||
priority: New priority for MX/SRV records (optional)
|
||||
"""
|
||||
return await vultr_client.update_record(domain, record_id, name, data, ttl, priority)
|
||||
|
||||
@mcp.tool
|
||||
async def delete_dns_record(domain: str, record_id: str) -> Dict[str, str]:
|
||||
"""Delete a DNS record.
|
||||
|
||||
Args:
|
||||
domain: The domain name
|
||||
record_id: The record ID to delete
|
||||
"""
|
||||
await vultr_client.delete_record(domain, record_id)
|
||||
return {"status": "success", "message": f"Record {record_id} deleted successfully"}
|
||||
|
||||
@mcp.tool
|
||||
async def validate_dns_record(
|
||||
record_type: str,
|
||||
name: str,
|
||||
data: str,
|
||||
ttl: int = 300,
|
||||
priority: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Validate a DNS record before creation.
|
||||
|
||||
Args:
|
||||
record_type: Record type (A, AAAA, CNAME, MX, TXT, NS, SRV)
|
||||
name: Record name/subdomain
|
||||
data: Record data/value
|
||||
ttl: Time to live in seconds
|
||||
priority: Priority for MX/SRV records
|
||||
"""
|
||||
return await vultr_client.validate_record(record_type, name, data, ttl, priority)
|
||||
|
||||
@mcp.tool
|
||||
async def analyze_dns_records(domain: str) -> Dict[str, Any]:
|
||||
"""Analyze DNS records for a domain and provide recommendations.
|
||||
|
||||
Args:
|
||||
domain: The domain name to analyze
|
||||
"""
|
||||
return await vultr_client.analyze_records(domain)
|
||||
|
||||
@mcp.tool
|
||||
async def setup_website_dns(domain: str, ip: str, www_enabled: bool = True) -> List[Dict[str, Any]]:
|
||||
"""Set up basic DNS records for a website.
|
||||
|
||||
Args:
|
||||
domain: The domain name
|
||||
ip: The website IP address
|
||||
www_enabled: Whether to create www subdomain record (default: True)
|
||||
"""
|
||||
records = []
|
||||
|
||||
# Create A record for domain
|
||||
records.append(await vultr_client.create_record(domain, "A", "@", ip))
|
||||
|
||||
# Create www CNAME if enabled
|
||||
if www_enabled:
|
||||
records.append(await vultr_client.create_record(domain, "CNAME", "www", domain))
|
||||
|
||||
return records
|
||||
|
||||
return mcp
|
||||
|
||||
|
||||
def run_server(api_key: Optional[str] = None) -> None:
|
||||
"""
|
||||
Create and run a Vultr DNS FastMCP server.
|
||||
|
||||
Args:
|
||||
api_key: Vultr API key. If not provided, will read from VULTR_API_KEY env var.
|
||||
"""
|
||||
mcp = create_vultr_mcp_server(api_key)
|
||||
mcp.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_server()
|
@ -5,6 +5,7 @@ This module contains the main VultrDNSServer class and MCP server implementation
|
||||
for managing DNS records through the Vultr API.
|
||||
"""
|
||||
|
||||
import ipaddress
|
||||
import os
|
||||
import re
|
||||
from typing import Any, Dict, List, Optional
|
||||
@ -16,6 +17,35 @@ from mcp.types import Resource, Tool, TextContent
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class VultrAPIError(Exception):
|
||||
"""Base exception for Vultr API errors."""
|
||||
|
||||
def __init__(self, status_code: int, message: str):
|
||||
self.status_code = status_code
|
||||
self.message = message
|
||||
super().__init__(f"Vultr API error {status_code}: {message}")
|
||||
|
||||
|
||||
class VultrAuthError(VultrAPIError):
|
||||
"""Raised when API authentication fails (401, 403)."""
|
||||
pass
|
||||
|
||||
|
||||
class VultrRateLimitError(VultrAPIError):
|
||||
"""Raised when API rate limit is exceeded (429)."""
|
||||
pass
|
||||
|
||||
|
||||
class VultrResourceNotFoundError(VultrAPIError):
|
||||
"""Raised when requested resource is not found (404)."""
|
||||
pass
|
||||
|
||||
|
||||
class VultrValidationError(VultrAPIError):
|
||||
"""Raised when request validation fails (400, 422)."""
|
||||
pass
|
||||
|
||||
|
||||
class VultrDNSServer:
|
||||
"""
|
||||
Vultr DNS API client for managing domains and DNS records.
|
||||
@ -48,7 +78,10 @@ class VultrDNSServer:
|
||||
"""Make an HTTP request to the Vultr API."""
|
||||
url = f"{self.API_BASE}{endpoint}"
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
# Configure timeout: 30 seconds total, 10 seconds to connect
|
||||
timeout = httpx.Timeout(30.0, connect=10.0)
|
||||
|
||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||
response = await client.request(
|
||||
method=method,
|
||||
url=url,
|
||||
@ -57,7 +90,19 @@ class VultrDNSServer:
|
||||
)
|
||||
|
||||
if response.status_code not in [200, 201, 204]:
|
||||
raise Exception(f"Vultr API error {response.status_code}: {response.text}")
|
||||
# Raise specific exceptions based on status code
|
||||
if response.status_code == 401:
|
||||
raise VultrAuthError(response.status_code, "Invalid API key")
|
||||
elif response.status_code == 403:
|
||||
raise VultrAuthError(response.status_code, "Insufficient permissions")
|
||||
elif response.status_code == 404:
|
||||
raise VultrResourceNotFoundError(response.status_code, "Resource not found")
|
||||
elif response.status_code == 429:
|
||||
raise VultrRateLimitError(response.status_code, "Rate limit exceeded")
|
||||
elif response.status_code in [400, 422]:
|
||||
raise VultrValidationError(response.status_code, response.text)
|
||||
else:
|
||||
raise VultrAPIError(response.status_code, response.text)
|
||||
|
||||
if response.status_code == 204:
|
||||
return {}
|
||||
@ -592,15 +637,32 @@ def create_mcp_server(api_key: Optional[str] = None) -> Server:
|
||||
|
||||
# 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):
|
||||
try:
|
||||
ipaddress.IPv4Address(data)
|
||||
except ipaddress.AddressValueError:
|
||||
validation_result["valid"] = False
|
||||
validation_result["errors"].append("Invalid IPv4 address format")
|
||||
|
||||
elif record_type == 'AAAA':
|
||||
if '::' in data and data.count('::') > 1:
|
||||
try:
|
||||
ipv6_addr = ipaddress.IPv6Address(data)
|
||||
# Add helpful suggestions for IPv6 addresses
|
||||
if ipv6_addr.ipv4_mapped:
|
||||
validation_result["suggestions"].append("Consider using a native IPv6 address instead of IPv4-mapped format")
|
||||
elif ipv6_addr.compressed != data:
|
||||
validation_result["suggestions"].append(f"Consider using compressed format: {ipv6_addr.compressed}")
|
||||
|
||||
# Check for common special addresses
|
||||
if ipv6_addr.is_loopback:
|
||||
validation_result["warnings"].append("This is the IPv6 loopback address (::1)")
|
||||
elif ipv6_addr.is_link_local:
|
||||
validation_result["warnings"].append("This is an IPv6 link-local address (fe80::/10)")
|
||||
elif ipv6_addr.is_private:
|
||||
validation_result["warnings"].append("This is an IPv6 private address")
|
||||
|
||||
except ipaddress.AddressValueError as e:
|
||||
validation_result["valid"] = False
|
||||
validation_result["errors"].append("Invalid IPv6 address: multiple :: sequences")
|
||||
validation_result["errors"].append(f"Invalid IPv6 address: {str(e)}")
|
||||
|
||||
elif record_type == 'CNAME':
|
||||
if name == '@' or name == '':
|
||||
|
37
test_async_fastmcp.py
Normal file
37
test_async_fastmcp.py
Normal file
@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from fastmcp import FastMCP
|
||||
|
||||
print("TESTING ASYNC FASTMCP PATTERNS", file=sys.stderr)
|
||||
|
||||
# Create FastMCP server
|
||||
mcp = FastMCP(name="async-test")
|
||||
|
||||
@mcp.tool
|
||||
def sync_tool() -> str:
|
||||
"""A synchronous test tool"""
|
||||
return "Sync tool working"
|
||||
|
||||
@mcp.tool
|
||||
async def async_tool() -> str:
|
||||
"""An asynchronous test tool"""
|
||||
await asyncio.sleep(0.1) # Simulate async work
|
||||
return "Async tool working"
|
||||
|
||||
@mcp.tool
|
||||
async def async_tool_with_params(name: str, count: int = 1) -> dict:
|
||||
"""An async tool with parameters"""
|
||||
await asyncio.sleep(0.1)
|
||||
return {
|
||||
"message": f"Hello {name}",
|
||||
"count": count,
|
||||
"status": "async success"
|
||||
}
|
||||
|
||||
print("TOOLS REGISTERED, STARTING SERVER", file=sys.stderr)
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("RUNNING ASYNC TEST SERVER", file=sys.stderr)
|
||||
mcp.run()
|
143
test_improvements.py
Normal file
143
test_improvements.py
Normal file
@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Test our improvements to the Vultr DNS MCP package."""
|
||||
|
||||
import ipaddress
|
||||
from vultr_dns_mcp.server import (
|
||||
VultrAPIError,
|
||||
VultrAuthError,
|
||||
VultrRateLimitError,
|
||||
VultrResourceNotFoundError,
|
||||
VultrValidationError
|
||||
)
|
||||
|
||||
def test_exceptions():
|
||||
"""Test our enhanced exception classes."""
|
||||
print("🧪 Testing Enhanced Exception Classes:")
|
||||
|
||||
# Test basic exception
|
||||
error = VultrAPIError(500, 'Test error')
|
||||
print(f"✅ VultrAPIError: {error}")
|
||||
|
||||
# Test specific exceptions
|
||||
auth_error = VultrAuthError(401, 'Auth failed')
|
||||
print(f"✅ VultrAuthError: {auth_error}")
|
||||
|
||||
rate_error = VultrRateLimitError(429, 'Too many requests')
|
||||
print(f"✅ VultrRateLimitError: {rate_error}")
|
||||
|
||||
not_found_error = VultrResourceNotFoundError(404, 'Not found')
|
||||
print(f"✅ VultrResourceNotFoundError: {not_found_error}")
|
||||
|
||||
validation_error = VultrValidationError(400, 'Bad request')
|
||||
print(f"✅ VultrValidationError: {validation_error}")
|
||||
|
||||
# Test inheritance
|
||||
print(f"✅ VultrAuthError is VultrAPIError: {isinstance(auth_error, VultrAPIError)}")
|
||||
print(f"✅ VultrAuthError status_code: {auth_error.status_code}")
|
||||
print(f"✅ VultrAuthError message: {auth_error.message}")
|
||||
print()
|
||||
|
||||
def test_ipv6_validation():
|
||||
"""Test enhanced IPv6 validation."""
|
||||
print("🧪 Testing Enhanced IPv6 Validation:")
|
||||
|
||||
test_addresses = [
|
||||
('2001:db8::1', 'Standard format'),
|
||||
('::1', 'Loopback'),
|
||||
('::ffff:192.0.2.1', 'IPv4-mapped'),
|
||||
('2001:0db8:0000:0000:0000:0000:0000:0001', 'Full format'),
|
||||
('fe80::1', 'Link-local'),
|
||||
('invalid::address::bad', 'Invalid (multiple ::)'),
|
||||
('gggg::1', 'Invalid hex'),
|
||||
('192.168.1.1', 'IPv4 (should fail for IPv6)'),
|
||||
]
|
||||
|
||||
for addr, description in test_addresses:
|
||||
try:
|
||||
ipv6_addr = ipaddress.IPv6Address(addr)
|
||||
print(f"✅ {addr:<35} -> {ipv6_addr.compressed} ({description})")
|
||||
|
||||
# Test our enhanced features
|
||||
if ipv6_addr.ipv4_mapped:
|
||||
print(f" 📝 IPv4-mapped: {ipv6_addr.ipv4_mapped}")
|
||||
if ipv6_addr.is_loopback:
|
||||
print(f" 🔄 Loopback address")
|
||||
if ipv6_addr.is_link_local:
|
||||
print(f" 🔗 Link-local address")
|
||||
if ipv6_addr.is_private:
|
||||
print(f" 🔒 Private address")
|
||||
if ipv6_addr.compressed != addr:
|
||||
print(f" 💡 Could compress to: {ipv6_addr.compressed}")
|
||||
|
||||
except ipaddress.AddressValueError as e:
|
||||
print(f"❌ {addr:<35} -> Invalid: {e} ({description})")
|
||||
|
||||
print()
|
||||
|
||||
def test_mcp_server_creation():
|
||||
"""Test that we can create an MCP server (without API key)."""
|
||||
print("🧪 Testing MCP Server Creation:")
|
||||
|
||||
try:
|
||||
from vultr_dns_mcp.server import create_mcp_server
|
||||
|
||||
# This should fail without API key (expected)
|
||||
try:
|
||||
server = create_mcp_server(None)
|
||||
print("❌ Should have failed without API key")
|
||||
except ValueError as e:
|
||||
print(f"✅ Correctly failed without API key: {e}")
|
||||
|
||||
# Test with mock API key
|
||||
server = create_mcp_server("mock-api-key")
|
||||
print(f"✅ MCP server created successfully: {type(server)}")
|
||||
print(f"✅ Server has tools: {hasattr(server, '_tools')}")
|
||||
print(f"✅ Server has resources: {hasattr(server, '_resources')}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to create MCP server: {e}")
|
||||
|
||||
print()
|
||||
|
||||
def simulate_domain_query():
|
||||
"""Simulate what a domain query would look like."""
|
||||
print("🧪 Simulating Domain Query (Mock Response):")
|
||||
|
||||
# This is what the response would look like with a real API key
|
||||
mock_domains = [
|
||||
{
|
||||
"domain": "example.com",
|
||||
"date_created": "2024-01-01T00:00:00Z",
|
||||
"dns_sec": "disabled"
|
||||
},
|
||||
{
|
||||
"domain": "test-site.net",
|
||||
"date_created": "2024-06-15T12:30:00Z",
|
||||
"dns_sec": "enabled"
|
||||
}
|
||||
]
|
||||
|
||||
print("📋 Available domains (mock data):")
|
||||
for domain in mock_domains:
|
||||
name = domain['domain']
|
||||
created = domain['date_created']
|
||||
dnssec = domain['dns_sec']
|
||||
print(f" • {name:<20} (created: {created}, DNSSEC: {dnssec})")
|
||||
|
||||
print()
|
||||
print("💡 To query real domains, set VULTR_API_KEY and run:")
|
||||
print(" export VULTR_API_KEY='your-api-key'")
|
||||
print(" uv run vultr-dns-mcp domains list")
|
||||
print()
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🚀 Testing Vultr DNS MCP Improvements\n")
|
||||
print("=" * 50)
|
||||
|
||||
test_exceptions()
|
||||
test_ipv6_validation()
|
||||
test_mcp_server_creation()
|
||||
simulate_domain_query()
|
||||
|
||||
print("✅ All improvement tests completed successfully!")
|
||||
print("\n🎉 Ready to use with: uv run vultr-dns-mcp --help")
|
@ -362,6 +362,94 @@ class TestValidationLogic:
|
||||
})
|
||||
assert result is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_aaaa_record_validation(self, mcp_server):
|
||||
"""Test comprehensive AAAA (IPv6) record validation logic."""
|
||||
async with ClientSession(mcp_server) as session:
|
||||
# Valid IPv6 addresses
|
||||
valid_ipv6_addresses = [
|
||||
"2001:db8::1", # Standard format
|
||||
"2001:0db8:0000:0000:0000:0000:0000:0001", # Full format
|
||||
"::", # All zeros
|
||||
"::1", # Loopback
|
||||
"fe80::1", # Link-local
|
||||
"2001:db8:85a3::8a2e:370:7334", # Mixed compression
|
||||
"::ffff:192.0.2.1", # IPv4-mapped
|
||||
]
|
||||
|
||||
for ipv6_addr in valid_ipv6_addresses:
|
||||
result = await session.call_tool("validate_dns_record", {
|
||||
"record_type": "AAAA",
|
||||
"name": "www",
|
||||
"data": ipv6_addr
|
||||
})
|
||||
assert result is not None
|
||||
# Parse the result to check validation passed
|
||||
import json
|
||||
parsed = json.loads(result[0].text.replace("'", '"'))
|
||||
assert parsed["validation"]["valid"] == True, f"Failed to validate {ipv6_addr}"
|
||||
|
||||
# Invalid IPv6 addresses
|
||||
invalid_ipv6_addresses = [
|
||||
"2001:db8::1::2", # Multiple ::
|
||||
"2001:db8:85a3::8a2e::7334", # Multiple ::
|
||||
"gggg::1", # Invalid hex
|
||||
"2001:db8:85a3:0:0:8a2e:370g:7334", # Invalid character
|
||||
"2001:db8:85a3:0:0:8a2e:370:7334:extra", # Too many groups
|
||||
"", # Empty
|
||||
"192.168.1.1", # IPv4 instead of IPv6
|
||||
]
|
||||
|
||||
for ipv6_addr in invalid_ipv6_addresses:
|
||||
result = await session.call_tool("validate_dns_record", {
|
||||
"record_type": "AAAA",
|
||||
"name": "www",
|
||||
"data": ipv6_addr
|
||||
})
|
||||
assert result is not None
|
||||
# Parse the result to check validation failed
|
||||
import json
|
||||
parsed = json.loads(result[0].text.replace("'", '"'))
|
||||
assert parsed["validation"]["valid"] == False, f"Should have failed to validate {ipv6_addr}"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ipv6_suggestions_and_warnings(self, mcp_server):
|
||||
"""Test that IPv6 validation provides helpful suggestions and warnings."""
|
||||
async with ClientSession(mcp_server) as session:
|
||||
# Test IPv4-mapped suggestion
|
||||
result = await session.call_tool("validate_dns_record", {
|
||||
"record_type": "AAAA",
|
||||
"name": "www",
|
||||
"data": "::ffff:192.0.2.1"
|
||||
})
|
||||
assert result is not None
|
||||
import json
|
||||
parsed = json.loads(result[0].text.replace("'", '"'))
|
||||
suggestions = parsed["validation"]["suggestions"]
|
||||
assert any("IPv4-mapped" in s for s in suggestions)
|
||||
|
||||
# Test compression suggestion
|
||||
result = await session.call_tool("validate_dns_record", {
|
||||
"record_type": "AAAA",
|
||||
"name": "www",
|
||||
"data": "2001:0db8:0000:0000:0000:0000:0000:0001"
|
||||
})
|
||||
assert result is not None
|
||||
parsed = json.loads(result[0].text.replace("'", '"'))
|
||||
suggestions = parsed["validation"]["suggestions"]
|
||||
assert any("compressed format" in s for s in suggestions)
|
||||
|
||||
# Test loopback warning
|
||||
result = await session.call_tool("validate_dns_record", {
|
||||
"record_type": "AAAA",
|
||||
"name": "www",
|
||||
"data": "::1"
|
||||
})
|
||||
assert result is not None
|
||||
parsed = json.loads(result[0].text.replace("'", '"'))
|
||||
warnings = parsed["validation"]["warnings"]
|
||||
assert any("loopback" in w for w in warnings)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__])
|
||||
|
@ -3,7 +3,14 @@
|
||||
import pytest
|
||||
import httpx
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from vultr_dns_mcp.server import VultrDNSServer
|
||||
from vultr_dns_mcp.server import (
|
||||
VultrDNSServer,
|
||||
VultrAPIError,
|
||||
VultrAuthError,
|
||||
VultrRateLimitError,
|
||||
VultrResourceNotFoundError,
|
||||
VultrValidationError
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@ -74,10 +81,11 @@ class TestVultrDNSServer:
|
||||
with patch('httpx.AsyncClient') as mock_client:
|
||||
mock_client.return_value.__aenter__.return_value.request.return_value = mock_response
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(VultrValidationError) as exc_info:
|
||||
await server._make_request("GET", "/test")
|
||||
|
||||
assert "Vultr API error 400: Bad Request" in str(exc_info.value)
|
||||
assert exc_info.value.status_code == 400
|
||||
assert "Bad Request" in str(exc_info.value)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_make_request_error_401(self, mock_api_key):
|
||||
@ -91,10 +99,11 @@ class TestVultrDNSServer:
|
||||
with patch('httpx.AsyncClient') as mock_client:
|
||||
mock_client.return_value.__aenter__.return_value.request.return_value = mock_response
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(VultrAuthError) as exc_info:
|
||||
await server._make_request("GET", "/test")
|
||||
|
||||
assert "Vultr API error 401: Unauthorized" in str(exc_info.value)
|
||||
assert exc_info.value.status_code == 401
|
||||
assert "Invalid API key" in str(exc_info.value)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_make_request_error_500(self, mock_api_key):
|
||||
@ -108,10 +117,11 @@ class TestVultrDNSServer:
|
||||
with patch('httpx.AsyncClient') as mock_client:
|
||||
mock_client.return_value.__aenter__.return_value.request.return_value = mock_response
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(VultrAPIError) as exc_info:
|
||||
await server._make_request("GET", "/test")
|
||||
|
||||
assert "Vultr API error 500: Internal Server Error" in str(exc_info.value)
|
||||
assert exc_info.value.status_code == 500
|
||||
assert "Internal Server Error" in str(exc_info.value)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@ -448,11 +458,106 @@ class TestErrorScenarios:
|
||||
with patch('httpx.AsyncClient') as mock_client:
|
||||
mock_client.return_value.__aenter__.return_value.request.return_value = mock_response
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(VultrRateLimitError) as exc_info:
|
||||
await server._make_request("GET", "/domains")
|
||||
|
||||
assert exc_info.value.status_code == 429
|
||||
assert "Rate limit exceeded" in str(exc_info.value)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_not_found_error(self, mock_api_key):
|
||||
"""Test handling of 404 Not Found error."""
|
||||
server = VultrDNSServer(mock_api_key)
|
||||
|
||||
mock_response = AsyncMock()
|
||||
mock_response.status_code = 404
|
||||
mock_response.text = "Domain not found"
|
||||
|
||||
with patch('httpx.AsyncClient') as mock_client:
|
||||
mock_client.return_value.__aenter__.return_value.request.return_value = mock_response
|
||||
|
||||
with pytest.raises(VultrResourceNotFoundError) as exc_info:
|
||||
await server._make_request("GET", "/domains/nonexistent.com")
|
||||
|
||||
assert exc_info.value.status_code == 404
|
||||
assert "Resource not found" in str(exc_info.value)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forbidden_error(self, mock_api_key):
|
||||
"""Test handling of 403 Forbidden error."""
|
||||
server = VultrDNSServer(mock_api_key)
|
||||
|
||||
mock_response = AsyncMock()
|
||||
mock_response.status_code = 403
|
||||
mock_response.text = "Forbidden"
|
||||
|
||||
with patch('httpx.AsyncClient') as mock_client:
|
||||
mock_client.return_value.__aenter__.return_value.request.return_value = mock_response
|
||||
|
||||
with pytest.raises(VultrAuthError) as exc_info:
|
||||
await server._make_request("GET", "/domains")
|
||||
|
||||
assert exc_info.value.status_code == 403
|
||||
assert "Insufficient permissions" in str(exc_info.value)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validation_error_422(self, mock_api_key):
|
||||
"""Test handling of 422 Unprocessable Entity error."""
|
||||
server = VultrDNSServer(mock_api_key)
|
||||
|
||||
mock_response = AsyncMock()
|
||||
mock_response.status_code = 422
|
||||
mock_response.text = "Invalid domain format"
|
||||
|
||||
with patch('httpx.AsyncClient') as mock_client:
|
||||
mock_client.return_value.__aenter__.return_value.request.return_value = mock_response
|
||||
|
||||
with pytest.raises(VultrValidationError) as exc_info:
|
||||
await server._make_request("POST", "/domains")
|
||||
|
||||
assert exc_info.value.status_code == 422
|
||||
assert "Invalid domain format" in str(exc_info.value)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestExceptionProperties:
|
||||
"""Test custom exception properties and behavior."""
|
||||
|
||||
def test_vultr_api_error_properties(self):
|
||||
"""Test VultrAPIError has correct properties."""
|
||||
error = VultrAPIError(500, "Server Error")
|
||||
assert error.status_code == 500
|
||||
assert error.message == "Server Error"
|
||||
assert str(error) == "Vultr API error 500: Server Error"
|
||||
|
||||
def test_vultr_auth_error_inheritance(self):
|
||||
"""Test VultrAuthError inherits from VultrAPIError."""
|
||||
error = VultrAuthError(401, "Unauthorized")
|
||||
assert isinstance(error, VultrAPIError)
|
||||
assert error.status_code == 401
|
||||
assert error.message == "Unauthorized"
|
||||
|
||||
def test_vultr_rate_limit_error_inheritance(self):
|
||||
"""Test VultrRateLimitError inherits from VultrAPIError."""
|
||||
error = VultrRateLimitError(429, "Too Many Requests")
|
||||
assert isinstance(error, VultrAPIError)
|
||||
assert error.status_code == 429
|
||||
assert error.message == "Too Many Requests"
|
||||
|
||||
def test_vultr_not_found_error_inheritance(self):
|
||||
"""Test VultrResourceNotFoundError inherits from VultrAPIError."""
|
||||
error = VultrResourceNotFoundError(404, "Not Found")
|
||||
assert isinstance(error, VultrAPIError)
|
||||
assert error.status_code == 404
|
||||
assert error.message == "Not Found"
|
||||
|
||||
def test_vultr_validation_error_inheritance(self):
|
||||
"""Test VultrValidationError inherits from VultrAPIError."""
|
||||
error = VultrValidationError(400, "Bad Request")
|
||||
assert isinstance(error, VultrAPIError)
|
||||
assert error.status_code == 400
|
||||
assert error.message == "Bad Request"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__])
|
||||
|
Loading…
x
Reference in New Issue
Block a user