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.
|
# install all needed dependencies.
|
||||||
#Pipfile.lock
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# uv
|
||||||
|
uv.lock
|
||||||
|
|
||||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||||
__pypackages__/
|
__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! 🎉
|
370
README.md
370
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
|
```bash
|
||||||
# From your vultr-dns-mcp repository root:
|
# Using uv (recommended - fast and modern)
|
||||||
bash /home/rpm/claude/vultr-dns-mcp-fix/fix_tests.sh
|
uv add vultr-dns-mcp
|
||||||
|
|
||||||
|
# Or using pip
|
||||||
|
pip install vultr-dns-mcp
|
||||||
```
|
```
|
||||||
|
|
||||||
### Manual Fix (recommended):
|
### Basic Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Navigate to your repository
|
# Set your Vultr API key
|
||||||
cd /path/to/vultr-dns-mcp
|
export VULTR_API_KEY="your-api-key"
|
||||||
|
|
||||||
# 2. Backup current files
|
# List domains
|
||||||
cp tests/conftest.py tests/conftest.py.backup
|
vultr-dns-mcp domains list
|
||||||
cp tests/test_mcp_server.py tests/test_mcp_server.py.backup
|
|
||||||
|
|
||||||
# 3. Copy fixed files
|
# List DNS records
|
||||||
cp /home/rpm/claude/vultr-dns-mcp-fix/fixed_conftest.py tests/conftest.py
|
vultr-dns-mcp records list example.com
|
||||||
cp /home/rpm/claude/vultr-dns-mcp-fix/fixed_test_mcp_server.py tests/test_mcp_server.py
|
|
||||||
|
|
||||||
# 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]
|
pip install -e .[dev]
|
||||||
|
|
||||||
# 5. Run tests
|
# Run tests
|
||||||
pytest tests/ -v
|
pytest
|
||||||
|
|
||||||
|
# Run comprehensive test suite
|
||||||
|
python run_tests.py --all-checks
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔍 Problems Identified & Fixed
|
## MCP Tools Available
|
||||||
|
|
||||||
| Issue | Severity | Status | Fix Applied |
|
| Tool | Description |
|
||||||
|-------|----------|--------|------------|
|
|------|-------------|
|
||||||
| Import path problems | 🔴 Critical | ✅ Fixed | Updated all import statements |
|
| `list_dns_domains` | List all DNS domains |
|
||||||
| Async/await patterns | 🔴 Critical | ✅ Fixed | Fixed FastMCP Client usage |
|
| `get_dns_domain` | Get domain details |
|
||||||
| Mock configuration | 🟡 Medium | ✅ Fixed | Complete API response mocks |
|
| `create_dns_domain` | Create new domain |
|
||||||
| Test data structure | 🟡 Medium | ✅ Fixed | Updated fixtures to match API |
|
| `delete_dns_domain` | Delete domain and all records |
|
||||||
| Error handling gaps | 🟢 Low | ✅ Fixed | Added comprehensive error tests |
|
| `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
|
## CLI Commands
|
||||||
|
|
||||||
### Core Fixes
|
```bash
|
||||||
- **`fixed_conftest.py`** - Updated test configuration with proper mocks
|
# Domain management
|
||||||
- **`fixed_test_mcp_server.py`** - All MCP server tests with correct async patterns
|
vultr-dns-mcp domains list
|
||||||
- **`fix_tests.sh`** - Automated installer script
|
vultr-dns-mcp domains info example.com
|
||||||
|
vultr-dns-mcp domains create newdomain.com 192.168.1.100
|
||||||
|
|
||||||
### Documentation
|
# Record management
|
||||||
- **`FINAL_SOLUTION.md`** - Complete solution overview
|
vultr-dns-mcp records list example.com
|
||||||
- **`COMPLETE_FIX_GUIDE.md`** - Detailed fix documentation
|
vultr-dns-mcp records add example.com A www 192.168.1.100
|
||||||
|
vultr-dns-mcp records delete example.com record-id
|
||||||
|
|
||||||
### Utilities
|
# Setup utilities
|
||||||
- **`analyze_test_issues.py`** - Issue analysis script
|
vultr-dns-mcp setup-website example.com 192.168.1.100
|
||||||
- **`comprehensive_test_fix.py`** - Complete fix generator
|
vultr-dns-mcp setup-email example.com mail.example.com
|
||||||
- **`create_fixes.py`** - Simple fix creator
|
|
||||||
|
|
||||||
## 🚀 What Gets Fixed
|
# Start MCP server
|
||||||
|
vultr-dns-mcp server
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
This project follows FastMCP testing best practices with comprehensive test coverage:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests (uv)
|
||||||
|
uv run pytest
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# With coverage
|
||||||
|
uv run pytest --cov=vultr_dns_mcp --cov-report=html
|
||||||
|
|
||||||
|
# Full validation suite
|
||||||
|
uv run python run_tests.py --all-checks
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
The package provides specific exception types for better error handling:
|
||||||
|
|
||||||
### Before (Broken):
|
|
||||||
```python
|
```python
|
||||||
# Incorrect async pattern
|
from vultr_dns_mcp import (
|
||||||
async def test_tool(self, mcp_server):
|
VultrAPIError,
|
||||||
result = await client.call_tool("tool_name", {})
|
VultrAuthError,
|
||||||
# ❌ Missing proper async context
|
VultrRateLimitError,
|
||||||
# ❌ No mock configuration
|
VultrResourceNotFoundError,
|
||||||
# ❌ Incomplete error handling
|
VultrValidationError
|
||||||
|
)
|
||||||
|
|
||||||
|
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}")
|
||||||
```
|
```
|
||||||
|
|
||||||
### After (Fixed):
|
## Configuration
|
||||||
|
|
||||||
|
Set your Vultr API key via environment variable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export VULTR_API_KEY="your-vultr-api-key"
|
||||||
|
```
|
||||||
|
|
||||||
|
Or pass directly to the client:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@pytest.mark.asyncio
|
client = VultrDNSClient("your-api-key")
|
||||||
async def test_tool(self, mock_vultr_client):
|
server = create_mcp_server("your-api-key")
|
||||||
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
|
## License
|
||||||
|
|
||||||
After applying the fixes, you should see:
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
```bash
|
## Links
|
||||||
$ pytest tests/test_mcp_server.py -v
|
|
||||||
|
|
||||||
tests/test_mcp_server.py::TestMCPServerBasics::test_server_creation PASSED
|
- [GitHub Repository](https://github.com/rsp2k/vultr-dns-mcp)
|
||||||
tests/test_mcp_server.py::TestMCPTools::test_list_dns_domains_tool PASSED
|
- [PyPI Package](https://pypi.org/project/vultr-dns-mcp/)
|
||||||
tests/test_mcp_server.py::TestMCPTools::test_get_dns_domain_tool PASSED
|
- [Vultr API Documentation](https://www.vultr.com/api/)
|
||||||
tests/test_mcp_server.py::TestMCPTools::test_create_dns_domain_tool PASSED
|
- [Model Context Protocol](https://modelcontextprotocol.io/)
|
||||||
tests/test_mcp_server.py::TestMCPResources::test_domains_resource PASSED
|
- [uv Package Manager](https://docs.astral.sh/uv/)
|
||||||
tests/test_mcp_server.py::TestMCPIntegration::test_complete_domain_workflow PASSED
|
|
||||||
tests/test_mcp_server.py::TestValidationLogic::test_a_record_validation PASSED
|
|
||||||
|
|
||||||
========================== 25 passed in 2.34s ==========================
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 Key Technical Fixes
|
|
||||||
|
|
||||||
### 1. Fixed Async Patterns
|
|
||||||
- Proper `@pytest.mark.asyncio` usage
|
|
||||||
- Correct `async with Client(server) as client:` context managers
|
|
||||||
- Fixed await patterns throughout
|
|
||||||
|
|
||||||
### 2. Improved Mock Configuration
|
|
||||||
- Complete `AsyncMock` setup with proper specs
|
|
||||||
- All Vultr API methods properly mocked
|
|
||||||
- Realistic API response structures
|
|
||||||
|
|
||||||
### 3. Better Error Handling
|
|
||||||
- Comprehensive error scenario testing
|
|
||||||
- Graceful handling of API failures
|
|
||||||
- Proper exception testing patterns
|
|
||||||
|
|
||||||
### 4. Updated Dependencies
|
|
||||||
- Fixed pytest-asyncio configuration
|
|
||||||
- Proper FastMCP version requirements
|
|
||||||
- Added missing test dependencies
|
|
||||||
|
|
||||||
## 🆘 Troubleshooting
|
|
||||||
|
|
||||||
### If tests still fail:
|
|
||||||
|
|
||||||
1. **Check installation**:
|
|
||||||
```bash
|
|
||||||
pip list | grep -E "(pytest|fastmcp|httpx)"
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Verify imports**:
|
|
||||||
```bash
|
|
||||||
python -c "from vultr_dns_mcp.server import create_mcp_server"
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Run single test**:
|
|
||||||
```bash
|
|
||||||
pytest tests/test_mcp_server.py::TestMCPTools::test_list_dns_domains_tool -vvv
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Check pytest config**:
|
|
||||||
```bash
|
|
||||||
pytest --collect-only tests/
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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
|
|
||||||
|
|
||||||
## 📊 Success Metrics
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
## 🎉 Summary
|
|
||||||
|
|
||||||
This fix package addresses all the major issues in the vultr-dns-mcp test suite:
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
The fixed test suite follows FastMCP best practices and provides reliable, maintainable tests for the Vultr DNS MCP server functionality.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**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`.
|
|
||||||
|
|
||||||
**Need Help?** Check `FINAL_SOLUTION.md` for detailed instructions or `COMPLETE_FIX_GUIDE.md` for comprehensive documentation.
|
|
33
TESTING.md
33
TESTING.md
@ -104,32 +104,41 @@ def mock_vultr_client():
|
|||||||
|
|
||||||
## 🚀 Running Tests
|
## 🚀 Running Tests
|
||||||
|
|
||||||
### Using pytest directly:
|
### Using uv (recommended):
|
||||||
```bash
|
```bash
|
||||||
# All tests
|
# All tests
|
||||||
pytest
|
uv run pytest
|
||||||
|
|
||||||
# Specific categories
|
# Specific categories
|
||||||
pytest -m unit
|
uv run pytest -m unit
|
||||||
pytest -m integration
|
uv run pytest -m integration
|
||||||
pytest -m mcp
|
uv run pytest -m mcp
|
||||||
pytest -m "not slow"
|
uv run pytest -m "not slow"
|
||||||
|
|
||||||
# With coverage
|
# With coverage
|
||||||
pytest --cov=vultr_dns_mcp --cov-report=html
|
uv run pytest --cov=vultr_dns_mcp --cov-report=html
|
||||||
```
|
```
|
||||||
|
|
||||||
### Using the test runner:
|
### Using the test runner:
|
||||||
```bash
|
```bash
|
||||||
# Comprehensive test runner
|
# Comprehensive test runner (uv)
|
||||||
python run_tests.py
|
uv run python run_tests.py
|
||||||
|
|
||||||
# Specific test types
|
# Specific test types
|
||||||
python run_tests.py --type unit --verbose
|
uv run python run_tests.py --type unit --verbose
|
||||||
python run_tests.py --type mcp --coverage
|
uv run python run_tests.py --type mcp --coverage
|
||||||
python run_tests.py --fast # Skip slow tests
|
uv run python run_tests.py --fast # Skip slow tests
|
||||||
|
|
||||||
# Full validation
|
# 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
|
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,6 +10,34 @@ echo "🔧 Installing vultr-dns-mcp in development mode..."
|
|||||||
# Change to package directory
|
# Change to package directory
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
# 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
|
# Check if we're in a virtual environment
|
||||||
if [[ -z "$VIRTUAL_ENV" ]]; then
|
if [[ -z "$VIRTUAL_ENV" ]]; then
|
||||||
echo "⚠️ Warning: Not in a virtual environment"
|
echo "⚠️ Warning: Not in a virtual environment"
|
||||||
@ -31,8 +59,10 @@ echo " vultr-dns-mcp --help"
|
|||||||
echo " vultr-dns-mcp server"
|
echo " vultr-dns-mcp server"
|
||||||
echo ""
|
echo ""
|
||||||
echo "🧪 Run tests with:"
|
echo "🧪 Run tests with:"
|
||||||
echo " python test_fix.py"
|
|
||||||
echo " pytest"
|
echo " pytest"
|
||||||
|
echo " python run_tests.py --all-checks"
|
||||||
echo ""
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
echo "📝 Set your API key:"
|
echo "📝 Set your API key:"
|
||||||
echo " export VULTR_API_KEY='your-api-key-here'"
|
echo " export VULTR_API_KEY='your-api-key-here'"
|
||||||
|
@ -43,7 +43,7 @@ classifiers = [
|
|||||||
]
|
]
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"mcp>=1.0.0",
|
"fastmcp>=0.1.0",
|
||||||
"httpx>=0.24.0",
|
"httpx>=0.24.0",
|
||||||
"pydantic>=2.0.0",
|
"pydantic>=2.0.0",
|
||||||
"click>=8.0.0"
|
"click>=8.0.0"
|
||||||
@ -180,3 +180,16 @@ exclude_lines = [
|
|||||||
"if 0:",
|
"if 0:",
|
||||||
"if __name__ == .__main__.:"
|
"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
|
# Change to package directory
|
||||||
package_dir = Path(__file__).parent
|
package_dir = Path(__file__).parent
|
||||||
|
|
||||||
# Base pytest command
|
# Base pytest command using uv run
|
||||||
cmd = ["python", "-m", "pytest"]
|
cmd = ["uv", "run", "pytest"]
|
||||||
|
|
||||||
# Add verbosity
|
# Add verbosity
|
||||||
if verbose:
|
if verbose:
|
||||||
@ -73,7 +73,7 @@ def run_tests(test_type="all", verbose=False, coverage=False, fast=False):
|
|||||||
return result.returncode == 0
|
return result.returncode == 0
|
||||||
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
print("❌ Error: pytest not found. Install with: pip install pytest")
|
print("❌ Error: pytest not found. Install with: uv add pytest")
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Error running tests: {e}")
|
print(f"❌ Error running tests: {e}")
|
||||||
@ -86,10 +86,10 @@ def run_linting():
|
|||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
|
|
||||||
checks = [
|
checks = [
|
||||||
(["python", "-m", "black", "--check", "src", "tests"], "Black formatting"),
|
(["uv", "run", "black", "--check", "src", "tests"], "Black formatting"),
|
||||||
(["python", "-m", "isort", "--check", "src", "tests"], "Import sorting"),
|
(["uv", "run", "isort", "--check", "src", "tests"], "Import sorting"),
|
||||||
(["python", "-m", "flake8", "src", "tests"], "Flake8 linting"),
|
(["uv", "run", "flake8", "src", "tests"], "Flake8 linting"),
|
||||||
(["python", "-m", "mypy", "src"], "Type checking")
|
(["uv", "run", "mypy", "src"], "Type checking")
|
||||||
]
|
]
|
||||||
|
|
||||||
all_passed = True
|
all_passed = True
|
||||||
@ -209,9 +209,9 @@ def main():
|
|||||||
if success:
|
if success:
|
||||||
print("🎉 All checks passed!")
|
print("🎉 All checks passed!")
|
||||||
print("\n📚 Next steps:")
|
print("\n📚 Next steps:")
|
||||||
print(" • Run 'python -m build' to build the package")
|
print(" • Run 'uv build' to build the package")
|
||||||
print(" • Run 'python -m twine check dist/*' to validate")
|
print(" • Run 'uv run twine check dist/*' to validate")
|
||||||
print(" • Upload to PyPI with 'python -m twine upload dist/*'")
|
print(" • Upload to PyPI with 'uv run twine upload dist/*'")
|
||||||
else:
|
else:
|
||||||
print("❌ Some checks failed. Please fix the issues above.")
|
print("❌ Some checks failed. Please fix the issues above.")
|
||||||
sys.exit(1)
|
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
|
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 .client import VultrDNSClient
|
||||||
from ._version import __version__, __version_info__
|
from ._version import __version__, __version_info__
|
||||||
|
|
||||||
@ -32,6 +41,11 @@ __all__ = [
|
|||||||
"VultrDNSClient",
|
"VultrDNSClient",
|
||||||
"create_mcp_server",
|
"create_mcp_server",
|
||||||
"run_server",
|
"run_server",
|
||||||
|
"VultrAPIError",
|
||||||
|
"VultrAuthError",
|
||||||
|
"VultrRateLimitError",
|
||||||
|
"VultrResourceNotFoundError",
|
||||||
|
"VultrValidationError",
|
||||||
"__version__",
|
"__version__",
|
||||||
"__version_info__"
|
"__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.
|
for managing DNS records through the Vultr API.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import ipaddress
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
@ -16,6 +17,35 @@ from mcp.types import Resource, Tool, TextContent
|
|||||||
from pydantic import BaseModel
|
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:
|
class VultrDNSServer:
|
||||||
"""
|
"""
|
||||||
Vultr DNS API client for managing domains and DNS records.
|
Vultr DNS API client for managing domains and DNS records.
|
||||||
@ -48,7 +78,10 @@ class VultrDNSServer:
|
|||||||
"""Make an HTTP request to the Vultr API."""
|
"""Make an HTTP request to the Vultr API."""
|
||||||
url = f"{self.API_BASE}{endpoint}"
|
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(
|
response = await client.request(
|
||||||
method=method,
|
method=method,
|
||||||
url=url,
|
url=url,
|
||||||
@ -57,7 +90,19 @@ class VultrDNSServer:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if response.status_code not in [200, 201, 204]:
|
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:
|
if response.status_code == 204:
|
||||||
return {}
|
return {}
|
||||||
@ -592,15 +637,32 @@ def create_mcp_server(api_key: Optional[str] = None) -> Server:
|
|||||||
|
|
||||||
# Record-specific validation
|
# Record-specific validation
|
||||||
if record_type == 'A':
|
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]?)$'
|
try:
|
||||||
if not re.match(ipv4_pattern, data):
|
ipaddress.IPv4Address(data)
|
||||||
|
except ipaddress.AddressValueError:
|
||||||
validation_result["valid"] = False
|
validation_result["valid"] = False
|
||||||
validation_result["errors"].append("Invalid IPv4 address format")
|
validation_result["errors"].append("Invalid IPv4 address format")
|
||||||
|
|
||||||
elif record_type == 'AAAA':
|
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["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':
|
elif record_type == 'CNAME':
|
||||||
if name == '@' or name == '':
|
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
|
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__":
|
if __name__ == "__main__":
|
||||||
pytest.main([__file__])
|
pytest.main([__file__])
|
||||||
|
@ -3,7 +3,14 @@
|
|||||||
import pytest
|
import pytest
|
||||||
import httpx
|
import httpx
|
||||||
from unittest.mock import AsyncMock, patch
|
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
|
@pytest.mark.unit
|
||||||
@ -74,10 +81,11 @@ class TestVultrDNSServer:
|
|||||||
with patch('httpx.AsyncClient') as mock_client:
|
with patch('httpx.AsyncClient') as mock_client:
|
||||||
mock_client.return_value.__aenter__.return_value.request.return_value = mock_response
|
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")
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_make_request_error_401(self, mock_api_key):
|
async def test_make_request_error_401(self, mock_api_key):
|
||||||
@ -91,10 +99,11 @@ class TestVultrDNSServer:
|
|||||||
with patch('httpx.AsyncClient') as mock_client:
|
with patch('httpx.AsyncClient') as mock_client:
|
||||||
mock_client.return_value.__aenter__.return_value.request.return_value = mock_response
|
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")
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_make_request_error_500(self, mock_api_key):
|
async def test_make_request_error_500(self, mock_api_key):
|
||||||
@ -108,10 +117,11 @@ class TestVultrDNSServer:
|
|||||||
with patch('httpx.AsyncClient') as mock_client:
|
with patch('httpx.AsyncClient') as mock_client:
|
||||||
mock_client.return_value.__aenter__.return_value.request.return_value = mock_response
|
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")
|
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
|
@pytest.mark.unit
|
||||||
@ -448,11 +458,106 @@ class TestErrorScenarios:
|
|||||||
with patch('httpx.AsyncClient') as mock_client:
|
with patch('httpx.AsyncClient') as mock_client:
|
||||||
mock_client.return_value.__aenter__.return_value.request.return_value = mock_response
|
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")
|
await server._make_request("GET", "/domains")
|
||||||
|
|
||||||
|
assert exc_info.value.status_code == 429
|
||||||
assert "Rate limit exceeded" in str(exc_info.value)
|
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__":
|
if __name__ == "__main__":
|
||||||
pytest.main([__file__])
|
pytest.main([__file__])
|
||||||
|
Loading…
x
Reference in New Issue
Block a user