Compare commits

..

10 Commits

Author SHA1 Message Date
e6f66dc931 Refactor package name from vultr-dns-mcp to mcp-vultr
Some checks are pending
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
Tests / test (3.13) (push) Waiting to run
Tests / build (push) Blocked by required conditions
Tests / test-install (3.10) (push) Blocked by required conditions
Tests / test-install (3.13) (push) Blocked by required conditions
Tests / security (push) Waiting to run
- Rename package from vultr-dns-mcp to mcp-vultr for MCP organization
- Update module name from vultr_dns_mcp to mcp_vultr throughout codebase
- Rename src/vultr_dns_mcp/ to src/mcp_vultr/
- Update all import statements and references in Python files
- Update documentation files (README.md, CLAUDE.md, etc.)
- Update CLI script names in pyproject.toml
- Update test files with new import paths

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-16 21:49:38 -06:00
2d866d275d Add zone file import/export functionality (v1.1.0)
Major new feature: DNS Zone File Management
- Add export_zone_file() method to export domain records as standard zone files
- Add import_zone_file() method to import records from zone file format
- Add comprehensive zone file parser with $TTL and $ORIGIN support
- Add dry-run mode for import validation without making changes
- Add zone file tools to FastMCP server (export_zone_file_tool, import_zone_file_tool)
- Add dns://domains/{domain}/zone-file resource for MCP clients

Features:
- Standard zone file format compliance (BIND, PowerDNS compatible)
- Support for all DNS record types (A, AAAA, CNAME, MX, TXT, NS, SRV)
- Proper handling of quoted strings and record priorities
- Line-by-line error reporting for invalid zone data
- Backup and migration capabilities

This enables easy DNS configuration backup, restoration, and bulk operations.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-16 19:29:06 -06:00
509423e650 Fix tool wrappers callable error (v1.0.4)
- Fix tool wrappers to call vultr_client methods directly instead of resource functions
- Resolve "FunctionResource object is not callable" error in Claude Desktop
- Tool wrappers now properly call vultr_client.list_domains(), vultr_client.get_domain(), etc.
- Maintains hybrid approach while fixing the execution error
- Bump version to 1.0.4

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-16 15:49:04 -06:00
ce640e2ee5 Add tool wrappers for Claude Desktop compatibility (v1.0.3)
- Add tool wrappers for all resource endpoints to ensure Claude Desktop can access them
- Implement hybrid approach: resources for MCP spec compliance, tools for practical usage
- Add 5 new tool wrappers: list_domains_tool, get_domain_tool, list_records_tool, get_record_tool, analyze_domain_tool
- Update documentation to reflect the hybrid approach
- Bump version to 1.0.3

This ensures compatibility with Claude Desktop while maintaining MCP best practices.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-16 14:51:04 -06:00
691963a43b Refactor to use MCP resources for read operations (v1.0.2)
- Convert all read/list operations from tools to resources following MCP best practices
- Add @mcp.resource decorators for domains, records, and analysis endpoints
- Update version to 1.0.2
- Add uvx support documentation for Claude Desktop integration
- Fix CLI asyncio usage for FastMCP synchronous run() method
- Add vultr-mcp-server console script entry point

This improves alignment with MCP patterns where resources represent
readable data and tools perform actions that modify state.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-16 14:28:21 -06:00
66c4c3cb18 Update CLAUDE.md with FastMCP migration details
- Document migration from low-level MCP to FastMCP 2.0
- Update project structure with new files
- Add FastMCP server features and error handling improvements
- Update version history with all recent changes
- Add Claude Desktop integration section
- Update troubleshooting for common FastMCP issues

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-16 11:28:13 -06:00
75ffe33008 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>
2025-07-16 10:09:20 -06:00
7273fe8539 fixed 2025-06-11 18:14:19 -06:00
Ryan Malloy
5df5e9e8a0 Update CLI to use async run_server function 2025-06-11 17:55:21 -06:00
Ryan Malloy
b8fd6e4632 Migrate from fastmcp to official MCP package 2025-06-11 17:53:43 -06:00
34 changed files with 2966 additions and 975 deletions

3
.gitignore vendored
View File

@ -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__/

View File

@ -1,6 +1,6 @@
# Building and Publishing to PyPI # Building and Publishing to PyPI
This document provides instructions for building and publishing the `vultr-dns-mcp` package to PyPI. This document provides instructions for building and publishing the `mcp-vultr` package to PyPI.
## Prerequisites ## Prerequisites
@ -43,8 +43,8 @@ This document provides instructions for building and publishing the `vultr-dns-m
``` ```
This creates: This creates:
- `dist/vultr_dns_mcp-1.0.0-py3-none-any.whl` (wheel) - `dist/mcp_vultr-1.0.0-py3-none-any.whl` (wheel)
- `dist/vultr-dns-mcp-1.0.0.tar.gz` (source distribution) - `dist/mcp-vultr-1.0.0.tar.gz` (source distribution)
3. **Verify the build:** 3. **Verify the build:**
```bash ```bash
@ -60,13 +60,13 @@ This document provides instructions for building and publishing the `vultr-dns-m
2. **Test installation from TestPyPI:** 2. **Test installation from TestPyPI:**
```bash ```bash
pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple vultr-dns-mcp pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple mcp-vultr
``` ```
3. **Test functionality:** 3. **Test functionality:**
```bash ```bash
vultr-dns-mcp --help mcp-vultr --help
python -c "from vultr_dns_mcp import VultrDNSClient; print('Import successful')" python -c "from mcp_vultr import VultrDNSClient; print('Import successful')"
``` ```
## Publishing to PyPI ## Publishing to PyPI
@ -77,8 +77,8 @@ This document provides instructions for building and publishing the `vultr-dns-m
``` ```
2. **Verify publication:** 2. **Verify publication:**
- Check the package page: https://pypi.org/project/vultr-dns-mcp/ - Check the package page: https://pypi.org/project/mcp-vultr/
- Test installation: `pip install vultr-dns-mcp` - Test installation: `pip install mcp-vultr`
## Version Management ## Version Management

View File

@ -5,6 +5,69 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.1.0] - 2025-01-16
### Added
- **Zone File Import/Export** - Major new feature for DNS backup and migration
- `export_zone_file_tool(domain)` - Export domain records as standard DNS zone file format
- `import_zone_file_tool(domain, zone_data, dry_run)` - Import DNS records from zone file format
- `dns://domains/{domain}/zone-file` resource for zone file access
- Support for all standard DNS record types (A, AAAA, CNAME, MX, TXT, NS, SRV)
- Comprehensive zone file parsing with proper handling of $TTL and $ORIGIN directives
- Dry-run mode for import validation without making changes
- Standard zone file format compliance for interoperability
### Features
- **Backup & Migration**: Easy DNS configuration backup and restoration
- **Bulk Operations**: Import multiple records at once from zone files
- **Validation**: Pre-import validation with detailed error reporting
- **Compatibility**: Standard zone file format works with BIND, PowerDNS, and other DNS servers
### Technical
- Added comprehensive zone file parsing engine with quoted string handling
- Proper record type detection and formatting
- Error handling with line-by-line validation feedback
- Support for both tool and resource access patterns
## [1.0.4] - 2025-01-16
### Fixed
- Fixed tool wrappers to properly call underlying VultrDNSServer methods instead of trying to call FunctionResource objects
- Resolved "FunctionResource object is not callable" error in Claude Desktop
- Tool wrappers now directly call `vultr_client.list_domains()`, `vultr_client.get_domain()`, etc.
### Technical
- Changed tool wrapper implementation from calling resource functions to calling the underlying client methods
- Maintains functionality while fixing the callable object error
## [1.0.3] - 2025-01-16
### Added
- Tool wrappers for resource access to ensure Claude Desktop compatibility
- `list_domains_tool()` - wrapper for dns://domains resource
- `get_domain_tool()` - wrapper for dns://domains/{domain} resource
- `list_records_tool()` - wrapper for dns://domains/{domain}/records resource
- `get_record_tool()` - wrapper for dns://domains/{domain}/records/{record_id} resource
- `analyze_domain_tool()` - wrapper for dns://domains/{domain}/analysis resource
### Technical
- Hybrid approach: resources for direct MCP access, tools for Claude Desktop compatibility
- Maintains both patterns to support different MCP client implementations
## [1.0.2] - 2025-01-16
### Changed
- Refactored read operations to use MCP resources instead of tools
- List domains endpoint: `@mcp.resource("dns://domains")`
- Get domain endpoint: `@mcp.resource("dns://domains/{domain}")`
- List records endpoint: `@mcp.resource("dns://domains/{domain}/records")`
- Get record endpoint: `@mcp.resource("dns://domains/{domain}/records/{record_id}")`
- Analyze domain endpoint: `@mcp.resource("dns://domains/{domain}/analysis")`
### Improved
- Better alignment with MCP best practices (resources for read, tools for write)
- Enhanced Claude Desktop integration documentation with uvx support
## [1.0.1] - 2024-12-20 ## [1.0.1] - 2024-12-20
### Fixed ### Fixed

290
CLAUDE.md Normal file
View File

@ -0,0 +1,290 @@
# 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 mcp-vultr
# Or using pip
pip install mcp-vultr
```
### Basic Usage
```bash
# CLI - requires VULTR_API_KEY environment variable
mcp-vultr domains list
mcp-vultr records list example.com
# As MCP server
vultr-mcp-server
# or using Python module
python -m mcp_vultr
```
## 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=mcp_vultr --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
```
mcp-vultr/
├── src/mcp_vultr/
│ ├── __init__.py # Package initialization with exports
│ ├── __main__.py # Module entry point for python -m
│ ├── client.py # High-level DNS client
│ ├── server.py # Core Vultr API client with exceptions
│ ├── fastmcp_server.py # FastMCP server implementation
│ └── cli.py # Command-line interface
├── tests/ # Comprehensive test suite
├── CLAUDE.md # This documentation file
├── CLAUDE_DESKTOP_SETUP.md # Claude Desktop integration guide
├── README.md # Professional project documentation
└── pyproject.toml # Project configuration with FastMCP
```
## Key Features
### FastMCP Server Implementation
- Built on FastMCP 2.0 framework for better Claude Desktop compatibility
- All tools use proper async/await patterns
- 12 comprehensive DNS management tools
- **Important**: FastMCP's `run()` method is synchronous, not async. Do not wrap with `asyncio.run()`
### MCP Tools (19 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
- Setup utilities: Quick website and email DNS configuration
- **Zone File Management**: Import/export DNS records in standard zone file format
- Resource access tools: Tool wrappers for Claude Desktop compatibility
### Enhanced Error Handling
- Custom exception hierarchy: VultrAPIError, VultrAuthError, VultrRateLimitError, etc.
- HTTP timeout configuration (30s total, 10s connect)
- Specific error types based on HTTP status codes
### Modern Development Features
- Full uv package manager integration
- IPv6 validation using Python's ipaddress module
- Comprehensive test coverage with improvement validation
### 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
- **MAJOR**: Migrated from low-level MCP to FastMCP 2.0 framework
- **FEATURE**: Added custom exception hierarchy for better error handling
- **FEATURE**: Replaced basic IPv6 validation with Python's ipaddress module
- **FEATURE**: Added HTTP request timeouts (30s total, 10s connect)
- **FEATURE**: Full uv package manager integration throughout project
- **FEATURE**: Created comprehensive Claude Desktop setup documentation
- **FIX**: Resolved event loop issues - FastMCP 2.0 uses synchronous `run()` method
- **FEATURE**: Added `vultr-mcp-server` console script entry point for easier Claude Desktop integration
- **IMPROVEMENT**: Enhanced README with professional structure and badges
- **IMPROVEMENT**: Added test suite for validating all improvements
### 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 `uv sync` or `pip install -e .` from repository root
- **AsyncioError**: FastMCP handles async properly, ensure tools use `async def`
- **Event Loop Error**: FastMCP 2.0's `run()` method is synchronous - do NOT use `asyncio.run()`
- **MCP Connection**: Ensure Claude Desktop config uses absolute Python path
- **API Errors**: Verify VULTR_API_KEY environment variable is set
### 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 mcp_vultr.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 mcp_vultr.server import create_mcp_server"
```
## Claude Desktop Integration
### Quick Setup
1. Install the package: `pip install mcp-vultr`
2. Add to Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
```json
{
"mcpServers": {
"vultr-dns": {
"command": "vultr-mcp-server",
"args": [],
"env": {
"VULTR_API_KEY": "your-api-key"
}
}
}
}
```
3. Restart Claude Desktop
For detailed setup instructions, see `CLAUDE_DESKTOP_SETUP.md`.
## Support & Documentation
- **GitHub**: https://github.com/rsp2k/mcp-vultr
- **PyPI**: https://pypi.org/project/mcp-vultr/
- **Documentation**: Complete API documentation and examples in package
- **Issues**: Use GitHub Issues for bug reports and feature requests

308
CLAUDE_DESKTOP_SETUP.md Normal file
View File

@ -0,0 +1,308 @@
# 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 mcp-vultr
```
### Option 2: Install from Local Development
```bash
# From this project directory
pip install -e .
```
### Option 3: Using uv (Fastest)
```bash
uv add mcp-vultr
```
## 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": "vultr-mcp-server",
"args": [],
"env": {
"VULTR_API_KEY": "YOUR_VULTR_API_KEY_HERE"
}
}
}
}
```
**Note**: If `vultr-mcp-server` is not in your PATH, use the full Python module approach:
```json
{
"mcpServers": {
"vultr-dns": {
"command": "python",
"args": ["-m", "mcp_vultr"],
"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", "vultr-mcp-server"],
"env": {
"VULTR_API_KEY": "YOUR_VULTR_API_KEY_HERE"
}
}
}
}
```
### 3a. Using uvx (Recommended for Easy Installation)
This approach uses `uvx` to automatically install and run the package without needing to manage Python environments:
```json
{
"mcpServers": {
"vultr-dns": {
"command": "uvx",
"args": [
"--from", "mcp-vultr",
"vultr-mcp-server"
],
"env": {
"VULTR_API_KEY": "YOUR_VULTR_API_KEY_HERE"
}
}
}
}
```
For TestPyPI version:
```json
{
"mcpServers": {
"vultr-dns": {
"command": "uvx",
"args": [
"--index-url", "https://test.pypi.org/simple/",
"--extra-index-url", "https://pypi.org/simple/",
"--index-strategy", "unsafe-best-match",
"--from", "mcp-vultr==1.0.1",
"vultr-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", "mcp_vultr"],
"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 mcp_vultr; print('✅ Package installed')"
```
2. **Test server manually**:
```bash
export VULTR_API_KEY="your-key"
vultr-mcp-server
# or
python -m mcp_vultr
```
3. **Check API key**:
```bash
export VULTR_API_KEY="your-key"
python -c "
from mcp_vultr.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", "mcp_vultr"],
"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/vultr-mcp-server",
"args": [],
"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! 🎉

View File

@ -1,6 +1,6 @@
# PyPI Publishing Setup Guide # PyPI Publishing Setup Guide
This guide explains how to set up and use the automated PyPI publishing workflow for the `vultr-dns-mcp` package. This guide explains how to set up and use the automated PyPI publishing workflow for the `mcp-vultr` package.
## 🔐 Setting Up Trusted Publishing (Recommended) ## 🔐 Setting Up Trusted Publishing (Recommended)
@ -8,29 +8,29 @@ The workflow uses [PyPI's trusted publishing](https://docs.pypi.org/trusted-publ
### For PyPI (Production) ### For PyPI (Production)
1. **Go to your project on PyPI**: https://pypi.org/manage/project/vultr-dns-mcp/ 1. **Go to your project on PyPI**: https://pypi.org/manage/project/mcp-vultr/
2. **Navigate to "Publishing"** tab 2. **Navigate to "Publishing"** tab
3. **Add a new trusted publisher** with these settings: 3. **Add a new trusted publisher** with these settings:
- **PyPI Project Name**: `vultr-dns-mcp` - **PyPI Project Name**: `mcp-vultr`
- **Owner**: `rsp2k` - **Owner**: `rsp2k`
- **Repository name**: `vultr-dns-mcp` - **Repository name**: `mcp-vultr`
- **Workflow filename**: `publish.yml` - **Workflow filename**: `publish.yml`
- **Environment name**: `pypi` - **Environment name**: `pypi`
### For TestPyPI (Testing) ### For TestPyPI (Testing)
1. **Go to TestPyPI**: https://test.pypi.org/manage/project/vultr-dns-mcp/ 1. **Go to TestPyPI**: https://test.pypi.org/manage/project/mcp-vultr/
2. **Navigate to "Publishing"** tab 2. **Navigate to "Publishing"** tab
3. **Add a new trusted publisher** with these settings: 3. **Add a new trusted publisher** with these settings:
- **PyPI Project Name**: `vultr-dns-mcp` - **PyPI Project Name**: `mcp-vultr`
- **Owner**: `rsp2k` - **Owner**: `rsp2k`
- **Repository name**: `vultr-dns-mcp` - **Repository name**: `mcp-vultr`
- **Workflow filename**: `publish.yml` - **Workflow filename**: `publish.yml`
- **Environment name**: `testpypi` - **Environment name**: `testpypi`
### GitHub Environment Setup ### GitHub Environment Setup
1. **Go to your repository settings**: https://github.com/rsp2k/vultr-dns-mcp/settings/environments 1. **Go to your repository settings**: https://github.com/rsp2k/mcp-vultr/settings/environments
2. **Create two environments**: 2. **Create two environments**:
- `pypi` (for production releases) - `pypi` (for production releases)
- `testpypi` (for testing) - `testpypi` (for testing)
@ -43,7 +43,7 @@ The workflow uses [PyPI's trusted publishing](https://docs.pypi.org/trusted-publ
1. **Update the version** in `pyproject.toml`: 1. **Update the version** in `pyproject.toml`:
```toml ```toml
[project] [project]
name = "vultr-dns-mcp" name = "mcp-vultr"
version = "1.0.2" # Increment this version = "1.0.2" # Increment this
``` ```
@ -185,9 +185,9 @@ python -m twine check dist/*
## 📊 Monitoring ## 📊 Monitoring
After publishing, monitor: After publishing, monitor:
- **PyPI downloads**: https://pypistats.org/packages/vultr-dns-mcp - **PyPI downloads**: https://pypistats.org/packages/mcp-vultr
- **GitHub releases**: https://github.com/rsp2k/vultr-dns-mcp/releases - **GitHub releases**: https://github.com/rsp2k/mcp-vultr/releases
- **Actions logs**: https://github.com/rsp2k/vultr-dns-mcp/actions - **Actions logs**: https://github.com/rsp2k/mcp-vultr/actions
## 🎯 Next Steps ## 🎯 Next Steps

346
README.md
View File

@ -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: [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![MCP Compatible](https://img.shields.io/badge/MCP-Compatible-green.svg)](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 mcp-vultr
# Or using pip
pip install mcp-vultr
``` ```
### 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 mcp-vultr 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 mcp-vultr 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
mcp-vultr setup-website example.com 192.168.1.100
# Run as MCP server
uv run python -m mcp_vultr.server
```
### Python API
```python
import asyncio
from mcp_vultr 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 mcp_vultr 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/mcp-vultr.git
cd mcp-vultr
# 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
- **`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:
```bash ```bash
$ pytest tests/test_mcp_server.py -v # Domain management
mcp-vultr domains list
mcp-vultr domains info example.com
mcp-vultr domains create newdomain.com 192.168.1.100
tests/test_mcp_server.py::TestMCPServerBasics::test_server_creation PASSED # Record management
tests/test_mcp_server.py::TestMCPTools::test_list_dns_domains_tool PASSED mcp-vultr records list example.com
tests/test_mcp_server.py::TestMCPTools::test_get_dns_domain_tool PASSED mcp-vultr records add example.com A www 192.168.1.100
tests/test_mcp_server.py::TestMCPTools::test_create_dns_domain_tool PASSED mcp-vultr records delete example.com record-id
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
========================== 25 passed in 2.34s ========================== # Setup utilities
mcp-vultr setup-website example.com 192.168.1.100
mcp-vultr setup-email example.com mail.example.com
# Start MCP server
mcp-vultr server
``` ```
## 🔧 Key Technical Fixes ## Testing
### 1. Fixed Async Patterns This project follows FastMCP testing best practices with comprehensive test coverage:
- Proper `@pytest.mark.asyncio` usage
- Correct `async with Client(server) as client:` context managers
- Fixed await patterns throughout
### 2. Improved Mock Configuration ```bash
- Complete `AsyncMock` setup with proper specs # Run all tests (uv)
- All Vultr API methods properly mocked uv run pytest
- Realistic API response structures
### 3. Better Error Handling # Run specific test categories
- Comprehensive error scenario testing uv run pytest -m unit # Unit tests
- Graceful handling of API failures uv run pytest -m integration # Integration tests
- Proper exception testing patterns uv run pytest -m mcp # MCP-specific tests
### 4. Updated Dependencies # With coverage
- Fixed pytest-asyncio configuration uv run pytest --cov=mcp_vultr --cov-report=html
- Proper FastMCP version requirements
- Added missing test dependencies
## 🆘 Troubleshooting # Full validation suite
uv run python run_tests.py --all-checks
```
### If tests still fail: ## Contributing
1. **Check installation**: 1. Fork the repository
```bash 2. Create a feature branch (`git checkout -b feature/amazing-feature`)
pip list | grep -E "(pytest|fastmcp|httpx)" 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**: ## Error Handling
```bash
python -c "from vultr_dns_mcp.server import create_mcp_server"
```
3. **Run single test**: The package provides specific exception types for better error handling:
```bash
pytest tests/test_mcp_server.py::TestMCPTools::test_list_dns_domains_tool -vvv
```
4. **Check pytest config**: ```python
```bash from mcp_vultr import (
pytest --collect-only tests/ VultrAPIError,
``` VultrAuthError,
VultrRateLimitError,
VultrResourceNotFoundError,
VultrValidationError
)
### Common Issues: try:
- **ImportError**: Run `pip install -e .` from repository root await client.get_domain("example.com")
- **AsyncioError**: Ensure `asyncio_mode = "auto"` in pyproject.toml except VultrAuthError:
- **MockError**: Check that fixed_conftest.py was properly copied 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: Set your Vultr API key via environment variable:
- ✅ 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 ```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 ```python
2. **Provides comprehensive mock configuration** matching the Vultr API client = VultrDNSClient("your-api-key")
3. **Adds proper error handling tests** for robustness server = create_mcp_server("your-api-key")
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. ## 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/mcp-vultr)
- [PyPI Package](https://pypi.org/project/mcp-vultr/)
- [Vultr API Documentation](https://www.vultr.com/api/)
- [Model Context Protocol](https://modelcontextprotocol.io/)
- [uv Package Manager](https://docs.astral.sh/uv/)

View File

@ -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=mcp_vultr --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
``` ```
@ -154,7 +163,7 @@ testpaths = ["tests"]
addopts = [ addopts = [
"--strict-markers", "--strict-markers",
"--verbose", "--verbose",
"--cov=vultr_dns_mcp", "--cov=mcp_vultr",
"--cov-fail-under=80" "--cov-fail-under=80"
] ]
asyncio_mode = "auto" asyncio_mode = "auto"

53
debug_server.py Normal file
View 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())

View File

@ -11,7 +11,7 @@ This script demonstrates various ways to use the package:
import asyncio import asyncio
import os import os
from vultr_dns_mcp import VultrDNSClient, VultrDNSServer, create_mcp_server from mcp_vultr import VultrDNSClient, VultrDNSServer, create_mcp_server
async def client_example(): async def client_example():
@ -82,7 +82,7 @@ async def validation_example():
print("=" * 40) print("=" * 40)
# Import the validation from the server module # Import the validation from the server module
from vultr_dns_mcp.server import create_mcp_server from mcp_vultr.server import create_mcp_server
# Create a test server instance for validation (won't make API calls) # Create a test server instance for validation (won't make API calls)
try: try:

View File

@ -10,29 +10,59 @@ echo "🔧 Installing vultr-dns-mcp in development mode..."
# Change to package directory # Change to package directory
cd "$(dirname "$0")" cd "$(dirname "$0")"
# Check if we're in a virtual environment # Check for uv first, fall back to pip
if [[ -z "$VIRTUAL_ENV" ]]; then if command -v uv &> /dev/null; then
echo "⚠️ Warning: Not in a virtual environment" echo "📦 Using uv for fast, modern dependency management..."
echo " Consider running: python -m venv .venv && source .venv/bin/activate"
# 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 "" echo ""
fi 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 "📝 Set your API key:"
echo " export VULTR_API_KEY='your-api-key-here'" echo " export VULTR_API_KEY='your-api-key-here'"

View File

@ -3,8 +3,8 @@ requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project] [project]
name = "vultr-dns-mcp" name = "mcp-vultr"
version = "1.0.1" version = "1.1.0"
description = "A comprehensive Model Context Protocol (MCP) server for managing Vultr DNS records" description = "A comprehensive Model Context Protocol (MCP) server for managing Vultr DNS records"
readme = "README.md" readme = "README.md"
license = {text = "MIT"} license = {text = "MIT"}
@ -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"
@ -80,14 +80,15 @@ Repository = "https://github.com/rsp2k/vultr-dns-mcp.git"
Changelog = "https://github.com/rsp2k/vultr-dns-mcp/blob/main/CHANGELOG.md" Changelog = "https://github.com/rsp2k/vultr-dns-mcp/blob/main/CHANGELOG.md"
[project.scripts] [project.scripts]
vultr-dns-mcp = "vultr_dns_mcp.cli:main" mcp-vultr = "mcp_vultr.cli:main"
vultr-dns-server = "vultr_dns_mcp.cli:server_command" vultr-dns-server = "mcp_vultr.cli:server_command"
vultr-mcp-server = "mcp_vultr.fastmcp_server:run_server"
[tool.setuptools.packages.find] [tool.setuptools.packages.find]
where = ["src"] where = ["src"]
[tool.setuptools.package-data] [tool.setuptools.package-data]
vultr_dns_mcp = ["py.typed"] mcp_vultr = ["py.typed"]
[tool.black] [tool.black]
line-length = 88 line-length = 88
@ -111,7 +112,7 @@ extend-exclude = '''
profile = "black" profile = "black"
multi_line_output = 3 multi_line_output = 3
line_length = 88 line_length = 88
known_first_party = ["vultr_dns_mcp"] known_first_party = ["mcp_vultr"]
[tool.mypy] [tool.mypy]
python_version = "3.10" python_version = "3.10"
@ -162,7 +163,7 @@ filterwarnings = [
] ]
[tool.coverage.run] [tool.coverage.run]
source = ["src/vultr_dns_mcp"] source = ["src/mcp_vultr"]
omit = [ omit = [
"*/tests/*", "*/tests/*",
"*/test_*", "*/test_*",
@ -180,3 +181,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"
]

View File

@ -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:
@ -29,7 +29,7 @@ def run_tests(test_type="all", verbose=False, coverage=False, fast=False):
# Add coverage if requested # Add coverage if requested
if coverage: if coverage:
cmd.extend(["--cov=vultr_dns_mcp", "--cov-report=term-missing", "--cov-report=html"]) cmd.extend(["--cov=mcp_vultr", "--cov-report=term-missing", "--cov-report=html"])
# Select tests based on type # Select tests based on type
if test_type == "unit": if test_type == "unit":
@ -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
@ -130,8 +130,8 @@ def run_package_validation():
sys.path.insert(0, str(src_path)) sys.path.insert(0, str(src_path))
# Test main imports # Test main imports
from vultr_dns_mcp import VultrDNSClient, VultrDNSServer, create_mcp_server from mcp_vultr import VultrDNSClient, VultrDNSServer, create_mcp_server
from vultr_dns_mcp._version import __version__ from mcp_vultr._version import __version__
print(f" ✅ Package imports successful (version {__version__})") print(f" ✅ Package imports successful (version {__version__})")
@ -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
View 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()

View File

@ -6,7 +6,7 @@ the Vultr API. It includes tools for domain management, DNS record operations,
configuration analysis, and validation. configuration analysis, and validation.
Example usage: Example usage:
from vultr_dns_mcp import VultrDNSServer, create_mcp_server from mcp_vultr import VultrDNSServer, create_mcp_server
# Create a server instance # Create a server instance
server = VultrDNSServer(api_key="your-api-key") server = VultrDNSServer(api_key="your-api-key")
@ -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/mcp_vultr/__main__.py Normal file
View 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)

View File

@ -1,4 +1,4 @@
"""Version information for vultr-dns-mcp package.""" """Version information for vultr-dns-mcp package."""
__version__ = "1.0.1" __version__ = "1.1.0"
__version_info__ = tuple(int(i) for i in __version__.split(".") if i.isdigit()) __version_info__ = tuple(int(i) for i in __version__.split(".") if i.isdigit())

View File

@ -32,19 +32,8 @@ def cli(ctx: click.Context, api_key: Optional[str]):
@cli.command() @cli.command()
@click.option(
"--host",
default="localhost",
help="Host to bind the server to"
)
@click.option(
"--port",
default=8000,
type=int,
help="Port to bind the server to"
)
@click.pass_context @click.pass_context
def server(ctx: click.Context, host: str, port: int): def server(ctx: click.Context):
"""Start the Vultr DNS MCP server.""" """Start the Vultr DNS MCP server."""
api_key = ctx.obj.get('api_key') api_key = ctx.obj.get('api_key')
@ -54,7 +43,7 @@ def server(ctx: click.Context, host: str, port: int):
sys.exit(1) sys.exit(1)
click.echo(f"🚀 Starting Vultr DNS MCP Server...") click.echo(f"🚀 Starting Vultr DNS MCP Server...")
click.echo(f"📡 API Key: {api_key[:8]}...") click.echo(f"🔑 API Key: {api_key[:8]}...")
click.echo(f"🔄 Press Ctrl+C to stop") click.echo(f"🔄 Press Ctrl+C to stop")
try: try:
@ -211,7 +200,7 @@ def list_records(ctx: click.Context, domain: str, record_type: Optional[str]):
data = record.get('data', 'Unknown') data = record.get('data', 'Unknown')
ttl = record.get('ttl', 'Unknown') ttl = record.get('ttl', 'Unknown')
click.echo(f" • [{record_id}] {r_type:6} {name:20} {data} (TTL: {ttl})") click.echo(f" • [{record_id}] {r_type:6} {name:20} {data} (TTL: {ttl})")
except Exception as e: except Exception as e:
click.echo(f"Error: {e}", err=True) click.echo(f"Error: {e}", err=True)
@ -253,7 +242,7 @@ def add_record(
sys.exit(1) sys.exit(1)
record_id = result.get('id', 'Unknown') record_id = result.get('id', 'Unknown')
click.echo(f"✅ Created {record_type} record [{record_id}]: {name} {value}") click.echo(f"✅ Created {record_type} record [{record_id}]: {name} {value}")
except Exception as e: except Exception as e:
click.echo(f"Error: {e}", err=True) click.echo(f"Error: {e}", err=True)

View File

@ -0,0 +1,297 @@
"""
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.resource("dns://domains")
async def list_dns_domains() -> List[Dict[str, Any]]:
"""List all DNS domains in your Vultr account."""
return await vultr_client.list_domains()
@mcp.resource("dns://domains/{domain}")
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.resource("dns://domains/{domain}/records")
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.resource("dns://domains/{domain}/records/{record_id}")
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.resource("dns://domains/{domain}/analysis")
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
@mcp.resource("dns://domains/{domain}/zone-file")
async def export_zone_file_resource(domain: str) -> str:
"""Export domain records as standard DNS zone file format.
Args:
domain: The domain name to export
"""
return await vultr_client.export_zone_file(domain)
# Tool wrappers for resources (for compatibility with Claude Desktop)
@mcp.tool
async def list_domains_tool() -> List[Dict[str, Any]]:
"""List all DNS domains in your Vultr account.
This is a tool wrapper for the dns://domains resource.
"""
return await vultr_client.list_domains()
@mcp.tool
async def get_domain_tool(domain: str) -> Dict[str, Any]:
"""Get details for a specific DNS domain.
Args:
domain: The domain name to get details for
This is a tool wrapper for the dns://domains/{domain} resource.
"""
return await vultr_client.get_domain(domain)
@mcp.tool
async def list_records_tool(domain: str) -> List[Dict[str, Any]]:
"""List all DNS records for a domain.
Args:
domain: The domain name to list records for
This is a tool wrapper for the dns://domains/{domain}/records resource.
"""
return await vultr_client.list_records(domain)
@mcp.tool
async def get_record_tool(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
This is a tool wrapper for the dns://domains/{domain}/records/{record_id} resource.
"""
return await vultr_client.get_record(domain, record_id)
@mcp.tool
async def analyze_domain_tool(domain: str) -> Dict[str, Any]:
"""Analyze DNS configuration for a domain and provide recommendations.
Args:
domain: The domain name to analyze
This is a tool wrapper for the dns://domains/{domain}/analysis resource.
"""
return await vultr_client.analyze_records(domain)
@mcp.tool
async def export_zone_file_tool(domain: str) -> str:
"""Export domain records as standard DNS zone file format.
Args:
domain: The domain name to export
Returns:
DNS zone file content as string
"""
return await vultr_client.export_zone_file(domain)
@mcp.tool
async def import_zone_file_tool(domain: str, zone_data: str, dry_run: bool = False) -> List[Dict[str, Any]]:
"""Import DNS records from zone file format.
Args:
domain: The domain name to import records to
zone_data: DNS zone file content as string
dry_run: If True, only validate and return what would be created without making changes
Returns:
List of created records or validation results
"""
return await vultr_client.import_zone_file(domain, zone_data, dry_run)
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()

1035
src/mcp_vultr/server.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,608 +0,0 @@
"""
Vultr DNS MCP Server Implementation.
This module contains the main VultrDNSServer class and MCP server implementation
for managing DNS records through the Vultr API.
"""
import os
import re
from typing import Any, Dict, List, Optional
import httpx
from fastmcp import FastMCP
from pydantic import BaseModel
class VultrDNSServer:
"""
Vultr DNS API client for managing domains and DNS records.
This class provides async methods for all DNS operations including
domain management and record CRUD operations.
"""
API_BASE = "https://api.vultr.com/v2"
def __init__(self, api_key: str):
"""
Initialize the Vultr DNS server.
Args:
api_key: Your Vultr API key
"""
self.api_key = api_key
self.headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
async def _make_request(
self,
method: str,
endpoint: str,
data: Optional[Dict] = None
) -> Dict[str, Any]:
"""Make an HTTP request to the Vultr API."""
url = f"{self.API_BASE}{endpoint}"
async with httpx.AsyncClient() as client:
response = await client.request(
method=method,
url=url,
headers=self.headers,
json=data
)
if response.status_code not in [200, 201, 204]:
raise Exception(f"Vultr API error {response.status_code}: {response.text}")
if response.status_code == 204:
return {}
return response.json()
# Domain Management Methods
async def list_domains(self) -> List[Dict[str, Any]]:
"""List all DNS domains."""
result = await self._make_request("GET", "/domains")
return result.get("domains", [])
async def get_domain(self, domain: str) -> Dict[str, Any]:
"""Get details for a specific domain."""
return await self._make_request("GET", f"/domains/{domain}")
async def create_domain(self, domain: str, ip: str) -> Dict[str, Any]:
"""Create a new DNS domain."""
data = {"domain": domain, "ip": ip}
return await self._make_request("POST", "/domains", data)
async def delete_domain(self, domain: str) -> Dict[str, Any]:
"""Delete a DNS domain."""
return await self._make_request("DELETE", f"/domains/{domain}")
# DNS Record Management Methods
async def list_records(self, domain: str) -> List[Dict[str, Any]]:
"""List all DNS records for a domain."""
result = await self._make_request("GET", f"/domains/{domain}/records")
return result.get("records", [])
async def get_record(self, domain: str, record_id: str) -> Dict[str, Any]:
"""Get a specific DNS record."""
return await self._make_request("GET", f"/domains/{domain}/records/{record_id}")
async def create_record(
self,
domain: str,
record_type: str,
name: str,
data: str,
ttl: Optional[int] = None,
priority: Optional[int] = None
) -> Dict[str, Any]:
"""Create a new DNS record."""
payload = {
"type": record_type,
"name": name,
"data": data
}
if ttl is not None:
payload["ttl"] = ttl
if priority is not None:
payload["priority"] = priority
return await self._make_request("POST", f"/domains/{domain}/records", payload)
async def update_record(
self,
domain: str,
record_id: str,
record_type: str,
name: str,
data: str,
ttl: Optional[int] = None,
priority: Optional[int] = None
) -> Dict[str, Any]:
"""Update an existing DNS record."""
payload = {
"type": record_type,
"name": name,
"data": data
}
if ttl is not None:
payload["ttl"] = ttl
if priority is not None:
payload["priority"] = priority
return await self._make_request("PATCH", f"/domains/{domain}/records/{record_id}", payload)
async def delete_record(self, domain: str, record_id: str) -> Dict[str, Any]:
"""Delete a DNS record."""
return await self._make_request("DELETE", f"/domains/{domain}/records/{record_id}")
def create_mcp_server(api_key: Optional[str] = None) -> FastMCP:
"""
Create and configure 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
Raises:
ValueError: If API key is not provided and not found in environment
"""
if api_key is None:
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"
)
# Initialize FastMCP server
mcp = FastMCP("Vultr DNS Manager")
# Initialize Vultr client
vultr_client = VultrDNSServer(api_key)
# Add resources for client discovery
@mcp.resource("vultr://domains")
async def get_domains_resource():
"""Resource listing all available domains."""
try:
domains = await vultr_client.list_domains()
return {
"uri": "vultr://domains",
"name": "DNS Domains",
"description": "All DNS domains in your Vultr account",
"mimeType": "application/json",
"content": domains
}
except Exception as e:
return {
"uri": "vultr://domains",
"name": "DNS Domains",
"description": f"Error loading domains: {str(e)}",
"mimeType": "application/json",
"content": {"error": str(e)}
}
@mcp.resource("vultr://capabilities")
async def get_capabilities_resource():
"""Resource describing server capabilities and supported record types."""
return {
"uri": "vultr://capabilities",
"name": "Server Capabilities",
"description": "Vultr DNS server capabilities and supported features",
"mimeType": "application/json",
"content": {
"supported_record_types": [
{
"type": "A",
"description": "IPv4 address record",
"example": "192.168.1.100",
"requires_priority": False
},
{
"type": "AAAA",
"description": "IPv6 address record",
"example": "2001:db8::1",
"requires_priority": False
},
{
"type": "CNAME",
"description": "Canonical name record (alias)",
"example": "example.com",
"requires_priority": False
},
{
"type": "MX",
"description": "Mail exchange record",
"example": "mail.example.com",
"requires_priority": True
},
{
"type": "TXT",
"description": "Text record for verification and SPF",
"example": "v=spf1 include:_spf.google.com ~all",
"requires_priority": False
},
{
"type": "NS",
"description": "Name server record",
"example": "ns1.example.com",
"requires_priority": False
},
{
"type": "SRV",
"description": "Service record",
"example": "0 5 443 example.com",
"requires_priority": True
}
],
"operations": {
"domains": ["list", "create", "delete", "get"],
"records": ["list", "create", "update", "delete", "get"]
},
"default_ttl": 300,
"min_ttl": 60,
"max_ttl": 86400
}
}
@mcp.resource("vultr://records/{domain}")
async def get_domain_records_resource(domain: str):
"""Resource listing all records for a specific domain."""
try:
records = await vultr_client.list_records(domain)
return {
"uri": f"vultr://records/{domain}",
"name": f"DNS Records for {domain}",
"description": f"All DNS records configured for domain {domain}",
"mimeType": "application/json",
"content": {
"domain": domain,
"records": records,
"record_count": len(records)
}
}
except Exception as e:
return {
"uri": f"vultr://records/{domain}",
"name": f"DNS Records for {domain}",
"description": f"Error loading records for {domain}: {str(e)}",
"mimeType": "application/json",
"content": {"error": str(e), "domain": domain}
}
# Define MCP tools
@mcp.tool()
async def list_dns_domains() -> List[Dict[str, Any]]:
"""
List all DNS domains in your Vultr account.
This tool retrieves all domains currently managed through Vultr DNS.
Each domain object includes domain name, creation date, and status information.
"""
try:
domains = await vultr_client.list_domains()
return domains
except Exception as e:
return [{"error": str(e)}]
@mcp.tool()
async def get_dns_domain(domain: str) -> Dict[str, Any]:
"""
Get detailed information for a specific DNS domain.
Args:
domain: The domain name to retrieve (e.g., "example.com")
"""
try:
return await vultr_client.get_domain(domain)
except Exception as e:
return {"error": str(e)}
@mcp.tool()
async def create_dns_domain(domain: str, ip: str) -> Dict[str, Any]:
"""
Create a new DNS domain with a default A record.
Args:
domain: The domain name to create (e.g., "newdomain.com")
ip: IPv4 address for the default A record (e.g., "192.168.1.100")
"""
try:
return await vultr_client.create_domain(domain, ip)
except Exception as e:
return {"error": str(e)}
@mcp.tool()
async def delete_dns_domain(domain: str) -> Dict[str, Any]:
"""
Delete a DNS domain and ALL its associated records.
WARNING: This permanently deletes the domain and all DNS records.
Args:
domain: The domain name to delete (e.g., "example.com")
"""
try:
await vultr_client.delete_domain(domain)
return {"success": f"Domain {domain} deleted successfully"}
except Exception as e:
return {"error": str(e)}
@mcp.tool()
async def list_dns_records(domain: str) -> List[Dict[str, Any]]:
"""
List all DNS records for a specific domain.
Args:
domain: The domain name (e.g., "example.com")
"""
try:
records = await vultr_client.list_records(domain)
return records
except Exception as e:
return [{"error": str(e)}]
@mcp.tool()
async def get_dns_record(domain: str, record_id: str) -> Dict[str, Any]:
"""
Get detailed information for a specific DNS record.
Args:
domain: The domain name (e.g., "example.com")
record_id: The unique record identifier
"""
try:
return await vultr_client.get_record(domain, record_id)
except Exception as e:
return {"error": str(e)}
@mcp.tool()
async def create_dns_record(
domain: str,
record_type: str,
name: str,
data: str,
ttl: Optional[int] = None,
priority: Optional[int] = None
) -> Dict[str, Any]:
"""
Create a new DNS record for a domain.
Args:
domain: The domain name (e.g., "example.com")
record_type: Record type (A, AAAA, CNAME, MX, TXT, NS, SRV)
name: Record name/subdomain
data: Record value
ttl: Time to live in seconds (60-86400, default: 300)
priority: Priority for MX/SRV records (0-65535)
"""
try:
return await vultr_client.create_record(domain, record_type, name, data, ttl, priority)
except Exception as e:
return {"error": str(e)}
@mcp.tool()
async def update_dns_record(
domain: str,
record_id: str,
record_type: str,
name: str,
data: str,
ttl: Optional[int] = None,
priority: Optional[int] = None
) -> Dict[str, Any]:
"""
Update an existing DNS record with new configuration.
Args:
domain: The domain name (e.g., "example.com")
record_id: The unique identifier of the record to update
record_type: New record type (A, AAAA, CNAME, MX, TXT, NS, SRV)
name: New record name/subdomain
data: New record value
ttl: New TTL in seconds (60-86400, optional)
priority: New priority for MX/SRV records (optional)
"""
try:
return await vultr_client.update_record(domain, record_id, record_type, name, data, ttl, priority)
except Exception as e:
return {"error": str(e)}
@mcp.tool()
async def delete_dns_record(domain: str, record_id: str) -> Dict[str, Any]:
"""
Delete a specific DNS record.
Args:
domain: The domain name (e.g., "example.com")
record_id: The unique identifier of the record to delete
"""
try:
await vultr_client.delete_record(domain, record_id)
return {"success": f"DNS record {record_id} deleted successfully"}
except Exception as e:
return {"error": str(e)}
@mcp.tool()
async def validate_dns_record(
record_type: str,
name: str,
data: str,
ttl: Optional[int] = None,
priority: Optional[int] = None
) -> Dict[str, Any]:
"""
Validate DNS record parameters before creation.
Args:
record_type: The record type (A, AAAA, CNAME, MX, TXT, NS, SRV)
name: The record name/subdomain
data: The record data/value
ttl: Time to live in seconds (optional)
priority: Priority for MX/SRV records (optional)
"""
validation_result = {
"valid": True,
"errors": [],
"warnings": [],
"suggestions": []
}
# Validate record type
valid_types = ['A', 'AAAA', 'CNAME', 'MX', 'TXT', 'NS', 'SRV']
if record_type.upper() not in valid_types:
validation_result["valid"] = False
validation_result["errors"].append(f"Invalid record type. Must be one of: {', '.join(valid_types)}")
record_type = record_type.upper()
# Validate TTL
if ttl is not None:
if ttl < 60 or ttl > 86400:
validation_result["warnings"].append("TTL should be between 60 and 86400 seconds")
elif ttl < 300:
validation_result["warnings"].append("Low TTL values may impact DNS performance")
# Record-specific validation
if record_type == 'A':
ipv4_pattern = r'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'
if not re.match(ipv4_pattern, data):
validation_result["valid"] = False
validation_result["errors"].append("Invalid IPv4 address format")
elif record_type == 'AAAA':
if '::' in data and data.count('::') > 1:
validation_result["valid"] = False
validation_result["errors"].append("Invalid IPv6 address: multiple :: sequences")
elif record_type == 'CNAME':
if name == '@' or name == '':
validation_result["valid"] = False
validation_result["errors"].append("CNAME records cannot be used for root domain (@)")
elif record_type == 'MX':
if priority is None:
validation_result["valid"] = False
validation_result["errors"].append("MX records require a priority value")
elif priority < 0 or priority > 65535:
validation_result["valid"] = False
validation_result["errors"].append("MX priority must be between 0 and 65535")
elif record_type == 'SRV':
if priority is None:
validation_result["valid"] = False
validation_result["errors"].append("SRV records require a priority value")
srv_parts = data.split()
if len(srv_parts) != 3:
validation_result["valid"] = False
validation_result["errors"].append("SRV data must be in format: 'weight port target'")
return {
"record_type": record_type,
"name": name,
"data": data,
"ttl": ttl,
"priority": priority,
"validation": validation_result
}
@mcp.tool()
async def analyze_dns_records(domain: str) -> Dict[str, Any]:
"""
Analyze DNS configuration for a domain and provide insights.
Args:
domain: The domain name to analyze (e.g., "example.com")
"""
try:
records = await vultr_client.list_records(domain)
# Analyze records
record_types = {}
total_records = len(records)
ttl_values = []
has_root_a = False
has_www = False
has_mx = False
has_spf = False
for record in records:
record_type = record.get('type', 'UNKNOWN')
record_name = record.get('name', '')
record_data = record.get('data', '')
ttl = record.get('ttl', 300)
record_types[record_type] = record_types.get(record_type, 0) + 1
ttl_values.append(ttl)
if record_type == 'A' and record_name in ['@', domain]:
has_root_a = True
if record_name == 'www':
has_www = True
if record_type == 'MX':
has_mx = True
if record_type == 'TXT' and 'spf1' in record_data.lower():
has_spf = True
# Generate recommendations
recommendations = []
issues = []
if not has_root_a:
recommendations.append("Consider adding an A record for the root domain (@)")
if not has_www:
recommendations.append("Consider adding a www subdomain (A or CNAME record)")
if not has_mx and total_records > 1:
recommendations.append("Consider adding MX records if you plan to use email")
if has_mx and not has_spf:
recommendations.append("Add SPF record (TXT) to prevent email spoofing")
avg_ttl = sum(ttl_values) / len(ttl_values) if ttl_values else 0
low_ttl_count = sum(1 for ttl in ttl_values if ttl < 300)
if low_ttl_count > total_records * 0.5:
issues.append("Many records have very low TTL values, which may impact performance")
return {
"domain": domain,
"analysis": {
"total_records": total_records,
"record_types": record_types,
"average_ttl": round(avg_ttl),
"configuration_status": {
"has_root_domain": has_root_a,
"has_www_subdomain": has_www,
"has_email_mx": has_mx,
"has_spf_protection": has_spf
}
},
"recommendations": recommendations,
"potential_issues": issues,
"records_detail": records
}
except Exception as e:
return {"error": str(e), "domain": domain}
return mcp
def run_server(api_key: Optional[str] = None) -> None:
"""
Create and run a Vultr DNS MCP server.
Args:
api_key: Vultr API key. If not provided, will read from VULTR_API_KEY env var.
"""
mcp = create_mcp_server(api_key)
mcp.run()

View File

@ -3,7 +3,7 @@
Version synchronization script for vultr-dns-mcp. Version synchronization script for vultr-dns-mcp.
This script ensures that the version number in pyproject.toml This script ensures that the version number in pyproject.toml
and src/vultr_dns_mcp/_version.py are kept in sync. and src/mcp_vultr/_version.py are kept in sync.
Usage: Usage:
python sync_version.py # Check if versions are in sync python sync_version.py # Check if versions are in sync
@ -40,9 +40,9 @@ def get_version_from_pyproject() -> str:
def get_version_from_version_py() -> str: def get_version_from_version_py() -> str:
"""Get version from _version.py.""" """Get version from _version.py."""
version_path = Path("src/vultr_dns_mcp/_version.py") version_path = Path("src/mcp_vultr/_version.py")
if not version_path.exists(): if not version_path.exists():
raise FileNotFoundError("src/vultr_dns_mcp/_version.py not found") raise FileNotFoundError("src/mcp_vultr/_version.py not found")
with open(version_path, "r") as f: with open(version_path, "r") as f:
content = f.read() content = f.read()
@ -56,7 +56,7 @@ def get_version_from_version_py() -> str:
def update_version_py(new_version: str) -> None: def update_version_py(new_version: str) -> None:
"""Update version in _version.py.""" """Update version in _version.py."""
version_path = Path("src/vultr_dns_mcp/_version.py") version_path = Path("src/mcp_vultr/_version.py")
content = f'''"""Version information for vultr-dns-mcp package.""" content = f'''"""Version information for vultr-dns-mcp package."""

37
test_async_fastmcp.py Normal file
View 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
View File

@ -0,0 +1,143 @@
#!/usr/bin/env python3
"""Test our improvements to the Vultr DNS MCP package."""
import ipaddress
from mcp_vultr.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 mcp_vultr.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")

View File

@ -1 +1 @@
"""Tests for vultr_dns_mcp package.""" """Tests for mcp_vultr package."""

View File

@ -3,7 +3,7 @@
import os import os
import pytest import pytest
from unittest.mock import AsyncMock, MagicMock from unittest.mock import AsyncMock, MagicMock
from vultr_dns_mcp.server import create_mcp_server from mcp_vultr.server import create_mcp_server
@pytest.fixture @pytest.fixture
@ -21,7 +21,7 @@ def mcp_server(mock_api_key):
@pytest.fixture @pytest.fixture
def mock_vultr_client(): def mock_vultr_client():
"""Create a mock VultrDNSServer for testing API interactions.""" """Create a mock VultrDNSServer for testing API interactions."""
from vultr_dns_mcp.server import VultrDNSServer from mcp_vultr.server import VultrDNSServer
mock_client = AsyncMock(spec=VultrDNSServer) mock_client = AsyncMock(spec=VultrDNSServer)

View File

@ -3,7 +3,7 @@
import pytest import pytest
from unittest.mock import patch, AsyncMock, MagicMock from unittest.mock import patch, AsyncMock, MagicMock
from click.testing import CliRunner from click.testing import CliRunner
from vultr_dns_mcp.cli import cli, main from mcp_vultr.cli import cli, main
@pytest.fixture @pytest.fixture
@ -92,7 +92,7 @@ class TestServerCommand:
assert result.exit_code == 1 assert result.exit_code == 1
assert "VULTR_API_KEY is required" in result.output assert "VULTR_API_KEY is required" in result.output
@patch('vultr_dns_mcp.cli.run_server') @patch('mcp_vultr.cli.run_server')
def test_server_command_with_api_key(self, mock_run_server, cli_runner): def test_server_command_with_api_key(self, mock_run_server, cli_runner):
"""Test server command with API key.""" """Test server command with API key."""
with patch.dict('os.environ', {'VULTR_API_KEY': 'test-key'}): with patch.dict('os.environ', {'VULTR_API_KEY': 'test-key'}):
@ -103,7 +103,7 @@ class TestServerCommand:
assert "Starting Vultr DNS MCP Server" in result.output assert "Starting Vultr DNS MCP Server" in result.output
mock_run_server.assert_called_once_with('test-key') mock_run_server.assert_called_once_with('test-key')
@patch('vultr_dns_mcp.cli.run_server') @patch('mcp_vultr.cli.run_server')
def test_server_command_with_error(self, mock_run_server, cli_runner): def test_server_command_with_error(self, mock_run_server, cli_runner):
"""Test server command with error.""" """Test server command with error."""
with patch.dict('os.environ', {'VULTR_API_KEY': 'test-key'}): with patch.dict('os.environ', {'VULTR_API_KEY': 'test-key'}):
@ -118,7 +118,7 @@ class TestServerCommand:
class TestDomainsCommands: class TestDomainsCommands:
"""Test domain management commands.""" """Test domain management commands."""
@patch('vultr_dns_mcp.cli.VultrDNSClient') @patch('mcp_vultr.cli.VultrDNSClient')
def test_list_domains(self, mock_client_class, cli_runner, mock_client_for_cli): def test_list_domains(self, mock_client_class, cli_runner, mock_client_for_cli):
"""Test domains list command.""" """Test domains list command."""
mock_client_class.return_value = mock_client_for_cli mock_client_class.return_value = mock_client_for_cli
@ -131,7 +131,7 @@ class TestDomainsCommands:
assert "test.com" in result.output assert "test.com" in result.output
mock_client_for_cli.domains.assert_called_once() mock_client_for_cli.domains.assert_called_once()
@patch('vultr_dns_mcp.cli.VultrDNSClient') @patch('mcp_vultr.cli.VultrDNSClient')
def test_list_domains_empty(self, mock_client_class, cli_runner): def test_list_domains_empty(self, mock_client_class, cli_runner):
"""Test domains list command with no domains.""" """Test domains list command with no domains."""
mock_client = AsyncMock() mock_client = AsyncMock()
@ -144,7 +144,7 @@ class TestDomainsCommands:
assert result.exit_code == 0 assert result.exit_code == 0
assert "No domains found" in result.output assert "No domains found" in result.output
@patch('vultr_dns_mcp.cli.VultrDNSClient') @patch('mcp_vultr.cli.VultrDNSClient')
def test_domain_info(self, mock_client_class, cli_runner, mock_client_for_cli): def test_domain_info(self, mock_client_class, cli_runner, mock_client_for_cli):
"""Test domains info command.""" """Test domains info command."""
mock_client_class.return_value = mock_client_for_cli mock_client_class.return_value = mock_client_for_cli
@ -157,7 +157,7 @@ class TestDomainsCommands:
assert "Total Records: 5" in result.output assert "Total Records: 5" in result.output
mock_client_for_cli.get_domain_summary.assert_called_once_with('example.com') mock_client_for_cli.get_domain_summary.assert_called_once_with('example.com')
@patch('vultr_dns_mcp.cli.VultrDNSClient') @patch('mcp_vultr.cli.VultrDNSClient')
def test_domain_info_error(self, mock_client_class, cli_runner): def test_domain_info_error(self, mock_client_class, cli_runner):
"""Test domains info command with error.""" """Test domains info command with error."""
mock_client = AsyncMock() mock_client = AsyncMock()
@ -170,7 +170,7 @@ class TestDomainsCommands:
assert result.exit_code == 1 assert result.exit_code == 1
assert "Domain not found" in result.output assert "Domain not found" in result.output
@patch('vultr_dns_mcp.cli.VultrDNSClient') @patch('mcp_vultr.cli.VultrDNSClient')
def test_create_domain(self, mock_client_class, cli_runner, mock_client_for_cli): def test_create_domain(self, mock_client_class, cli_runner, mock_client_for_cli):
"""Test domains create command.""" """Test domains create command."""
mock_client_class.return_value = mock_client_for_cli mock_client_class.return_value = mock_client_for_cli
@ -182,7 +182,7 @@ class TestDomainsCommands:
assert "Created domain newdomain.com" in result.output assert "Created domain newdomain.com" in result.output
mock_client_for_cli.add_domain.assert_called_once_with('newdomain.com', '192.168.1.100') mock_client_for_cli.add_domain.assert_called_once_with('newdomain.com', '192.168.1.100')
@patch('vultr_dns_mcp.cli.VultrDNSClient') @patch('mcp_vultr.cli.VultrDNSClient')
def test_create_domain_error(self, mock_client_class, cli_runner): def test_create_domain_error(self, mock_client_class, cli_runner):
"""Test domains create command with error.""" """Test domains create command with error."""
mock_client = AsyncMock() mock_client = AsyncMock()
@ -200,7 +200,7 @@ class TestDomainsCommands:
class TestRecordsCommands: class TestRecordsCommands:
"""Test DNS records commands.""" """Test DNS records commands."""
@patch('vultr_dns_mcp.cli.VultrDNSClient') @patch('mcp_vultr.cli.VultrDNSClient')
def test_list_records(self, mock_client_class, cli_runner, mock_client_for_cli): def test_list_records(self, mock_client_class, cli_runner, mock_client_for_cli):
"""Test records list command.""" """Test records list command."""
mock_client_class.return_value = mock_client_for_cli mock_client_class.return_value = mock_client_for_cli
@ -213,7 +213,7 @@ class TestRecordsCommands:
assert "rec1" in result.output assert "rec1" in result.output
mock_client_for_cli.records.assert_called_once_with('example.com') mock_client_for_cli.records.assert_called_once_with('example.com')
@patch('vultr_dns_mcp.cli.VultrDNSClient') @patch('mcp_vultr.cli.VultrDNSClient')
def test_list_records_filtered(self, mock_client_class, cli_runner, mock_client_for_cli): def test_list_records_filtered(self, mock_client_class, cli_runner, mock_client_for_cli):
"""Test records list command with type filter.""" """Test records list command with type filter."""
mock_client_class.return_value = mock_client_for_cli mock_client_class.return_value = mock_client_for_cli
@ -224,7 +224,7 @@ class TestRecordsCommands:
assert result.exit_code == 0 assert result.exit_code == 0
mock_client_for_cli.find_records_by_type.assert_called_once_with('example.com', 'A') mock_client_for_cli.find_records_by_type.assert_called_once_with('example.com', 'A')
@patch('vultr_dns_mcp.cli.VultrDNSClient') @patch('mcp_vultr.cli.VultrDNSClient')
def test_list_records_empty(self, mock_client_class, cli_runner): def test_list_records_empty(self, mock_client_class, cli_runner):
"""Test records list command with no records.""" """Test records list command with no records."""
mock_client = AsyncMock() mock_client = AsyncMock()
@ -237,7 +237,7 @@ class TestRecordsCommands:
assert result.exit_code == 0 assert result.exit_code == 0
assert "No records found" in result.output assert "No records found" in result.output
@patch('vultr_dns_mcp.cli.VultrDNSClient') @patch('mcp_vultr.cli.VultrDNSClient')
def test_add_record(self, mock_client_class, cli_runner, mock_client_for_cli): def test_add_record(self, mock_client_class, cli_runner, mock_client_for_cli):
"""Test records add command.""" """Test records add command."""
mock_client_class.return_value = mock_client_for_cli mock_client_class.return_value = mock_client_for_cli
@ -253,7 +253,7 @@ class TestRecordsCommands:
'example.com', 'A', 'www', '192.168.1.100', None, None 'example.com', 'A', 'www', '192.168.1.100', None, None
) )
@patch('vultr_dns_mcp.cli.VultrDNSClient') @patch('mcp_vultr.cli.VultrDNSClient')
def test_add_record_with_ttl_and_priority(self, mock_client_class, cli_runner, mock_client_for_cli): def test_add_record_with_ttl_and_priority(self, mock_client_class, cli_runner, mock_client_for_cli):
"""Test records add command with TTL and priority.""" """Test records add command with TTL and priority."""
mock_client_class.return_value = mock_client_for_cli mock_client_class.return_value = mock_client_for_cli
@ -269,7 +269,7 @@ class TestRecordsCommands:
'example.com', 'MX', '@', 'mail.example.com', 600, 10 'example.com', 'MX', '@', 'mail.example.com', 600, 10
) )
@patch('vultr_dns_mcp.cli.VultrDNSClient') @patch('mcp_vultr.cli.VultrDNSClient')
def test_add_record_error(self, mock_client_class, cli_runner): def test_add_record_error(self, mock_client_class, cli_runner):
"""Test records add command with error.""" """Test records add command with error."""
mock_client = AsyncMock() mock_client = AsyncMock()
@ -284,7 +284,7 @@ class TestRecordsCommands:
assert result.exit_code == 1 assert result.exit_code == 1
assert "Invalid record" in result.output assert "Invalid record" in result.output
@patch('vultr_dns_mcp.cli.VultrDNSClient') @patch('mcp_vultr.cli.VultrDNSClient')
def test_delete_record(self, mock_client_class, cli_runner, mock_client_for_cli): def test_delete_record(self, mock_client_class, cli_runner, mock_client_for_cli):
"""Test records delete command.""" """Test records delete command."""
mock_client_class.return_value = mock_client_for_cli mock_client_class.return_value = mock_client_for_cli
@ -298,7 +298,7 @@ class TestRecordsCommands:
assert "Deleted record record-123" in result.output assert "Deleted record record-123" in result.output
mock_client_for_cli.remove_record.assert_called_once_with('example.com', 'record-123') mock_client_for_cli.remove_record.assert_called_once_with('example.com', 'record-123')
@patch('vultr_dns_mcp.cli.VultrDNSClient') @patch('mcp_vultr.cli.VultrDNSClient')
def test_delete_record_failure(self, mock_client_class, cli_runner): def test_delete_record_failure(self, mock_client_class, cli_runner):
"""Test records delete command failure.""" """Test records delete command failure."""
mock_client = AsyncMock() mock_client = AsyncMock()
@ -318,7 +318,7 @@ class TestRecordsCommands:
class TestSetupCommands: class TestSetupCommands:
"""Test setup utility commands.""" """Test setup utility commands."""
@patch('vultr_dns_mcp.cli.VultrDNSClient') @patch('mcp_vultr.cli.VultrDNSClient')
def test_setup_website(self, mock_client_class, cli_runner, mock_client_for_cli): def test_setup_website(self, mock_client_class, cli_runner, mock_client_for_cli):
"""Test setup-website command.""" """Test setup-website command."""
mock_client_class.return_value = mock_client_for_cli mock_client_class.return_value = mock_client_for_cli
@ -335,7 +335,7 @@ class TestSetupCommands:
'example.com', '192.168.1.100', True, None 'example.com', '192.168.1.100', True, None
) )
@patch('vultr_dns_mcp.cli.VultrDNSClient') @patch('mcp_vultr.cli.VultrDNSClient')
def test_setup_website_no_www(self, mock_client_class, cli_runner, mock_client_for_cli): def test_setup_website_no_www(self, mock_client_class, cli_runner, mock_client_for_cli):
"""Test setup-website command without www.""" """Test setup-website command without www."""
mock_client_class.return_value = mock_client_for_cli mock_client_class.return_value = mock_client_for_cli
@ -350,7 +350,7 @@ class TestSetupCommands:
'example.com', '192.168.1.100', False, None 'example.com', '192.168.1.100', False, None
) )
@patch('vultr_dns_mcp.cli.VultrDNSClient') @patch('mcp_vultr.cli.VultrDNSClient')
def test_setup_website_with_ttl(self, mock_client_class, cli_runner, mock_client_for_cli): def test_setup_website_with_ttl(self, mock_client_class, cli_runner, mock_client_for_cli):
"""Test setup-website command with custom TTL.""" """Test setup-website command with custom TTL."""
mock_client_class.return_value = mock_client_for_cli mock_client_class.return_value = mock_client_for_cli
@ -365,7 +365,7 @@ class TestSetupCommands:
'example.com', '192.168.1.100', True, 600 'example.com', '192.168.1.100', True, 600
) )
@patch('vultr_dns_mcp.cli.VultrDNSClient') @patch('mcp_vultr.cli.VultrDNSClient')
def test_setup_website_with_errors(self, mock_client_class, cli_runner): def test_setup_website_with_errors(self, mock_client_class, cli_runner):
"""Test setup-website command with errors.""" """Test setup-website command with errors."""
mock_client = AsyncMock() mock_client = AsyncMock()
@ -384,7 +384,7 @@ class TestSetupCommands:
assert result.exit_code == 0 assert result.exit_code == 0
assert "Setup completed with some errors" in result.output assert "Setup completed with some errors" in result.output
@patch('vultr_dns_mcp.cli.VultrDNSClient') @patch('mcp_vultr.cli.VultrDNSClient')
def test_setup_email(self, mock_client_class, cli_runner, mock_client_for_cli): def test_setup_email(self, mock_client_class, cli_runner, mock_client_for_cli):
"""Test setup-email command.""" """Test setup-email command."""
mock_client_class.return_value = mock_client_for_cli mock_client_class.return_value = mock_client_for_cli
@ -401,7 +401,7 @@ class TestSetupCommands:
'example.com', 'mail.example.com', 10, None 'example.com', 'mail.example.com', 10, None
) )
@patch('vultr_dns_mcp.cli.VultrDNSClient') @patch('mcp_vultr.cli.VultrDNSClient')
def test_setup_email_custom_priority(self, mock_client_class, cli_runner, mock_client_for_cli): def test_setup_email_custom_priority(self, mock_client_class, cli_runner, mock_client_for_cli):
"""Test setup-email command with custom priority.""" """Test setup-email command with custom priority."""
mock_client_class.return_value = mock_client_for_cli mock_client_class.return_value = mock_client_for_cli
@ -421,7 +421,7 @@ class TestSetupCommands:
class TestCLIErrorHandling: class TestCLIErrorHandling:
"""Test CLI error handling.""" """Test CLI error handling."""
@patch('vultr_dns_mcp.cli.VultrDNSClient') @patch('mcp_vultr.cli.VultrDNSClient')
def test_api_exception_handling(self, mock_client_class, cli_runner): def test_api_exception_handling(self, mock_client_class, cli_runner):
"""Test CLI handling of API exceptions.""" """Test CLI handling of API exceptions."""
mock_client = AsyncMock() mock_client = AsyncMock()

View File

@ -2,7 +2,7 @@
import pytest import pytest
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
from vultr_dns_mcp.client import VultrDNSClient from mcp_vultr.client import VultrDNSClient
@pytest.mark.unit @pytest.mark.unit
@ -18,7 +18,7 @@ class TestVultrDNSClient:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_domains_method(self, mock_api_key, mock_vultr_client): async def test_domains_method(self, mock_api_key, mock_vultr_client):
"""Test the domains() method.""" """Test the domains() method."""
with patch('vultr_dns_mcp.client.VultrDNSServer', return_value=mock_vultr_client): with patch('mcp_vultr.client.VultrDNSServer', return_value=mock_vultr_client):
client = VultrDNSClient(mock_api_key) client = VultrDNSClient(mock_api_key)
result = await client.domains() result = await client.domains()
@ -28,7 +28,7 @@ class TestVultrDNSClient:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_domain_method(self, mock_api_key, mock_vultr_client): async def test_domain_method(self, mock_api_key, mock_vultr_client):
"""Test the domain() method.""" """Test the domain() method."""
with patch('vultr_dns_mcp.client.VultrDNSServer', return_value=mock_vultr_client): with patch('mcp_vultr.client.VultrDNSServer', return_value=mock_vultr_client):
client = VultrDNSClient(mock_api_key) client = VultrDNSClient(mock_api_key)
result = await client.domain("example.com") result = await client.domain("example.com")
@ -38,7 +38,7 @@ class TestVultrDNSClient:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_add_domain_method(self, mock_api_key, mock_vultr_client): async def test_add_domain_method(self, mock_api_key, mock_vultr_client):
"""Test the add_domain() method.""" """Test the add_domain() method."""
with patch('vultr_dns_mcp.client.VultrDNSServer', return_value=mock_vultr_client): with patch('mcp_vultr.client.VultrDNSServer', return_value=mock_vultr_client):
client = VultrDNSClient(mock_api_key) client = VultrDNSClient(mock_api_key)
result = await client.add_domain("newdomain.com", "192.168.1.100") result = await client.add_domain("newdomain.com", "192.168.1.100")
@ -48,7 +48,7 @@ class TestVultrDNSClient:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_remove_domain_success(self, mock_api_key, mock_vultr_client): async def test_remove_domain_success(self, mock_api_key, mock_vultr_client):
"""Test successful domain removal.""" """Test successful domain removal."""
with patch('vultr_dns_mcp.client.VultrDNSServer', return_value=mock_vultr_client): with patch('mcp_vultr.client.VultrDNSServer', return_value=mock_vultr_client):
client = VultrDNSClient(mock_api_key) client = VultrDNSClient(mock_api_key)
result = await client.remove_domain("example.com") result = await client.remove_domain("example.com")
@ -61,7 +61,7 @@ class TestVultrDNSClient:
mock_client = AsyncMock() mock_client = AsyncMock()
mock_client.delete_domain.side_effect = Exception("API Error") mock_client.delete_domain.side_effect = Exception("API Error")
with patch('vultr_dns_mcp.client.VultrDNSServer', return_value=mock_client): with patch('mcp_vultr.client.VultrDNSServer', return_value=mock_client):
client = VultrDNSClient(mock_api_key) client = VultrDNSClient(mock_api_key)
result = await client.remove_domain("example.com") result = await client.remove_domain("example.com")
@ -70,7 +70,7 @@ class TestVultrDNSClient:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_records_method(self, mock_api_key, mock_vultr_client): async def test_records_method(self, mock_api_key, mock_vultr_client):
"""Test the records() method.""" """Test the records() method."""
with patch('vultr_dns_mcp.client.VultrDNSServer', return_value=mock_vultr_client): with patch('mcp_vultr.client.VultrDNSServer', return_value=mock_vultr_client):
client = VultrDNSClient(mock_api_key) client = VultrDNSClient(mock_api_key)
result = await client.records("example.com") result = await client.records("example.com")
@ -80,7 +80,7 @@ class TestVultrDNSClient:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_add_record_method(self, mock_api_key, mock_vultr_client): async def test_add_record_method(self, mock_api_key, mock_vultr_client):
"""Test the add_record() method.""" """Test the add_record() method."""
with patch('vultr_dns_mcp.client.VultrDNSServer', return_value=mock_vultr_client): with patch('mcp_vultr.client.VultrDNSServer', return_value=mock_vultr_client):
client = VultrDNSClient(mock_api_key) client = VultrDNSClient(mock_api_key)
result = await client.add_record("example.com", "A", "www", "192.168.1.100", 300) result = await client.add_record("example.com", "A", "www", "192.168.1.100", 300)
@ -92,7 +92,7 @@ class TestVultrDNSClient:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_update_record_method(self, mock_api_key, mock_vultr_client): async def test_update_record_method(self, mock_api_key, mock_vultr_client):
"""Test the update_record() method.""" """Test the update_record() method."""
with patch('vultr_dns_mcp.client.VultrDNSServer', return_value=mock_vultr_client): with patch('mcp_vultr.client.VultrDNSServer', return_value=mock_vultr_client):
client = VultrDNSClient(mock_api_key) client = VultrDNSClient(mock_api_key)
result = await client.update_record( result = await client.update_record(
"example.com", "record-123", "A", "www", "192.168.1.200", 600 "example.com", "record-123", "A", "www", "192.168.1.200", 600
@ -106,7 +106,7 @@ class TestVultrDNSClient:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_remove_record_success(self, mock_api_key, mock_vultr_client): async def test_remove_record_success(self, mock_api_key, mock_vultr_client):
"""Test successful record removal.""" """Test successful record removal."""
with patch('vultr_dns_mcp.client.VultrDNSServer', return_value=mock_vultr_client): with patch('mcp_vultr.client.VultrDNSServer', return_value=mock_vultr_client):
client = VultrDNSClient(mock_api_key) client = VultrDNSClient(mock_api_key)
result = await client.remove_record("example.com", "record-123") result = await client.remove_record("example.com", "record-123")
@ -119,7 +119,7 @@ class TestVultrDNSClient:
mock_client = AsyncMock() mock_client = AsyncMock()
mock_client.delete_record.side_effect = Exception("API Error") mock_client.delete_record.side_effect = Exception("API Error")
with patch('vultr_dns_mcp.client.VultrDNSServer', return_value=mock_client): with patch('mcp_vultr.client.VultrDNSServer', return_value=mock_client):
client = VultrDNSClient(mock_api_key) client = VultrDNSClient(mock_api_key)
result = await client.remove_record("example.com", "record-123") result = await client.remove_record("example.com", "record-123")
@ -133,7 +133,7 @@ class TestConvenienceMethods:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_add_a_record(self, mock_api_key, mock_vultr_client): async def test_add_a_record(self, mock_api_key, mock_vultr_client):
"""Test add_a_record convenience method.""" """Test add_a_record convenience method."""
with patch('vultr_dns_mcp.client.VultrDNSServer', return_value=mock_vultr_client): with patch('mcp_vultr.client.VultrDNSServer', return_value=mock_vultr_client):
client = VultrDNSClient(mock_api_key) client = VultrDNSClient(mock_api_key)
result = await client.add_a_record("example.com", "www", "192.168.1.100", 300) result = await client.add_a_record("example.com", "www", "192.168.1.100", 300)
@ -145,7 +145,7 @@ class TestConvenienceMethods:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_add_aaaa_record(self, mock_api_key, mock_vultr_client): async def test_add_aaaa_record(self, mock_api_key, mock_vultr_client):
"""Test add_aaaa_record convenience method.""" """Test add_aaaa_record convenience method."""
with patch('vultr_dns_mcp.client.VultrDNSServer', return_value=mock_vultr_client): with patch('mcp_vultr.client.VultrDNSServer', return_value=mock_vultr_client):
client = VultrDNSClient(mock_api_key) client = VultrDNSClient(mock_api_key)
result = await client.add_aaaa_record("example.com", "www", "2001:db8::1", 300) result = await client.add_aaaa_record("example.com", "www", "2001:db8::1", 300)
@ -157,7 +157,7 @@ class TestConvenienceMethods:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_add_cname_record(self, mock_api_key, mock_vultr_client): async def test_add_cname_record(self, mock_api_key, mock_vultr_client):
"""Test add_cname_record convenience method.""" """Test add_cname_record convenience method."""
with patch('vultr_dns_mcp.client.VultrDNSServer', return_value=mock_vultr_client): with patch('mcp_vultr.client.VultrDNSServer', return_value=mock_vultr_client):
client = VultrDNSClient(mock_api_key) client = VultrDNSClient(mock_api_key)
result = await client.add_cname_record("example.com", "www", "example.com", 300) result = await client.add_cname_record("example.com", "www", "example.com", 300)
@ -169,7 +169,7 @@ class TestConvenienceMethods:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_add_mx_record(self, mock_api_key, mock_vultr_client): async def test_add_mx_record(self, mock_api_key, mock_vultr_client):
"""Test add_mx_record convenience method.""" """Test add_mx_record convenience method."""
with patch('vultr_dns_mcp.client.VultrDNSServer', return_value=mock_vultr_client): with patch('mcp_vultr.client.VultrDNSServer', return_value=mock_vultr_client):
client = VultrDNSClient(mock_api_key) client = VultrDNSClient(mock_api_key)
result = await client.add_mx_record("example.com", "@", "mail.example.com", 10, 300) result = await client.add_mx_record("example.com", "@", "mail.example.com", 10, 300)
@ -181,7 +181,7 @@ class TestConvenienceMethods:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_add_txt_record(self, mock_api_key, mock_vultr_client): async def test_add_txt_record(self, mock_api_key, mock_vultr_client):
"""Test add_txt_record convenience method.""" """Test add_txt_record convenience method."""
with patch('vultr_dns_mcp.client.VultrDNSServer', return_value=mock_vultr_client): with patch('mcp_vultr.client.VultrDNSServer', return_value=mock_vultr_client):
client = VultrDNSClient(mock_api_key) client = VultrDNSClient(mock_api_key)
result = await client.add_txt_record("example.com", "@", "v=spf1 include:_spf.google.com ~all", 300) result = await client.add_txt_record("example.com", "@", "v=spf1 include:_spf.google.com ~all", 300)
@ -201,7 +201,7 @@ class TestUtilityMethods:
mock_client = AsyncMock() mock_client = AsyncMock()
mock_client.list_records.return_value = sample_records mock_client.list_records.return_value = sample_records
with patch('vultr_dns_mcp.client.VultrDNSServer', return_value=mock_client): with patch('mcp_vultr.client.VultrDNSServer', return_value=mock_client):
client = VultrDNSClient(mock_api_key) client = VultrDNSClient(mock_api_key)
result = await client.find_records_by_type("example.com", "A") result = await client.find_records_by_type("example.com", "A")
@ -214,7 +214,7 @@ class TestUtilityMethods:
mock_client = AsyncMock() mock_client = AsyncMock()
mock_client.list_records.return_value = sample_records mock_client.list_records.return_value = sample_records
with patch('vultr_dns_mcp.client.VultrDNSServer', return_value=mock_client): with patch('mcp_vultr.client.VultrDNSServer', return_value=mock_client):
client = VultrDNSClient(mock_api_key) client = VultrDNSClient(mock_api_key)
result = await client.find_records_by_name("example.com", "@") result = await client.find_records_by_name("example.com", "@")
@ -228,7 +228,7 @@ class TestUtilityMethods:
mock_client.get_domain.return_value = {"domain": "example.com", "date_created": "2024-01-01"} mock_client.get_domain.return_value = {"domain": "example.com", "date_created": "2024-01-01"}
mock_client.list_records.return_value = sample_records mock_client.list_records.return_value = sample_records
with patch('vultr_dns_mcp.client.VultrDNSServer', return_value=mock_client): with patch('mcp_vultr.client.VultrDNSServer', return_value=mock_client):
client = VultrDNSClient(mock_api_key) client = VultrDNSClient(mock_api_key)
result = await client.get_domain_summary("example.com") result = await client.get_domain_summary("example.com")
@ -246,7 +246,7 @@ class TestUtilityMethods:
mock_client = AsyncMock() mock_client = AsyncMock()
mock_client.get_domain.side_effect = Exception("API Error") mock_client.get_domain.side_effect = Exception("API Error")
with patch('vultr_dns_mcp.client.VultrDNSServer', return_value=mock_client): with patch('mcp_vultr.client.VultrDNSServer', return_value=mock_client):
client = VultrDNSClient(mock_api_key) client = VultrDNSClient(mock_api_key)
result = await client.get_domain_summary("example.com") result = await client.get_domain_summary("example.com")
@ -261,7 +261,7 @@ class TestSetupMethods:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_setup_basic_website_success(self, mock_api_key, mock_vultr_client): async def test_setup_basic_website_success(self, mock_api_key, mock_vultr_client):
"""Test successful website setup.""" """Test successful website setup."""
with patch('vultr_dns_mcp.client.VultrDNSServer', return_value=mock_vultr_client): with patch('mcp_vultr.client.VultrDNSServer', return_value=mock_vultr_client):
client = VultrDNSClient(mock_api_key) client = VultrDNSClient(mock_api_key)
result = await client.setup_basic_website("example.com", "192.168.1.100", True, 300) result = await client.setup_basic_website("example.com", "192.168.1.100", True, 300)
@ -275,7 +275,7 @@ class TestSetupMethods:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_setup_basic_website_no_www(self, mock_api_key, mock_vultr_client): async def test_setup_basic_website_no_www(self, mock_api_key, mock_vultr_client):
"""Test website setup without www subdomain.""" """Test website setup without www subdomain."""
with patch('vultr_dns_mcp.client.VultrDNSServer', return_value=mock_vultr_client): with patch('mcp_vultr.client.VultrDNSServer', return_value=mock_vultr_client):
client = VultrDNSClient(mock_api_key) client = VultrDNSClient(mock_api_key)
result = await client.setup_basic_website("example.com", "192.168.1.100", False, 300) result = await client.setup_basic_website("example.com", "192.168.1.100", False, 300)
@ -291,7 +291,7 @@ class TestSetupMethods:
mock_client = AsyncMock() mock_client = AsyncMock()
mock_client.create_record.side_effect = Exception("API Error") mock_client.create_record.side_effect = Exception("API Error")
with patch('vultr_dns_mcp.client.VultrDNSServer', return_value=mock_client): with patch('mcp_vultr.client.VultrDNSServer', return_value=mock_client):
client = VultrDNSClient(mock_api_key) client = VultrDNSClient(mock_api_key)
result = await client.setup_basic_website("example.com", "192.168.1.100", True, 300) result = await client.setup_basic_website("example.com", "192.168.1.100", True, 300)
@ -301,7 +301,7 @@ class TestSetupMethods:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_setup_email_success(self, mock_api_key, mock_vultr_client): async def test_setup_email_success(self, mock_api_key, mock_vultr_client):
"""Test successful email setup.""" """Test successful email setup."""
with patch('vultr_dns_mcp.client.VultrDNSServer', return_value=mock_vultr_client): with patch('mcp_vultr.client.VultrDNSServer', return_value=mock_vultr_client):
client = VultrDNSClient(mock_api_key) client = VultrDNSClient(mock_api_key)
result = await client.setup_email("example.com", "mail.example.com", 10, 300) result = await client.setup_email("example.com", "mail.example.com", 10, 300)
@ -320,7 +320,7 @@ class TestSetupMethods:
mock_client = AsyncMock() mock_client = AsyncMock()
mock_client.create_record.side_effect = Exception("API Error") mock_client.create_record.side_effect = Exception("API Error")
with patch('vultr_dns_mcp.client.VultrDNSServer', return_value=mock_client): with patch('mcp_vultr.client.VultrDNSServer', return_value=mock_client):
client = VultrDNSClient(mock_api_key) client = VultrDNSClient(mock_api_key)
result = await client.setup_email("example.com", "mail.example.com", 10, 300) result = await client.setup_email("example.com", "mail.example.com", 10, 300)

View File

@ -1,9 +1,10 @@
"""Tests for MCP server functionality using FastMCP testing patterns.""" """Tests for MCP server functionality using official MCP testing patterns."""
import pytest import pytest
from unittest.mock import patch, AsyncMock from unittest.mock import patch, AsyncMock
from fastmcp import Client from mcp.client.session import ClientSession
from vultr_dns_mcp.server import VultrDNSServer, create_mcp_server from mcp.client.stdio import stdio_client
from mcp_vultr.server import VultrDNSServer, create_mcp_server
class TestMCPServerBasics: class TestMCPServerBasics:
@ -35,11 +36,12 @@ class TestMCPTools:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_list_dns_domains_tool(self, mcp_server, mock_vultr_client): async def test_list_dns_domains_tool(self, mcp_server, mock_vultr_client):
"""Test the list_dns_domains MCP tool.""" """Test the list_dns_domains MCP tool."""
with patch('vultr_dns_mcp.server.VultrDNSServer', return_value=mock_vultr_client): with patch('mcp_vultr.server.VultrDNSServer', return_value=mock_vultr_client):
server = create_mcp_server("test-api-key") server = create_mcp_server("test-api-key")
async with Client(server) as client: # For the official MCP package, we need to use ClientSession
result = await client.call_tool("list_dns_domains", {}) async with ClientSession(server) as session:
result = await session.call_tool("list_dns_domains", {})
assert isinstance(result, list) assert isinstance(result, list)
# The result should be a list containing the response # The result should be a list containing the response
@ -52,11 +54,11 @@ class TestMCPTools:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_dns_domain_tool(self, mcp_server, mock_vultr_client): async def test_get_dns_domain_tool(self, mcp_server, mock_vultr_client):
"""Test the get_dns_domain MCP tool.""" """Test the get_dns_domain MCP tool."""
with patch('vultr_dns_mcp.server.VultrDNSServer', return_value=mock_vultr_client): with patch('mcp_vultr.server.VultrDNSServer', return_value=mock_vultr_client):
server = create_mcp_server("test-api-key") server = create_mcp_server("test-api-key")
async with Client(server) as client: async with ClientSession(server) as session:
result = await client.call_tool("get_dns_domain", {"domain": "example.com"}) result = await session.call_tool("get_dns_domain", {"domain": "example.com"})
assert result is not None assert result is not None
mock_vultr_client.get_domain.assert_called_once_with("example.com") mock_vultr_client.get_domain.assert_called_once_with("example.com")
@ -64,11 +66,11 @@ class TestMCPTools:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_create_dns_domain_tool(self, mcp_server, mock_vultr_client): async def test_create_dns_domain_tool(self, mcp_server, mock_vultr_client):
"""Test the create_dns_domain MCP tool.""" """Test the create_dns_domain MCP tool."""
with patch('vultr_dns_mcp.server.VultrDNSServer', return_value=mock_vultr_client): with patch('mcp_vultr.server.VultrDNSServer', return_value=mock_vultr_client):
server = create_mcp_server("test-api-key") server = create_mcp_server("test-api-key")
async with Client(server) as client: async with ClientSession(server) as session:
result = await client.call_tool("create_dns_domain", { result = await session.call_tool("create_dns_domain", {
"domain": "newdomain.com", "domain": "newdomain.com",
"ip": "192.168.1.100" "ip": "192.168.1.100"
}) })
@ -79,11 +81,11 @@ class TestMCPTools:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_delete_dns_domain_tool(self, mcp_server, mock_vultr_client): async def test_delete_dns_domain_tool(self, mcp_server, mock_vultr_client):
"""Test the delete_dns_domain MCP tool.""" """Test the delete_dns_domain MCP tool."""
with patch('vultr_dns_mcp.server.VultrDNSServer', return_value=mock_vultr_client): with patch('mcp_vultr.server.VultrDNSServer', return_value=mock_vultr_client):
server = create_mcp_server("test-api-key") server = create_mcp_server("test-api-key")
async with Client(server) as client: async with ClientSession(server) as session:
result = await client.call_tool("delete_dns_domain", {"domain": "example.com"}) result = await session.call_tool("delete_dns_domain", {"domain": "example.com"})
assert result is not None assert result is not None
mock_vultr_client.delete_domain.assert_called_once_with("example.com") mock_vultr_client.delete_domain.assert_called_once_with("example.com")
@ -91,11 +93,11 @@ class TestMCPTools:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_list_dns_records_tool(self, mcp_server, mock_vultr_client): async def test_list_dns_records_tool(self, mcp_server, mock_vultr_client):
"""Test the list_dns_records MCP tool.""" """Test the list_dns_records MCP tool."""
with patch('vultr_dns_mcp.server.VultrDNSServer', return_value=mock_vultr_client): with patch('mcp_vultr.server.VultrDNSServer', return_value=mock_vultr_client):
server = create_mcp_server("test-api-key") server = create_mcp_server("test-api-key")
async with Client(server) as client: async with ClientSession(server) as session:
result = await client.call_tool("list_dns_records", {"domain": "example.com"}) result = await session.call_tool("list_dns_records", {"domain": "example.com"})
assert result is not None assert result is not None
mock_vultr_client.list_records.assert_called_once_with("example.com") mock_vultr_client.list_records.assert_called_once_with("example.com")
@ -103,11 +105,11 @@ class TestMCPTools:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_create_dns_record_tool(self, mcp_server, mock_vultr_client): async def test_create_dns_record_tool(self, mcp_server, mock_vultr_client):
"""Test the create_dns_record MCP tool.""" """Test the create_dns_record MCP tool."""
with patch('vultr_dns_mcp.server.VultrDNSServer', return_value=mock_vultr_client): with patch('mcp_vultr.server.VultrDNSServer', return_value=mock_vultr_client):
server = create_mcp_server("test-api-key") server = create_mcp_server("test-api-key")
async with Client(server) as client: async with ClientSession(server) as session:
result = await client.call_tool("create_dns_record", { result = await session.call_tool("create_dns_record", {
"domain": "example.com", "domain": "example.com",
"record_type": "A", "record_type": "A",
"name": "www", "name": "www",
@ -123,9 +125,9 @@ class TestMCPTools:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_validate_dns_record_tool(self, mcp_server): async def test_validate_dns_record_tool(self, mcp_server):
"""Test the validate_dns_record MCP tool.""" """Test the validate_dns_record MCP tool."""
async with Client(mcp_server) as client: async with ClientSession(mcp_server) as session:
# Test valid A record # Test valid A record
result = await client.call_tool("validate_dns_record", { result = await session.call_tool("validate_dns_record", {
"record_type": "A", "record_type": "A",
"name": "www", "name": "www",
"data": "192.168.1.100", "data": "192.168.1.100",
@ -138,9 +140,9 @@ class TestMCPTools:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_validate_dns_record_invalid(self, mcp_server): async def test_validate_dns_record_invalid(self, mcp_server):
"""Test the validate_dns_record tool with invalid data.""" """Test the validate_dns_record tool with invalid data."""
async with Client(mcp_server) as client: async with ClientSession(mcp_server) as session:
# Test invalid A record (bad IP) # Test invalid A record (bad IP)
result = await client.call_tool("validate_dns_record", { result = await session.call_tool("validate_dns_record", {
"record_type": "A", "record_type": "A",
"name": "www", "name": "www",
"data": "invalid-ip-address" "data": "invalid-ip-address"
@ -152,11 +154,11 @@ class TestMCPTools:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_analyze_dns_records_tool(self, mcp_server, mock_vultr_client): async def test_analyze_dns_records_tool(self, mcp_server, mock_vultr_client):
"""Test the analyze_dns_records MCP tool.""" """Test the analyze_dns_records MCP tool."""
with patch('vultr_dns_mcp.server.VultrDNSServer', return_value=mock_vultr_client): with patch('mcp_vultr.server.VultrDNSServer', return_value=mock_vultr_client):
server = create_mcp_server("test-api-key") server = create_mcp_server("test-api-key")
async with Client(server) as client: async with ClientSession(server) as session:
result = await client.call_tool("analyze_dns_records", {"domain": "example.com"}) result = await session.call_tool("analyze_dns_records", {"domain": "example.com"})
assert result is not None assert result is not None
mock_vultr_client.list_records.assert_called_once_with("example.com") mock_vultr_client.list_records.assert_called_once_with("example.com")
@ -169,12 +171,12 @@ class TestMCPResources:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_domains_resource(self, mcp_server, mock_vultr_client): async def test_domains_resource(self, mcp_server, mock_vultr_client):
"""Test the vultr://domains resource.""" """Test the vultr://domains resource."""
with patch('vultr_dns_mcp.server.VultrDNSServer', return_value=mock_vultr_client): with patch('mcp_vultr.server.VultrDNSServer', return_value=mock_vultr_client):
server = create_mcp_server("test-api-key") server = create_mcp_server("test-api-key")
async with Client(server) as client: async with ClientSession(server) as session:
# Get available resources # Get available resources
resources = await client.list_resources() resources = await session.list_resources()
# Check that domains resource is available # Check that domains resource is available
resource_uris = [r.uri for r in resources] resource_uris = [r.uri for r in resources]
@ -183,24 +185,24 @@ class TestMCPResources:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_capabilities_resource(self, mcp_server): async def test_capabilities_resource(self, mcp_server):
"""Test the vultr://capabilities resource.""" """Test the vultr://capabilities resource."""
async with Client(mcp_server) as client: async with ClientSession(mcp_server) as session:
resources = await client.list_resources() resources = await session.list_resources()
resource_uris = [r.uri for r in resources] resource_uris = [r.uri for r in resources]
assert "vultr://capabilities" in resource_uris assert "vultr://capabilities" in resource_uris
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_read_domains_resource(self, mcp_server, mock_vultr_client): async def test_read_domains_resource(self, mcp_server, mock_vultr_client):
"""Test reading the domains resource content.""" """Test reading the domains resource content."""
with patch('vultr_dns_mcp.server.VultrDNSServer', return_value=mock_vultr_client): with patch('mcp_vultr.server.VultrDNSServer', return_value=mock_vultr_client):
server = create_mcp_server("test-api-key") server = create_mcp_server("test-api-key")
async with Client(server) as client: async with ClientSession(server) as session:
try: try:
result = await client.read_resource("vultr://domains") result = await session.read_resource("vultr://domains")
assert result is not None assert result is not None
mock_vultr_client.list_domains.assert_called_once() mock_vultr_client.list_domains.assert_called_once()
except Exception: except Exception:
# Resource reading might not be available in all FastMCP versions # Resource reading might not be available in all MCP versions
pass pass
@ -214,11 +216,11 @@ class TestMCPToolErrors:
mock_client = AsyncMock() mock_client = AsyncMock()
mock_client.list_domains.side_effect = Exception("API Error") mock_client.list_domains.side_effect = Exception("API Error")
with patch('vultr_dns_mcp.server.VultrDNSServer', return_value=mock_client): with patch('mcp_vultr.server.VultrDNSServer', return_value=mock_client):
server = create_mcp_server("test-api-key") server = create_mcp_server("test-api-key")
async with Client(server) as client: async with ClientSession(server) as session:
result = await client.call_tool("list_dns_domains", {}) result = await session.call_tool("list_dns_domains", {})
# Should handle the error gracefully # Should handle the error gracefully
assert result is not None assert result is not None
@ -226,10 +228,10 @@ class TestMCPToolErrors:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_missing_required_parameters(self, mcp_server): async def test_missing_required_parameters(self, mcp_server):
"""Test tool behavior with missing required parameters.""" """Test tool behavior with missing required parameters."""
async with Client(mcp_server) as client: async with ClientSession(mcp_server) as session:
with pytest.raises(Exception): with pytest.raises(Exception):
# This should fail due to missing required 'domain' parameter # This should fail due to missing required 'domain' parameter
await client.call_tool("get_dns_domain", {}) await session.call_tool("get_dns_domain", {})
@pytest.mark.integration @pytest.mark.integration
@ -239,24 +241,24 @@ class TestMCPIntegration:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_complete_domain_workflow(self, mcp_server, mock_vultr_client): async def test_complete_domain_workflow(self, mcp_server, mock_vultr_client):
"""Test a complete domain management workflow.""" """Test a complete domain management workflow."""
with patch('vultr_dns_mcp.server.VultrDNSServer', return_value=mock_vultr_client): with patch('mcp_vultr.server.VultrDNSServer', return_value=mock_vultr_client):
server = create_mcp_server("test-api-key") server = create_mcp_server("test-api-key")
async with Client(server) as client: async with ClientSession(server) as session:
# 1. List domains # 1. List domains
domains = await client.call_tool("list_dns_domains", {}) domains = await session.call_tool("list_dns_domains", {})
assert domains is not None assert domains is not None
# 2. Get domain details # 2. Get domain details
domain_info = await client.call_tool("get_dns_domain", {"domain": "example.com"}) domain_info = await session.call_tool("get_dns_domain", {"domain": "example.com"})
assert domain_info is not None assert domain_info is not None
# 3. List records # 3. List records
records = await client.call_tool("list_dns_records", {"domain": "example.com"}) records = await session.call_tool("list_dns_records", {"domain": "example.com"})
assert records is not None assert records is not None
# 4. Analyze configuration # 4. Analyze configuration
analysis = await client.call_tool("analyze_dns_records", {"domain": "example.com"}) analysis = await session.call_tool("analyze_dns_records", {"domain": "example.com"})
assert analysis is not None assert analysis is not None
# Verify all expected API calls were made # Verify all expected API calls were made
@ -267,12 +269,12 @@ class TestMCPIntegration:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_record_management_workflow(self, mcp_server, mock_vultr_client): async def test_record_management_workflow(self, mcp_server, mock_vultr_client):
"""Test record creation and management workflow.""" """Test record creation and management workflow."""
with patch('vultr_dns_mcp.server.VultrDNSServer', return_value=mock_vultr_client): with patch('mcp_vultr.server.VultrDNSServer', return_value=mock_vultr_client):
server = create_mcp_server("test-api-key") server = create_mcp_server("test-api-key")
async with Client(server) as client: async with ClientSession(server) as session:
# 1. Validate record before creation # 1. Validate record before creation
validation = await client.call_tool("validate_dns_record", { validation = await session.call_tool("validate_dns_record", {
"record_type": "A", "record_type": "A",
"name": "www", "name": "www",
"data": "192.168.1.100" "data": "192.168.1.100"
@ -280,7 +282,7 @@ class TestMCPIntegration:
assert validation is not None assert validation is not None
# 2. Create the record # 2. Create the record
create_result = await client.call_tool("create_dns_record", { create_result = await session.call_tool("create_dns_record", {
"domain": "example.com", "domain": "example.com",
"record_type": "A", "record_type": "A",
"name": "www", "name": "www",
@ -302,9 +304,9 @@ class TestValidationLogic:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_a_record_validation(self, mcp_server): async def test_a_record_validation(self, mcp_server):
"""Test A record validation logic.""" """Test A record validation logic."""
async with Client(mcp_server) as client: async with ClientSession(mcp_server) as session:
# Valid IPv4 # Valid IPv4
result = await client.call_tool("validate_dns_record", { result = await session.call_tool("validate_dns_record", {
"record_type": "A", "record_type": "A",
"name": "www", "name": "www",
"data": "192.168.1.1" "data": "192.168.1.1"
@ -312,7 +314,7 @@ class TestValidationLogic:
assert result is not None assert result is not None
# Invalid IPv4 # Invalid IPv4
result = await client.call_tool("validate_dns_record", { result = await session.call_tool("validate_dns_record", {
"record_type": "A", "record_type": "A",
"name": "www", "name": "www",
"data": "999.999.999.999" "data": "999.999.999.999"
@ -322,9 +324,9 @@ class TestValidationLogic:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_cname_validation(self, mcp_server): async def test_cname_validation(self, mcp_server):
"""Test CNAME record validation logic.""" """Test CNAME record validation logic."""
async with Client(mcp_server) as client: async with ClientSession(mcp_server) as session:
# Invalid: CNAME on root domain # Invalid: CNAME on root domain
result = await client.call_tool("validate_dns_record", { result = await session.call_tool("validate_dns_record", {
"record_type": "CNAME", "record_type": "CNAME",
"name": "@", "name": "@",
"data": "example.com" "data": "example.com"
@ -332,7 +334,7 @@ class TestValidationLogic:
assert result is not None assert result is not None
# Valid: CNAME on subdomain # Valid: CNAME on subdomain
result = await client.call_tool("validate_dns_record", { result = await session.call_tool("validate_dns_record", {
"record_type": "CNAME", "record_type": "CNAME",
"name": "www", "name": "www",
"data": "example.com" "data": "example.com"
@ -342,9 +344,9 @@ class TestValidationLogic:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_mx_validation(self, mcp_server): async def test_mx_validation(self, mcp_server):
"""Test MX record validation logic.""" """Test MX record validation logic."""
async with Client(mcp_server) as client: async with ClientSession(mcp_server) as session:
# Invalid: Missing priority # Invalid: Missing priority
result = await client.call_tool("validate_dns_record", { result = await session.call_tool("validate_dns_record", {
"record_type": "MX", "record_type": "MX",
"name": "@", "name": "@",
"data": "mail.example.com" "data": "mail.example.com"
@ -352,7 +354,7 @@ class TestValidationLogic:
assert result is not None assert result is not None
# Valid: With priority # Valid: With priority
result = await client.call_tool("validate_dns_record", { result = await session.call_tool("validate_dns_record", {
"record_type": "MX", "record_type": "MX",
"name": "@", "name": "@",
"data": "mail.example.com", "data": "mail.example.com",
@ -360,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__])

View File

@ -13,16 +13,16 @@ sys.path.insert(0, str(src_path))
def test_package_imports(): def test_package_imports():
"""Test that all main package imports work correctly.""" """Test that all main package imports work correctly."""
# Test main package imports # Test main package imports
from vultr_dns_mcp import VultrDNSClient, VultrDNSServer, create_mcp_server from mcp_vultr import VultrDNSClient, VultrDNSServer, create_mcp_server
assert VultrDNSClient is not None assert VultrDNSClient is not None
assert VultrDNSServer is not None assert VultrDNSServer is not None
assert create_mcp_server is not None assert create_mcp_server is not None
# Test individual module imports # Test individual module imports
from vultr_dns_mcp.server import VultrDNSServer as ServerClass from mcp_vultr.server import VultrDNSServer as ServerClass
from vultr_dns_mcp.client import VultrDNSClient as ClientClass from mcp_vultr.client import VultrDNSClient as ClientClass
from vultr_dns_mcp.cli import main from mcp_vultr.cli import main
from vultr_dns_mcp._version import __version__ from mcp_vultr._version import __version__
assert ServerClass is not None assert ServerClass is not None
assert ClientClass is not None assert ClientClass is not None
@ -32,7 +32,7 @@ def test_package_imports():
def test_version_consistency(): def test_version_consistency():
"""Test that version is consistent across files.""" """Test that version is consistent across files."""
from vultr_dns_mcp._version import __version__ from mcp_vultr._version import __version__
# Read version from pyproject.toml # Read version from pyproject.toml
pyproject_path = Path(__file__).parent.parent / "pyproject.toml" pyproject_path = Path(__file__).parent.parent / "pyproject.toml"
@ -58,7 +58,7 @@ def test_fastmcp_available():
def test_mcp_server_creation(): def test_mcp_server_creation():
"""Test that MCP server can be created without errors.""" """Test that MCP server can be created without errors."""
from vultr_dns_mcp.server import create_mcp_server from mcp_vultr.server import create_mcp_server
# This should work with any API key for creation (won't make API calls) # This should work with any API key for creation (won't make API calls)
server = create_mcp_server("test-api-key-for-testing") server = create_mcp_server("test-api-key-for-testing")
@ -71,7 +71,7 @@ def test_mcp_server_creation():
def test_cli_entry_points(): def test_cli_entry_points():
"""Test that CLI entry points are properly configured.""" """Test that CLI entry points are properly configured."""
from vultr_dns_mcp.cli import main, server_command from mcp_vultr.cli import main, server_command
assert callable(main) assert callable(main)
assert callable(server_command) assert callable(server_command)
@ -138,7 +138,7 @@ def test_environment_setup():
def test_package_structure(): def test_package_structure():
"""Test that package structure is correct.""" """Test that package structure is correct."""
package_root = Path(__file__).parent.parent / "src" / "vultr_dns_mcp" package_root = Path(__file__).parent.parent / "src" / "mcp_vultr"
# Check that all expected files exist # Check that all expected files exist
expected_files = [ expected_files = [

View File

@ -2,7 +2,7 @@
import pytest import pytest
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
from vultr_dns_mcp.server import VultrDNSServer, create_mcp_server from mcp_vultr.server import VultrDNSServer, create_mcp_server
class TestVultrDNSServer: class TestVultrDNSServer:
@ -103,14 +103,14 @@ class TestMCPServer:
@pytest.fixture @pytest.fixture
def mock_vultr_server(): def mock_vultr_server():
"""Fixture for mocked VultrDNSServer.""" """Fixture for mocked VultrDNSServer."""
with patch('vultr_dns_mcp.server.VultrDNSServer') as mock: with patch('mcp_vultr.server.VultrDNSServer') as mock:
yield mock yield mock
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_validation_tool(): async def test_validation_tool():
"""Test DNS record validation functionality.""" """Test DNS record validation functionality."""
from vultr_dns_mcp.server import create_mcp_server from mcp_vultr.server import create_mcp_server
# Create server (this will fail without API key, but we can test the structure) # Create server (this will fail without API key, but we can test the structure)
with pytest.raises(ValueError): with pytest.raises(ValueError):

View 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 mcp_vultr.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__])