Compare commits
10 Commits
5c87097158
...
e6f66dc931
Author | SHA1 | Date | |
---|---|---|---|
e6f66dc931 | |||
2d866d275d | |||
509423e650 | |||
ce640e2ee5 | |||
691963a43b | |||
66c4c3cb18 | |||
75ffe33008 | |||
7273fe8539 | |||
![]() |
5df5e9e8a0 | ||
![]() |
b8fd6e4632 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -91,6 +91,9 @@ ipython_config.py
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# uv
|
||||
uv.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
|
16
BUILD.md
16
BUILD.md
@ -1,6 +1,6 @@
|
||||
# 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
|
||||
|
||||
@ -43,8 +43,8 @@ This document provides instructions for building and publishing the `vultr-dns-m
|
||||
```
|
||||
|
||||
This creates:
|
||||
- `dist/vultr_dns_mcp-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-py3-none-any.whl` (wheel)
|
||||
- `dist/mcp-vultr-1.0.0.tar.gz` (source distribution)
|
||||
|
||||
3. **Verify the build:**
|
||||
```bash
|
||||
@ -60,13 +60,13 @@ This document provides instructions for building and publishing the `vultr-dns-m
|
||||
|
||||
2. **Test installation from TestPyPI:**
|
||||
```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:**
|
||||
```bash
|
||||
vultr-dns-mcp --help
|
||||
python -c "from vultr_dns_mcp import VultrDNSClient; print('Import successful')"
|
||||
mcp-vultr --help
|
||||
python -c "from mcp_vultr import VultrDNSClient; print('Import successful')"
|
||||
```
|
||||
|
||||
## Publishing to PyPI
|
||||
@ -77,8 +77,8 @@ This document provides instructions for building and publishing the `vultr-dns-m
|
||||
```
|
||||
|
||||
2. **Verify publication:**
|
||||
- Check the package page: https://pypi.org/project/vultr-dns-mcp/
|
||||
- Test installation: `pip install vultr-dns-mcp`
|
||||
- Check the package page: https://pypi.org/project/mcp-vultr/
|
||||
- Test installation: `pip install mcp-vultr`
|
||||
|
||||
## Version Management
|
||||
|
||||
|
63
CHANGELOG.md
63
CHANGELOG.md
@ -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/),
|
||||
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
|
||||
|
||||
### Fixed
|
||||
|
290
CLAUDE.md
Normal file
290
CLAUDE.md
Normal 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
308
CLAUDE_DESKTOP_SETUP.md
Normal 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! 🎉
|
@ -1,6 +1,6 @@
|
||||
# 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)
|
||||
|
||||
@ -8,29 +8,29 @@ The workflow uses [PyPI's trusted publishing](https://docs.pypi.org/trusted-publ
|
||||
|
||||
### 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
|
||||
3. **Add a new trusted publisher** with these settings:
|
||||
- **PyPI Project Name**: `vultr-dns-mcp`
|
||||
- **PyPI Project Name**: `mcp-vultr`
|
||||
- **Owner**: `rsp2k`
|
||||
- **Repository name**: `vultr-dns-mcp`
|
||||
- **Repository name**: `mcp-vultr`
|
||||
- **Workflow filename**: `publish.yml`
|
||||
- **Environment name**: `pypi`
|
||||
|
||||
### 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
|
||||
3. **Add a new trusted publisher** with these settings:
|
||||
- **PyPI Project Name**: `vultr-dns-mcp`
|
||||
- **PyPI Project Name**: `mcp-vultr`
|
||||
- **Owner**: `rsp2k`
|
||||
- **Repository name**: `vultr-dns-mcp`
|
||||
- **Repository name**: `mcp-vultr`
|
||||
- **Workflow filename**: `publish.yml`
|
||||
- **Environment name**: `testpypi`
|
||||
|
||||
### 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**:
|
||||
- `pypi` (for production releases)
|
||||
- `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`:
|
||||
```toml
|
||||
[project]
|
||||
name = "vultr-dns-mcp"
|
||||
name = "mcp-vultr"
|
||||
version = "1.0.2" # Increment this
|
||||
```
|
||||
|
||||
@ -185,9 +185,9 @@ python -m twine check dist/*
|
||||
## 📊 Monitoring
|
||||
|
||||
After publishing, monitor:
|
||||
- **PyPI downloads**: https://pypistats.org/packages/vultr-dns-mcp
|
||||
- **GitHub releases**: https://github.com/rsp2k/vultr-dns-mcp/releases
|
||||
- **Actions logs**: https://github.com/rsp2k/vultr-dns-mcp/actions
|
||||
- **PyPI downloads**: https://pypistats.org/packages/mcp-vultr
|
||||
- **GitHub releases**: https://github.com/rsp2k/mcp-vultr/releases
|
||||
- **Actions logs**: https://github.com/rsp2k/mcp-vultr/actions
|
||||
|
||||
## 🎯 Next Steps
|
||||
|
||||
|
346
README.md
346
README.md
@ -1,177 +1,253 @@
|
||||
# Vultr DNS MCP Test Suite - Complete Fix Package
|
||||
# Vultr DNS MCP
|
||||
|
||||
## 🎯 Quick Solution
|
||||
A comprehensive Model Context Protocol (MCP) server for managing Vultr DNS records through natural language interfaces.
|
||||
|
||||
I've analyzed the broken tests in the vultr-dns-mcp repository and created a complete fix package. Here's how to apply it:
|
||||
[](https://www.python.org/downloads/)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://modelcontextprotocol.io/)
|
||||
|
||||
## Features
|
||||
|
||||
- **Complete MCP Server**: Full Model Context Protocol implementation with 12 tools and 3 resources
|
||||
- **Comprehensive DNS Management**: Support for all major record types (A, AAAA, CNAME, MX, TXT, NS, SRV)
|
||||
- **Intelligent Validation**: Pre-creation validation with helpful suggestions and warnings
|
||||
- **Configuration Analysis**: DNS setup analysis with security recommendations
|
||||
- **CLI Interface**: Complete command-line tool for direct DNS operations
|
||||
- **High-Level Client**: Convenient Python API for common operations
|
||||
- **Modern Development**: Fast development workflow with uv support
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Installation
|
||||
|
||||
### One-Command Fix (if you have access to this directory):
|
||||
```bash
|
||||
# From your vultr-dns-mcp repository root:
|
||||
bash /home/rpm/claude/vultr-dns-mcp-fix/fix_tests.sh
|
||||
# Using uv (recommended - fast and modern)
|
||||
uv add mcp-vultr
|
||||
|
||||
# Or using pip
|
||||
pip install mcp-vultr
|
||||
```
|
||||
|
||||
### Manual Fix (recommended):
|
||||
### Basic Usage
|
||||
|
||||
```bash
|
||||
# 1. Navigate to your repository
|
||||
cd /path/to/vultr-dns-mcp
|
||||
# Set your Vultr API key
|
||||
export VULTR_API_KEY="your-api-key"
|
||||
|
||||
# 2. Backup current files
|
||||
cp tests/conftest.py tests/conftest.py.backup
|
||||
cp tests/test_mcp_server.py tests/test_mcp_server.py.backup
|
||||
# List domains
|
||||
mcp-vultr domains list
|
||||
|
||||
# 3. Copy fixed files
|
||||
cp /home/rpm/claude/vultr-dns-mcp-fix/fixed_conftest.py tests/conftest.py
|
||||
cp /home/rpm/claude/vultr-dns-mcp-fix/fixed_test_mcp_server.py tests/test_mcp_server.py
|
||||
# List DNS records
|
||||
mcp-vultr records list example.com
|
||||
|
||||
# 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]
|
||||
|
||||
# 5. Run tests
|
||||
pytest tests/ -v
|
||||
# Run tests
|
||||
pytest
|
||||
|
||||
# Run comprehensive test suite
|
||||
python run_tests.py --all-checks
|
||||
```
|
||||
|
||||
## 🔍 Problems Identified & Fixed
|
||||
## MCP Tools Available
|
||||
|
||||
| Issue | Severity | Status | Fix Applied |
|
||||
|-------|----------|--------|------------|
|
||||
| Import path problems | 🔴 Critical | ✅ Fixed | Updated all import statements |
|
||||
| Async/await patterns | 🔴 Critical | ✅ Fixed | Fixed FastMCP Client usage |
|
||||
| Mock configuration | 🟡 Medium | ✅ Fixed | Complete API response mocks |
|
||||
| Test data structure | 🟡 Medium | ✅ Fixed | Updated fixtures to match API |
|
||||
| Error handling gaps | 🟢 Low | ✅ Fixed | Added comprehensive error tests |
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `list_dns_domains` | List all DNS domains |
|
||||
| `get_dns_domain` | Get domain details |
|
||||
| `create_dns_domain` | Create new domain |
|
||||
| `delete_dns_domain` | Delete domain and all records |
|
||||
| `list_dns_records` | List records for a domain |
|
||||
| `get_dns_record` | Get specific record details |
|
||||
| `create_dns_record` | Create new DNS record |
|
||||
| `update_dns_record` | Update existing record |
|
||||
| `delete_dns_record` | Delete DNS record |
|
||||
| `validate_dns_record` | Validate record before creation |
|
||||
| `analyze_dns_records` | Analyze domain configuration |
|
||||
|
||||
## 📁 Files in This Fix Package
|
||||
|
||||
### Core Fixes
|
||||
- **`fixed_conftest.py`** - Updated test configuration with proper mocks
|
||||
- **`fixed_test_mcp_server.py`** - All MCP server tests with correct async patterns
|
||||
- **`fix_tests.sh`** - Automated installer script
|
||||
|
||||
### Documentation
|
||||
- **`FINAL_SOLUTION.md`** - Complete solution overview
|
||||
- **`COMPLETE_FIX_GUIDE.md`** - Detailed fix documentation
|
||||
|
||||
### Utilities
|
||||
- **`analyze_test_issues.py`** - Issue analysis script
|
||||
- **`comprehensive_test_fix.py`** - Complete fix generator
|
||||
- **`create_fixes.py`** - Simple fix creator
|
||||
|
||||
## 🚀 What Gets Fixed
|
||||
|
||||
### Before (Broken):
|
||||
```python
|
||||
# Incorrect async pattern
|
||||
async def test_tool(self, mcp_server):
|
||||
result = await client.call_tool("tool_name", {})
|
||||
# ❌ Missing proper async context
|
||||
# ❌ No mock configuration
|
||||
# ❌ Incomplete error handling
|
||||
```
|
||||
|
||||
### After (Fixed):
|
||||
```python
|
||||
@pytest.mark.asyncio
|
||||
async def test_tool(self, mock_vultr_client):
|
||||
with patch('vultr_dns_mcp.server.VultrDNSServer', return_value=mock_vultr_client):
|
||||
server = create_mcp_server("test-api-key")
|
||||
async with Client(server) as client: # ✅ Proper context manager
|
||||
result = await client.call_tool("tool_name", {})
|
||||
assert result is not None # ✅ Proper assertions
|
||||
mock_vultr_client.method.assert_called_once() # ✅ Mock verification
|
||||
```
|
||||
|
||||
## 🧪 Expected Test Results
|
||||
|
||||
After applying the fixes, you should see:
|
||||
## CLI Commands
|
||||
|
||||
```bash
|
||||
$ pytest tests/test_mcp_server.py -v
|
||||
# Domain management
|
||||
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
|
||||
tests/test_mcp_server.py::TestMCPTools::test_list_dns_domains_tool PASSED
|
||||
tests/test_mcp_server.py::TestMCPTools::test_get_dns_domain_tool PASSED
|
||||
tests/test_mcp_server.py::TestMCPTools::test_create_dns_domain_tool PASSED
|
||||
tests/test_mcp_server.py::TestMCPResources::test_domains_resource PASSED
|
||||
tests/test_mcp_server.py::TestMCPIntegration::test_complete_domain_workflow PASSED
|
||||
tests/test_mcp_server.py::TestValidationLogic::test_a_record_validation PASSED
|
||||
# Record management
|
||||
mcp-vultr records list example.com
|
||||
mcp-vultr records add example.com A www 192.168.1.100
|
||||
mcp-vultr records delete example.com record-id
|
||||
|
||||
========================== 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
|
||||
- Proper `@pytest.mark.asyncio` usage
|
||||
- Correct `async with Client(server) as client:` context managers
|
||||
- Fixed await patterns throughout
|
||||
This project follows FastMCP testing best practices with comprehensive test coverage:
|
||||
|
||||
### 2. Improved Mock Configuration
|
||||
- Complete `AsyncMock` setup with proper specs
|
||||
- All Vultr API methods properly mocked
|
||||
- Realistic API response structures
|
||||
```bash
|
||||
# Run all tests (uv)
|
||||
uv run pytest
|
||||
|
||||
### 3. Better Error Handling
|
||||
- Comprehensive error scenario testing
|
||||
- Graceful handling of API failures
|
||||
- Proper exception testing patterns
|
||||
# Run specific test categories
|
||||
uv run pytest -m unit # Unit tests
|
||||
uv run pytest -m integration # Integration tests
|
||||
uv run pytest -m mcp # MCP-specific tests
|
||||
|
||||
### 4. Updated Dependencies
|
||||
- Fixed pytest-asyncio configuration
|
||||
- Proper FastMCP version requirements
|
||||
- Added missing test dependencies
|
||||
# With coverage
|
||||
uv run pytest --cov=mcp_vultr --cov-report=html
|
||||
|
||||
## 🆘 Troubleshooting
|
||||
# Full validation suite
|
||||
uv run python run_tests.py --all-checks
|
||||
```
|
||||
|
||||
### If tests still fail:
|
||||
## Contributing
|
||||
|
||||
1. **Check installation**:
|
||||
```bash
|
||||
pip list | grep -E "(pytest|fastmcp|httpx)"
|
||||
```
|
||||
1. Fork the repository
|
||||
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
||||
3. Make your changes
|
||||
4. Run the test suite (`uv run python run_tests.py --all-checks`)
|
||||
5. Commit your changes (`git commit -m 'Add amazing feature'`)
|
||||
6. Push to the branch (`git push origin feature/amazing-feature`)
|
||||
7. Open a Pull Request
|
||||
|
||||
2. **Verify imports**:
|
||||
```bash
|
||||
python -c "from vultr_dns_mcp.server import create_mcp_server"
|
||||
```
|
||||
## Error Handling
|
||||
|
||||
3. **Run single test**:
|
||||
```bash
|
||||
pytest tests/test_mcp_server.py::TestMCPTools::test_list_dns_domains_tool -vvv
|
||||
```
|
||||
The package provides specific exception types for better error handling:
|
||||
|
||||
4. **Check pytest config**:
|
||||
```bash
|
||||
pytest --collect-only tests/
|
||||
```
|
||||
```python
|
||||
from mcp_vultr import (
|
||||
VultrAPIError,
|
||||
VultrAuthError,
|
||||
VultrRateLimitError,
|
||||
VultrResourceNotFoundError,
|
||||
VultrValidationError
|
||||
)
|
||||
|
||||
### Common Issues:
|
||||
- **ImportError**: Run `pip install -e .` from repository root
|
||||
- **AsyncioError**: Ensure `asyncio_mode = "auto"` in pyproject.toml
|
||||
- **MockError**: Check that fixed_conftest.py was properly copied
|
||||
try:
|
||||
await client.get_domain("example.com")
|
||||
except VultrAuthError:
|
||||
print("Invalid API key or insufficient permissions")
|
||||
except VultrResourceNotFoundError:
|
||||
print("Domain not found")
|
||||
except VultrRateLimitError:
|
||||
print("Rate limit exceeded, please try again later")
|
||||
except VultrAPIError as e:
|
||||
print(f"API error {e.status_code}: {e.message}")
|
||||
```
|
||||
|
||||
## 📊 Success Metrics
|
||||
## Configuration
|
||||
|
||||
You'll know the fix worked when:
|
||||
- ✅ Zero test failures in MCP test suite
|
||||
- ✅ All async tests run without warnings
|
||||
- ✅ Mock verification passes
|
||||
- ✅ Coverage >80% on core modules
|
||||
- ✅ Integration tests complete end-to-end
|
||||
Set your Vultr API key via environment variable:
|
||||
|
||||
## 🎉 Summary
|
||||
```bash
|
||||
export VULTR_API_KEY="your-vultr-api-key"
|
||||
```
|
||||
|
||||
This fix package addresses all the major issues in the vultr-dns-mcp test suite:
|
||||
Or pass directly to the client:
|
||||
|
||||
1. **Fixes critical async/await patterns** that were causing test failures
|
||||
2. **Provides comprehensive mock configuration** matching the Vultr API
|
||||
3. **Adds proper error handling tests** for robustness
|
||||
4. **Updates all import statements** to work correctly
|
||||
5. **Includes complete documentation** for maintenance
|
||||
```python
|
||||
client = VultrDNSClient("your-api-key")
|
||||
server = create_mcp_server("your-api-key")
|
||||
```
|
||||
|
||||
The fixed test suite follows FastMCP best practices and provides reliable, maintainable tests for the Vultr DNS MCP server functionality.
|
||||
## License
|
||||
|
||||
---
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
**Quick Start**: Copy `fixed_conftest.py` and `fixed_test_mcp_server.py` to your `tests/` directory, install dependencies with `pip install -e .[dev]`, and run `pytest tests/ -v`.
|
||||
## Links
|
||||
|
||||
**Need Help?** Check `FINAL_SOLUTION.md` for detailed instructions or `COMPLETE_FIX_GUIDE.md` for comprehensive documentation.
|
||||
- [GitHub Repository](https://github.com/rsp2k/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/)
|
35
TESTING.md
35
TESTING.md
@ -104,32 +104,41 @@ def mock_vultr_client():
|
||||
|
||||
## 🚀 Running Tests
|
||||
|
||||
### Using pytest directly:
|
||||
### Using uv (recommended):
|
||||
```bash
|
||||
# All tests
|
||||
pytest
|
||||
uv run pytest
|
||||
|
||||
# Specific categories
|
||||
pytest -m unit
|
||||
pytest -m integration
|
||||
pytest -m mcp
|
||||
pytest -m "not slow"
|
||||
uv run pytest -m unit
|
||||
uv run pytest -m integration
|
||||
uv run pytest -m mcp
|
||||
uv run pytest -m "not slow"
|
||||
|
||||
# With coverage
|
||||
pytest --cov=vultr_dns_mcp --cov-report=html
|
||||
uv run pytest --cov=mcp_vultr --cov-report=html
|
||||
```
|
||||
|
||||
### Using the test runner:
|
||||
```bash
|
||||
# Comprehensive test runner
|
||||
python run_tests.py
|
||||
# Comprehensive test runner (uv)
|
||||
uv run python run_tests.py
|
||||
|
||||
# Specific test types
|
||||
python run_tests.py --type unit --verbose
|
||||
python run_tests.py --type mcp --coverage
|
||||
python run_tests.py --fast # Skip slow tests
|
||||
uv run python run_tests.py --type unit --verbose
|
||||
uv run python run_tests.py --type mcp --coverage
|
||||
uv run python run_tests.py --fast # Skip slow tests
|
||||
|
||||
# Full validation
|
||||
uv run python run_tests.py --all-checks
|
||||
```
|
||||
|
||||
### Traditional approach (fallback):
|
||||
```bash
|
||||
# All tests
|
||||
pytest
|
||||
|
||||
# Test runner
|
||||
python run_tests.py --all-checks
|
||||
```
|
||||
|
||||
@ -154,7 +163,7 @@ testpaths = ["tests"]
|
||||
addopts = [
|
||||
"--strict-markers",
|
||||
"--verbose",
|
||||
"--cov=vultr_dns_mcp",
|
||||
"--cov=mcp_vultr",
|
||||
"--cov-fail-under=80"
|
||||
]
|
||||
asyncio_mode = "auto"
|
||||
|
53
debug_server.py
Normal file
53
debug_server.py
Normal file
@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
from mcp.server import Server
|
||||
from mcp.server.stdio import stdio_server
|
||||
from mcp.types import Resource, Tool, TextContent
|
||||
|
||||
async def main():
|
||||
# Set up API key
|
||||
api_key = os.getenv("VULTR_API_KEY")
|
||||
if not api_key:
|
||||
print("VULTR_API_KEY not set", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Starting server with API key: {api_key[:8]}...", file=sys.stderr)
|
||||
|
||||
# Create minimal server
|
||||
server = Server("vultr-dns-debug")
|
||||
|
||||
@server.list_tools()
|
||||
async def list_tools() -> list[Tool]:
|
||||
return [
|
||||
Tool(
|
||||
name="test_tool",
|
||||
description="A simple test tool",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
@server.call_tool()
|
||||
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
|
||||
if name == "test_tool":
|
||||
return [TextContent(type="text", text="Test tool working!")]
|
||||
return [TextContent(type="text", text=f"Unknown tool: {name}")]
|
||||
|
||||
print("Server configured, starting stdio...", file=sys.stderr)
|
||||
|
||||
try:
|
||||
async with stdio_server() as (read_stream, write_stream):
|
||||
print("Running server...", file=sys.stderr)
|
||||
await server.run(read_stream, write_stream, None)
|
||||
except Exception as e:
|
||||
print(f"Server error: {e}", file=sys.stderr)
|
||||
raise
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
@ -11,7 +11,7 @@ This script demonstrates various ways to use the package:
|
||||
|
||||
import asyncio
|
||||
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():
|
||||
@ -82,7 +82,7 @@ async def validation_example():
|
||||
print("=" * 40)
|
||||
|
||||
# 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)
|
||||
try:
|
||||
|
@ -10,29 +10,59 @@ echo "🔧 Installing vultr-dns-mcp in development mode..."
|
||||
# Change to package directory
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Check if we're in a virtual environment
|
||||
if [[ -z "$VIRTUAL_ENV" ]]; then
|
||||
echo "⚠️ Warning: Not in a virtual environment"
|
||||
echo " Consider running: python -m venv .venv && source .venv/bin/activate"
|
||||
# Check for uv first, fall back to pip
|
||||
if command -v uv &> /dev/null; then
|
||||
echo "📦 Using uv for fast, modern dependency management..."
|
||||
|
||||
# Sync dependencies with dev extras
|
||||
echo "🔄 Syncing dependencies..."
|
||||
uv sync --extra dev
|
||||
|
||||
echo "✅ Installation complete!"
|
||||
echo ""
|
||||
echo "🚀 You can now run:"
|
||||
echo " vultr-dns-mcp --help"
|
||||
echo " vultr-dns-mcp server"
|
||||
echo ""
|
||||
echo "🧪 Run tests with:"
|
||||
echo " uv run pytest"
|
||||
echo " uv run python run_tests.py --all-checks"
|
||||
echo ""
|
||||
echo "🔧 Code quality tools:"
|
||||
echo " uv run black src tests"
|
||||
echo " uv run mypy src"
|
||||
echo ""
|
||||
|
||||
else
|
||||
echo "📦 Using pip (consider installing uv for faster dependency management)..."
|
||||
echo " Install uv: curl -LsSf https://astral.sh/uv/install.sh | sh"
|
||||
echo ""
|
||||
|
||||
# Check if we're in a virtual environment
|
||||
if [[ -z "$VIRTUAL_ENV" ]]; then
|
||||
echo "⚠️ Warning: Not in a virtual environment"
|
||||
echo " Consider running: python -m venv .venv && source .venv/bin/activate"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Install in development mode
|
||||
echo "📦 Installing package dependencies..."
|
||||
pip install -e .
|
||||
|
||||
echo "🧪 Installing development dependencies..."
|
||||
pip install -e .[dev]
|
||||
|
||||
echo "✅ Installation complete!"
|
||||
echo ""
|
||||
echo "🚀 You can now run:"
|
||||
echo " vultr-dns-mcp --help"
|
||||
echo " vultr-dns-mcp server"
|
||||
echo ""
|
||||
echo "🧪 Run tests with:"
|
||||
echo " pytest"
|
||||
echo " python run_tests.py --all-checks"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Install in development mode
|
||||
echo "📦 Installing package dependencies..."
|
||||
pip install -e .
|
||||
|
||||
echo "🧪 Installing development dependencies..."
|
||||
pip install -e .[dev]
|
||||
|
||||
echo "✅ Installation complete!"
|
||||
echo ""
|
||||
echo "🚀 You can now run:"
|
||||
echo " vultr-dns-mcp --help"
|
||||
echo " vultr-dns-mcp server"
|
||||
echo ""
|
||||
echo "🧪 Run tests with:"
|
||||
echo " python test_fix.py"
|
||||
echo " pytest"
|
||||
echo ""
|
||||
echo "📝 Set your API key:"
|
||||
echo " export VULTR_API_KEY='your-api-key-here'"
|
||||
|
@ -3,8 +3,8 @@ requires = ["setuptools>=61.0", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "vultr-dns-mcp"
|
||||
version = "1.0.1"
|
||||
name = "mcp-vultr"
|
||||
version = "1.1.0"
|
||||
description = "A comprehensive Model Context Protocol (MCP) server for managing Vultr DNS records"
|
||||
readme = "README.md"
|
||||
license = {text = "MIT"}
|
||||
@ -43,7 +43,7 @@ classifiers = [
|
||||
]
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"mcp>=1.0.0",
|
||||
"fastmcp>=0.1.0",
|
||||
"httpx>=0.24.0",
|
||||
"pydantic>=2.0.0",
|
||||
"click>=8.0.0"
|
||||
@ -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"
|
||||
|
||||
[project.scripts]
|
||||
vultr-dns-mcp = "vultr_dns_mcp.cli:main"
|
||||
vultr-dns-server = "vultr_dns_mcp.cli:server_command"
|
||||
mcp-vultr = "mcp_vultr.cli:main"
|
||||
vultr-dns-server = "mcp_vultr.cli:server_command"
|
||||
vultr-mcp-server = "mcp_vultr.fastmcp_server:run_server"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
vultr_dns_mcp = ["py.typed"]
|
||||
mcp_vultr = ["py.typed"]
|
||||
|
||||
[tool.black]
|
||||
line-length = 88
|
||||
@ -111,7 +112,7 @@ extend-exclude = '''
|
||||
profile = "black"
|
||||
multi_line_output = 3
|
||||
line_length = 88
|
||||
known_first_party = ["vultr_dns_mcp"]
|
||||
known_first_party = ["mcp_vultr"]
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.10"
|
||||
@ -162,7 +163,7 @@ filterwarnings = [
|
||||
]
|
||||
|
||||
[tool.coverage.run]
|
||||
source = ["src/vultr_dns_mcp"]
|
||||
source = ["src/mcp_vultr"]
|
||||
omit = [
|
||||
"*/tests/*",
|
||||
"*/test_*",
|
||||
@ -180,3 +181,16 @@ exclude_lines = [
|
||||
"if 0:",
|
||||
"if __name__ == .__main__.:"
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
dev-dependencies = [
|
||||
"pytest>=7.0.0",
|
||||
"pytest-asyncio>=0.21.0",
|
||||
"pytest-cov>=4.0.0",
|
||||
"black>=23.0.0",
|
||||
"isort>=5.12.0",
|
||||
"flake8>=6.0.0",
|
||||
"mypy>=1.0.0",
|
||||
"pre-commit>=3.0.0",
|
||||
"twine>=4.0.0"
|
||||
]
|
||||
|
26
run_tests.py
26
run_tests.py
@ -18,8 +18,8 @@ def run_tests(test_type="all", verbose=False, coverage=False, fast=False):
|
||||
# Change to package directory
|
||||
package_dir = Path(__file__).parent
|
||||
|
||||
# Base pytest command
|
||||
cmd = ["python", "-m", "pytest"]
|
||||
# Base pytest command using uv run
|
||||
cmd = ["uv", "run", "pytest"]
|
||||
|
||||
# Add verbosity
|
||||
if verbose:
|
||||
@ -29,7 +29,7 @@ def run_tests(test_type="all", verbose=False, coverage=False, fast=False):
|
||||
|
||||
# Add coverage if requested
|
||||
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
|
||||
if test_type == "unit":
|
||||
@ -73,7 +73,7 @@ def run_tests(test_type="all", verbose=False, coverage=False, fast=False):
|
||||
return result.returncode == 0
|
||||
|
||||
except FileNotFoundError:
|
||||
print("❌ Error: pytest not found. Install with: pip install pytest")
|
||||
print("❌ Error: pytest not found. Install with: uv add pytest")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ Error running tests: {e}")
|
||||
@ -86,10 +86,10 @@ def run_linting():
|
||||
print("=" * 50)
|
||||
|
||||
checks = [
|
||||
(["python", "-m", "black", "--check", "src", "tests"], "Black formatting"),
|
||||
(["python", "-m", "isort", "--check", "src", "tests"], "Import sorting"),
|
||||
(["python", "-m", "flake8", "src", "tests"], "Flake8 linting"),
|
||||
(["python", "-m", "mypy", "src"], "Type checking")
|
||||
(["uv", "run", "black", "--check", "src", "tests"], "Black formatting"),
|
||||
(["uv", "run", "isort", "--check", "src", "tests"], "Import sorting"),
|
||||
(["uv", "run", "flake8", "src", "tests"], "Flake8 linting"),
|
||||
(["uv", "run", "mypy", "src"], "Type checking")
|
||||
]
|
||||
|
||||
all_passed = True
|
||||
@ -130,8 +130,8 @@ def run_package_validation():
|
||||
sys.path.insert(0, str(src_path))
|
||||
|
||||
# Test main imports
|
||||
from vultr_dns_mcp import VultrDNSClient, VultrDNSServer, create_mcp_server
|
||||
from vultr_dns_mcp._version import __version__
|
||||
from mcp_vultr import VultrDNSClient, VultrDNSServer, create_mcp_server
|
||||
from mcp_vultr._version import __version__
|
||||
|
||||
print(f" ✅ Package imports successful (version {__version__})")
|
||||
|
||||
@ -209,9 +209,9 @@ def main():
|
||||
if success:
|
||||
print("🎉 All checks passed!")
|
||||
print("\n📚 Next steps:")
|
||||
print(" • Run 'python -m build' to build the package")
|
||||
print(" • Run 'python -m twine check dist/*' to validate")
|
||||
print(" • Upload to PyPI with 'python -m twine upload dist/*'")
|
||||
print(" • Run 'uv build' to build the package")
|
||||
print(" • Run 'uv run twine check dist/*' to validate")
|
||||
print(" • Upload to PyPI with 'uv run twine upload dist/*'")
|
||||
else:
|
||||
print("❌ Some checks failed. Please fix the issues above.")
|
||||
sys.exit(1)
|
||||
|
26
simple_fastmcp.py
Normal file
26
simple_fastmcp.py
Normal file
@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import os
|
||||
from fastmcp import FastMCP
|
||||
|
||||
print("SIMPLE FASTMCP SERVER STARTING", file=sys.stderr)
|
||||
|
||||
# Create FastMCP server
|
||||
mcp = FastMCP(name="vultr-dns-simple")
|
||||
|
||||
@mcp.tool
|
||||
def test_tool() -> str:
|
||||
"""A simple test tool"""
|
||||
return "Hello from Vultr DNS MCP!"
|
||||
|
||||
@mcp.tool
|
||||
def list_domains() -> str:
|
||||
"""List DNS domains"""
|
||||
return "This would list your DNS domains"
|
||||
|
||||
print("TOOLS REGISTERED, STARTING SERVER", file=sys.stderr)
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("RUNNING MCP SERVER", file=sys.stderr)
|
||||
mcp.run()
|
@ -6,7 +6,7 @@ the Vultr API. It includes tools for domain management, DNS record operations,
|
||||
configuration analysis, and validation.
|
||||
|
||||
Example usage:
|
||||
from vultr_dns_mcp import VultrDNSServer, create_mcp_server
|
||||
from mcp_vultr import VultrDNSServer, create_mcp_server
|
||||
|
||||
# Create a server instance
|
||||
server = VultrDNSServer(api_key="your-api-key")
|
||||
@ -23,7 +23,16 @@ Main functions:
|
||||
run_server: Convenience function to run the MCP server
|
||||
"""
|
||||
|
||||
from .server import VultrDNSServer, create_mcp_server, run_server
|
||||
from .server import (
|
||||
VultrDNSServer,
|
||||
create_mcp_server,
|
||||
run_server,
|
||||
VultrAPIError,
|
||||
VultrAuthError,
|
||||
VultrRateLimitError,
|
||||
VultrResourceNotFoundError,
|
||||
VultrValidationError
|
||||
)
|
||||
from .client import VultrDNSClient
|
||||
from ._version import __version__, __version_info__
|
||||
|
||||
@ -32,6 +41,11 @@ __all__ = [
|
||||
"VultrDNSClient",
|
||||
"create_mcp_server",
|
||||
"run_server",
|
||||
"VultrAPIError",
|
||||
"VultrAuthError",
|
||||
"VultrRateLimitError",
|
||||
"VultrResourceNotFoundError",
|
||||
"VultrValidationError",
|
||||
"__version__",
|
||||
"__version_info__"
|
||||
]
|
17
src/mcp_vultr/__main__.py
Normal file
17
src/mcp_vultr/__main__.py
Normal file
@ -0,0 +1,17 @@
|
||||
"""
|
||||
Main entry point for running the Vultr DNS FastMCP server.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from .fastmcp_server import run_server
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
run_server()
|
||||
except KeyboardInterrupt:
|
||||
print("Server stopped by user", file=sys.stderr)
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(f"Server error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
@ -1,4 +1,4 @@
|
||||
"""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())
|
@ -32,19 +32,8 @@ def cli(ctx: click.Context, api_key: Optional[str]):
|
||||
|
||||
|
||||
@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
|
||||
def server(ctx: click.Context, host: str, port: int):
|
||||
def server(ctx: click.Context):
|
||||
"""Start the Vultr DNS MCP server."""
|
||||
api_key = ctx.obj.get('api_key')
|
||||
|
||||
@ -54,7 +43,7 @@ def server(ctx: click.Context, host: str, port: int):
|
||||
sys.exit(1)
|
||||
|
||||
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")
|
||||
|
||||
try:
|
||||
@ -211,7 +200,7 @@ def list_records(ctx: click.Context, domain: str, record_type: Optional[str]):
|
||||
data = record.get('data', '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:
|
||||
click.echo(f"Error: {e}", err=True)
|
||||
@ -253,7 +242,7 @@ def add_record(
|
||||
sys.exit(1)
|
||||
|
||||
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:
|
||||
click.echo(f"Error: {e}", err=True)
|
297
src/mcp_vultr/fastmcp_server.py
Normal file
297
src/mcp_vultr/fastmcp_server.py
Normal 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
1035
src/mcp_vultr/server.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -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()
|
@ -3,7 +3,7 @@
|
||||
Version synchronization script for vultr-dns-mcp.
|
||||
|
||||
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:
|
||||
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:
|
||||
"""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():
|
||||
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:
|
||||
content = f.read()
|
||||
@ -56,7 +56,7 @@ def get_version_from_version_py() -> str:
|
||||
|
||||
def update_version_py(new_version: str) -> None:
|
||||
"""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."""
|
||||
|
||||
|
37
test_async_fastmcp.py
Normal file
37
test_async_fastmcp.py
Normal file
@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from fastmcp import FastMCP
|
||||
|
||||
print("TESTING ASYNC FASTMCP PATTERNS", file=sys.stderr)
|
||||
|
||||
# Create FastMCP server
|
||||
mcp = FastMCP(name="async-test")
|
||||
|
||||
@mcp.tool
|
||||
def sync_tool() -> str:
|
||||
"""A synchronous test tool"""
|
||||
return "Sync tool working"
|
||||
|
||||
@mcp.tool
|
||||
async def async_tool() -> str:
|
||||
"""An asynchronous test tool"""
|
||||
await asyncio.sleep(0.1) # Simulate async work
|
||||
return "Async tool working"
|
||||
|
||||
@mcp.tool
|
||||
async def async_tool_with_params(name: str, count: int = 1) -> dict:
|
||||
"""An async tool with parameters"""
|
||||
await asyncio.sleep(0.1)
|
||||
return {
|
||||
"message": f"Hello {name}",
|
||||
"count": count,
|
||||
"status": "async success"
|
||||
}
|
||||
|
||||
print("TOOLS REGISTERED, STARTING SERVER", file=sys.stderr)
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("RUNNING ASYNC TEST SERVER", file=sys.stderr)
|
||||
mcp.run()
|
143
test_improvements.py
Normal file
143
test_improvements.py
Normal file
@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Test our improvements to the Vultr DNS MCP package."""
|
||||
|
||||
import ipaddress
|
||||
from 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")
|
@ -1 +1 @@
|
||||
"""Tests for vultr_dns_mcp package."""
|
||||
"""Tests for mcp_vultr package."""
|
||||
|
@ -3,7 +3,7 @@
|
||||
import os
|
||||
import pytest
|
||||
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
|
||||
@ -21,7 +21,7 @@ def mcp_server(mock_api_key):
|
||||
@pytest.fixture
|
||||
def mock_vultr_client():
|
||||
"""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)
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
import pytest
|
||||
from unittest.mock import patch, AsyncMock, MagicMock
|
||||
from click.testing import CliRunner
|
||||
from vultr_dns_mcp.cli import cli, main
|
||||
from mcp_vultr.cli import cli, main
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -92,7 +92,7 @@ class TestServerCommand:
|
||||
assert result.exit_code == 1
|
||||
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):
|
||||
"""Test server command with API 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
|
||||
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):
|
||||
"""Test server command with error."""
|
||||
with patch.dict('os.environ', {'VULTR_API_KEY': 'test-key'}):
|
||||
@ -118,7 +118,7 @@ class TestServerCommand:
|
||||
class TestDomainsCommands:
|
||||
"""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):
|
||||
"""Test domains list command."""
|
||||
mock_client_class.return_value = mock_client_for_cli
|
||||
@ -131,7 +131,7 @@ class TestDomainsCommands:
|
||||
assert "test.com" in result.output
|
||||
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):
|
||||
"""Test domains list command with no domains."""
|
||||
mock_client = AsyncMock()
|
||||
@ -144,7 +144,7 @@ class TestDomainsCommands:
|
||||
assert result.exit_code == 0
|
||||
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):
|
||||
"""Test domains info command."""
|
||||
mock_client_class.return_value = mock_client_for_cli
|
||||
@ -157,7 +157,7 @@ class TestDomainsCommands:
|
||||
assert "Total Records: 5" in result.output
|
||||
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):
|
||||
"""Test domains info command with error."""
|
||||
mock_client = AsyncMock()
|
||||
@ -170,7 +170,7 @@ class TestDomainsCommands:
|
||||
assert result.exit_code == 1
|
||||
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):
|
||||
"""Test domains create command."""
|
||||
mock_client_class.return_value = mock_client_for_cli
|
||||
@ -182,7 +182,7 @@ class TestDomainsCommands:
|
||||
assert "Created domain newdomain.com" in result.output
|
||||
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):
|
||||
"""Test domains create command with error."""
|
||||
mock_client = AsyncMock()
|
||||
@ -200,7 +200,7 @@ class TestDomainsCommands:
|
||||
class TestRecordsCommands:
|
||||
"""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):
|
||||
"""Test records list command."""
|
||||
mock_client_class.return_value = mock_client_for_cli
|
||||
@ -213,7 +213,7 @@ class TestRecordsCommands:
|
||||
assert "rec1" in result.output
|
||||
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):
|
||||
"""Test records list command with type filter."""
|
||||
mock_client_class.return_value = mock_client_for_cli
|
||||
@ -224,7 +224,7 @@ class TestRecordsCommands:
|
||||
assert result.exit_code == 0
|
||||
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):
|
||||
"""Test records list command with no records."""
|
||||
mock_client = AsyncMock()
|
||||
@ -237,7 +237,7 @@ class TestRecordsCommands:
|
||||
assert result.exit_code == 0
|
||||
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):
|
||||
"""Test records add command."""
|
||||
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
|
||||
)
|
||||
|
||||
@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):
|
||||
"""Test records add command with TTL and priority."""
|
||||
mock_client_class.return_value = mock_client_for_cli
|
||||
@ -269,7 +269,7 @@ class TestRecordsCommands:
|
||||
'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):
|
||||
"""Test records add command with error."""
|
||||
mock_client = AsyncMock()
|
||||
@ -284,7 +284,7 @@ class TestRecordsCommands:
|
||||
assert result.exit_code == 1
|
||||
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):
|
||||
"""Test records delete command."""
|
||||
mock_client_class.return_value = mock_client_for_cli
|
||||
@ -298,7 +298,7 @@ class TestRecordsCommands:
|
||||
assert "Deleted record record-123" in result.output
|
||||
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):
|
||||
"""Test records delete command failure."""
|
||||
mock_client = AsyncMock()
|
||||
@ -318,7 +318,7 @@ class TestRecordsCommands:
|
||||
class TestSetupCommands:
|
||||
"""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):
|
||||
"""Test setup-website command."""
|
||||
mock_client_class.return_value = mock_client_for_cli
|
||||
@ -335,7 +335,7 @@ class TestSetupCommands:
|
||||
'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):
|
||||
"""Test setup-website command without www."""
|
||||
mock_client_class.return_value = mock_client_for_cli
|
||||
@ -350,7 +350,7 @@ class TestSetupCommands:
|
||||
'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):
|
||||
"""Test setup-website command with custom TTL."""
|
||||
mock_client_class.return_value = mock_client_for_cli
|
||||
@ -365,7 +365,7 @@ class TestSetupCommands:
|
||||
'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):
|
||||
"""Test setup-website command with errors."""
|
||||
mock_client = AsyncMock()
|
||||
@ -384,7 +384,7 @@ class TestSetupCommands:
|
||||
assert result.exit_code == 0
|
||||
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):
|
||||
"""Test setup-email command."""
|
||||
mock_client_class.return_value = mock_client_for_cli
|
||||
@ -401,7 +401,7 @@ class TestSetupCommands:
|
||||
'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):
|
||||
"""Test setup-email command with custom priority."""
|
||||
mock_client_class.return_value = mock_client_for_cli
|
||||
@ -421,7 +421,7 @@ class TestSetupCommands:
|
||||
class TestCLIErrorHandling:
|
||||
"""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):
|
||||
"""Test CLI handling of API exceptions."""
|
||||
mock_client = AsyncMock()
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from vultr_dns_mcp.client import VultrDNSClient
|
||||
from mcp_vultr.client import VultrDNSClient
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@ -18,7 +18,7 @@ class TestVultrDNSClient:
|
||||
@pytest.mark.asyncio
|
||||
async def test_domains_method(self, mock_api_key, mock_vultr_client):
|
||||
"""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)
|
||||
result = await client.domains()
|
||||
|
||||
@ -28,7 +28,7 @@ class TestVultrDNSClient:
|
||||
@pytest.mark.asyncio
|
||||
async def test_domain_method(self, mock_api_key, mock_vultr_client):
|
||||
"""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)
|
||||
result = await client.domain("example.com")
|
||||
|
||||
@ -38,7 +38,7 @@ class TestVultrDNSClient:
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_domain_method(self, mock_api_key, mock_vultr_client):
|
||||
"""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)
|
||||
result = await client.add_domain("newdomain.com", "192.168.1.100")
|
||||
|
||||
@ -48,7 +48,7 @@ class TestVultrDNSClient:
|
||||
@pytest.mark.asyncio
|
||||
async def test_remove_domain_success(self, mock_api_key, mock_vultr_client):
|
||||
"""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)
|
||||
result = await client.remove_domain("example.com")
|
||||
|
||||
@ -61,7 +61,7 @@ class TestVultrDNSClient:
|
||||
mock_client = AsyncMock()
|
||||
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)
|
||||
result = await client.remove_domain("example.com")
|
||||
|
||||
@ -70,7 +70,7 @@ class TestVultrDNSClient:
|
||||
@pytest.mark.asyncio
|
||||
async def test_records_method(self, mock_api_key, mock_vultr_client):
|
||||
"""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)
|
||||
result = await client.records("example.com")
|
||||
|
||||
@ -80,7 +80,7 @@ class TestVultrDNSClient:
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_record_method(self, mock_api_key, mock_vultr_client):
|
||||
"""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)
|
||||
result = await client.add_record("example.com", "A", "www", "192.168.1.100", 300)
|
||||
|
||||
@ -92,7 +92,7 @@ class TestVultrDNSClient:
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_record_method(self, mock_api_key, mock_vultr_client):
|
||||
"""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)
|
||||
result = await client.update_record(
|
||||
"example.com", "record-123", "A", "www", "192.168.1.200", 600
|
||||
@ -106,7 +106,7 @@ class TestVultrDNSClient:
|
||||
@pytest.mark.asyncio
|
||||
async def test_remove_record_success(self, mock_api_key, mock_vultr_client):
|
||||
"""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)
|
||||
result = await client.remove_record("example.com", "record-123")
|
||||
|
||||
@ -119,7 +119,7 @@ class TestVultrDNSClient:
|
||||
mock_client = AsyncMock()
|
||||
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)
|
||||
result = await client.remove_record("example.com", "record-123")
|
||||
|
||||
@ -133,7 +133,7 @@ class TestConvenienceMethods:
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_a_record(self, mock_api_key, mock_vultr_client):
|
||||
"""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)
|
||||
result = await client.add_a_record("example.com", "www", "192.168.1.100", 300)
|
||||
|
||||
@ -145,7 +145,7 @@ class TestConvenienceMethods:
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_aaaa_record(self, mock_api_key, mock_vultr_client):
|
||||
"""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)
|
||||
result = await client.add_aaaa_record("example.com", "www", "2001:db8::1", 300)
|
||||
|
||||
@ -157,7 +157,7 @@ class TestConvenienceMethods:
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_cname_record(self, mock_api_key, mock_vultr_client):
|
||||
"""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)
|
||||
result = await client.add_cname_record("example.com", "www", "example.com", 300)
|
||||
|
||||
@ -169,7 +169,7 @@ class TestConvenienceMethods:
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_mx_record(self, mock_api_key, mock_vultr_client):
|
||||
"""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)
|
||||
result = await client.add_mx_record("example.com", "@", "mail.example.com", 10, 300)
|
||||
|
||||
@ -181,7 +181,7 @@ class TestConvenienceMethods:
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_txt_record(self, mock_api_key, mock_vultr_client):
|
||||
"""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)
|
||||
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.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)
|
||||
result = await client.find_records_by_type("example.com", "A")
|
||||
|
||||
@ -214,7 +214,7 @@ class TestUtilityMethods:
|
||||
mock_client = AsyncMock()
|
||||
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)
|
||||
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.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)
|
||||
result = await client.get_domain_summary("example.com")
|
||||
|
||||
@ -246,7 +246,7 @@ class TestUtilityMethods:
|
||||
mock_client = AsyncMock()
|
||||
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)
|
||||
result = await client.get_domain_summary("example.com")
|
||||
|
||||
@ -261,7 +261,7 @@ class TestSetupMethods:
|
||||
@pytest.mark.asyncio
|
||||
async def test_setup_basic_website_success(self, mock_api_key, mock_vultr_client):
|
||||
"""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)
|
||||
result = await client.setup_basic_website("example.com", "192.168.1.100", True, 300)
|
||||
|
||||
@ -275,7 +275,7 @@ class TestSetupMethods:
|
||||
@pytest.mark.asyncio
|
||||
async def test_setup_basic_website_no_www(self, mock_api_key, mock_vultr_client):
|
||||
"""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)
|
||||
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.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)
|
||||
result = await client.setup_basic_website("example.com", "192.168.1.100", True, 300)
|
||||
|
||||
@ -301,7 +301,7 @@ class TestSetupMethods:
|
||||
@pytest.mark.asyncio
|
||||
async def test_setup_email_success(self, mock_api_key, mock_vultr_client):
|
||||
"""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)
|
||||
result = await client.setup_email("example.com", "mail.example.com", 10, 300)
|
||||
|
||||
@ -320,7 +320,7 @@ class TestSetupMethods:
|
||||
mock_client = AsyncMock()
|
||||
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)
|
||||
result = await client.setup_email("example.com", "mail.example.com", 10, 300)
|
||||
|
||||
|
@ -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
|
||||
from unittest.mock import patch, AsyncMock
|
||||
from fastmcp import Client
|
||||
from vultr_dns_mcp.server import VultrDNSServer, create_mcp_server
|
||||
from mcp.client.session import ClientSession
|
||||
from mcp.client.stdio import stdio_client
|
||||
from mcp_vultr.server import VultrDNSServer, create_mcp_server
|
||||
|
||||
|
||||
class TestMCPServerBasics:
|
||||
@ -35,11 +36,12 @@ class TestMCPTools:
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_dns_domains_tool(self, mcp_server, mock_vultr_client):
|
||||
"""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")
|
||||
|
||||
async with Client(server) as client:
|
||||
result = await client.call_tool("list_dns_domains", {})
|
||||
# For the official MCP package, we need to use ClientSession
|
||||
async with ClientSession(server) as session:
|
||||
result = await session.call_tool("list_dns_domains", {})
|
||||
|
||||
assert isinstance(result, list)
|
||||
# The result should be a list containing the response
|
||||
@ -52,11 +54,11 @@ class TestMCPTools:
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_dns_domain_tool(self, mcp_server, mock_vultr_client):
|
||||
"""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")
|
||||
|
||||
async with Client(server) as client:
|
||||
result = await client.call_tool("get_dns_domain", {"domain": "example.com"})
|
||||
async with ClientSession(server) as session:
|
||||
result = await session.call_tool("get_dns_domain", {"domain": "example.com"})
|
||||
|
||||
assert result is not None
|
||||
mock_vultr_client.get_domain.assert_called_once_with("example.com")
|
||||
@ -64,11 +66,11 @@ class TestMCPTools:
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_dns_domain_tool(self, mcp_server, mock_vultr_client):
|
||||
"""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")
|
||||
|
||||
async with Client(server) as client:
|
||||
result = await client.call_tool("create_dns_domain", {
|
||||
async with ClientSession(server) as session:
|
||||
result = await session.call_tool("create_dns_domain", {
|
||||
"domain": "newdomain.com",
|
||||
"ip": "192.168.1.100"
|
||||
})
|
||||
@ -79,11 +81,11 @@ class TestMCPTools:
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_dns_domain_tool(self, mcp_server, mock_vultr_client):
|
||||
"""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")
|
||||
|
||||
async with Client(server) as client:
|
||||
result = await client.call_tool("delete_dns_domain", {"domain": "example.com"})
|
||||
async with ClientSession(server) as session:
|
||||
result = await session.call_tool("delete_dns_domain", {"domain": "example.com"})
|
||||
|
||||
assert result is not None
|
||||
mock_vultr_client.delete_domain.assert_called_once_with("example.com")
|
||||
@ -91,11 +93,11 @@ class TestMCPTools:
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_dns_records_tool(self, mcp_server, mock_vultr_client):
|
||||
"""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")
|
||||
|
||||
async with Client(server) as client:
|
||||
result = await client.call_tool("list_dns_records", {"domain": "example.com"})
|
||||
async with ClientSession(server) as session:
|
||||
result = await session.call_tool("list_dns_records", {"domain": "example.com"})
|
||||
|
||||
assert result is not None
|
||||
mock_vultr_client.list_records.assert_called_once_with("example.com")
|
||||
@ -103,11 +105,11 @@ class TestMCPTools:
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_dns_record_tool(self, mcp_server, mock_vultr_client):
|
||||
"""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")
|
||||
|
||||
async with Client(server) as client:
|
||||
result = await client.call_tool("create_dns_record", {
|
||||
async with ClientSession(server) as session:
|
||||
result = await session.call_tool("create_dns_record", {
|
||||
"domain": "example.com",
|
||||
"record_type": "A",
|
||||
"name": "www",
|
||||
@ -123,9 +125,9 @@ class TestMCPTools:
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_dns_record_tool(self, mcp_server):
|
||||
"""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
|
||||
result = await client.call_tool("validate_dns_record", {
|
||||
result = await session.call_tool("validate_dns_record", {
|
||||
"record_type": "A",
|
||||
"name": "www",
|
||||
"data": "192.168.1.100",
|
||||
@ -138,9 +140,9 @@ class TestMCPTools:
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_dns_record_invalid(self, mcp_server):
|
||||
"""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)
|
||||
result = await client.call_tool("validate_dns_record", {
|
||||
result = await session.call_tool("validate_dns_record", {
|
||||
"record_type": "A",
|
||||
"name": "www",
|
||||
"data": "invalid-ip-address"
|
||||
@ -152,11 +154,11 @@ class TestMCPTools:
|
||||
@pytest.mark.asyncio
|
||||
async def test_analyze_dns_records_tool(self, mcp_server, mock_vultr_client):
|
||||
"""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")
|
||||
|
||||
async with Client(server) as client:
|
||||
result = await client.call_tool("analyze_dns_records", {"domain": "example.com"})
|
||||
async with ClientSession(server) as session:
|
||||
result = await session.call_tool("analyze_dns_records", {"domain": "example.com"})
|
||||
|
||||
assert result is not None
|
||||
mock_vultr_client.list_records.assert_called_once_with("example.com")
|
||||
@ -169,12 +171,12 @@ class TestMCPResources:
|
||||
@pytest.mark.asyncio
|
||||
async def test_domains_resource(self, mcp_server, mock_vultr_client):
|
||||
"""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")
|
||||
|
||||
async with Client(server) as client:
|
||||
async with ClientSession(server) as session:
|
||||
# Get available resources
|
||||
resources = await client.list_resources()
|
||||
resources = await session.list_resources()
|
||||
|
||||
# Check that domains resource is available
|
||||
resource_uris = [r.uri for r in resources]
|
||||
@ -183,24 +185,24 @@ class TestMCPResources:
|
||||
@pytest.mark.asyncio
|
||||
async def test_capabilities_resource(self, mcp_server):
|
||||
"""Test the vultr://capabilities resource."""
|
||||
async with Client(mcp_server) as client:
|
||||
resources = await client.list_resources()
|
||||
async with ClientSession(mcp_server) as session:
|
||||
resources = await session.list_resources()
|
||||
resource_uris = [r.uri for r in resources]
|
||||
assert "vultr://capabilities" in resource_uris
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_domains_resource(self, mcp_server, mock_vultr_client):
|
||||
"""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")
|
||||
|
||||
async with Client(server) as client:
|
||||
async with ClientSession(server) as session:
|
||||
try:
|
||||
result = await client.read_resource("vultr://domains")
|
||||
result = await session.read_resource("vultr://domains")
|
||||
assert result is not None
|
||||
mock_vultr_client.list_domains.assert_called_once()
|
||||
except Exception:
|
||||
# Resource reading might not be available in all FastMCP versions
|
||||
# Resource reading might not be available in all MCP versions
|
||||
pass
|
||||
|
||||
|
||||
@ -214,11 +216,11 @@ class TestMCPToolErrors:
|
||||
mock_client = AsyncMock()
|
||||
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")
|
||||
|
||||
async with Client(server) as client:
|
||||
result = await client.call_tool("list_dns_domains", {})
|
||||
async with ClientSession(server) as session:
|
||||
result = await session.call_tool("list_dns_domains", {})
|
||||
|
||||
# Should handle the error gracefully
|
||||
assert result is not None
|
||||
@ -226,10 +228,10 @@ class TestMCPToolErrors:
|
||||
@pytest.mark.asyncio
|
||||
async def test_missing_required_parameters(self, mcp_server):
|
||||
"""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):
|
||||
# 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
|
||||
@ -239,24 +241,24 @@ class TestMCPIntegration:
|
||||
@pytest.mark.asyncio
|
||||
async def test_complete_domain_workflow(self, mcp_server, mock_vultr_client):
|
||||
"""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")
|
||||
|
||||
async with Client(server) as client:
|
||||
async with ClientSession(server) as session:
|
||||
# 1. List domains
|
||||
domains = await client.call_tool("list_dns_domains", {})
|
||||
domains = await session.call_tool("list_dns_domains", {})
|
||||
assert domains is not None
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
# Verify all expected API calls were made
|
||||
@ -267,12 +269,12 @@ class TestMCPIntegration:
|
||||
@pytest.mark.asyncio
|
||||
async def test_record_management_workflow(self, mcp_server, mock_vultr_client):
|
||||
"""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")
|
||||
|
||||
async with Client(server) as client:
|
||||
async with ClientSession(server) as session:
|
||||
# 1. Validate record before creation
|
||||
validation = await client.call_tool("validate_dns_record", {
|
||||
validation = await session.call_tool("validate_dns_record", {
|
||||
"record_type": "A",
|
||||
"name": "www",
|
||||
"data": "192.168.1.100"
|
||||
@ -280,7 +282,7 @@ class TestMCPIntegration:
|
||||
assert validation is not None
|
||||
|
||||
# 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",
|
||||
"record_type": "A",
|
||||
"name": "www",
|
||||
@ -302,9 +304,9 @@ class TestValidationLogic:
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_record_validation(self, mcp_server):
|
||||
"""Test A record validation logic."""
|
||||
async with Client(mcp_server) as client:
|
||||
async with ClientSession(mcp_server) as session:
|
||||
# Valid IPv4
|
||||
result = await client.call_tool("validate_dns_record", {
|
||||
result = await session.call_tool("validate_dns_record", {
|
||||
"record_type": "A",
|
||||
"name": "www",
|
||||
"data": "192.168.1.1"
|
||||
@ -312,7 +314,7 @@ class TestValidationLogic:
|
||||
assert result is not None
|
||||
|
||||
# Invalid IPv4
|
||||
result = await client.call_tool("validate_dns_record", {
|
||||
result = await session.call_tool("validate_dns_record", {
|
||||
"record_type": "A",
|
||||
"name": "www",
|
||||
"data": "999.999.999.999"
|
||||
@ -322,9 +324,9 @@ class TestValidationLogic:
|
||||
@pytest.mark.asyncio
|
||||
async def test_cname_validation(self, mcp_server):
|
||||
"""Test CNAME record validation logic."""
|
||||
async with Client(mcp_server) as client:
|
||||
async with ClientSession(mcp_server) as session:
|
||||
# Invalid: CNAME on root domain
|
||||
result = await client.call_tool("validate_dns_record", {
|
||||
result = await session.call_tool("validate_dns_record", {
|
||||
"record_type": "CNAME",
|
||||
"name": "@",
|
||||
"data": "example.com"
|
||||
@ -332,7 +334,7 @@ class TestValidationLogic:
|
||||
assert result is not None
|
||||
|
||||
# Valid: CNAME on subdomain
|
||||
result = await client.call_tool("validate_dns_record", {
|
||||
result = await session.call_tool("validate_dns_record", {
|
||||
"record_type": "CNAME",
|
||||
"name": "www",
|
||||
"data": "example.com"
|
||||
@ -342,9 +344,9 @@ class TestValidationLogic:
|
||||
@pytest.mark.asyncio
|
||||
async def test_mx_validation(self, mcp_server):
|
||||
"""Test MX record validation logic."""
|
||||
async with Client(mcp_server) as client:
|
||||
async with ClientSession(mcp_server) as session:
|
||||
# Invalid: Missing priority
|
||||
result = await client.call_tool("validate_dns_record", {
|
||||
result = await session.call_tool("validate_dns_record", {
|
||||
"record_type": "MX",
|
||||
"name": "@",
|
||||
"data": "mail.example.com"
|
||||
@ -352,7 +354,7 @@ class TestValidationLogic:
|
||||
assert result is not None
|
||||
|
||||
# Valid: With priority
|
||||
result = await client.call_tool("validate_dns_record", {
|
||||
result = await session.call_tool("validate_dns_record", {
|
||||
"record_type": "MX",
|
||||
"name": "@",
|
||||
"data": "mail.example.com",
|
||||
@ -360,6 +362,94 @@ class TestValidationLogic:
|
||||
})
|
||||
assert result is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_aaaa_record_validation(self, mcp_server):
|
||||
"""Test comprehensive AAAA (IPv6) record validation logic."""
|
||||
async with ClientSession(mcp_server) as session:
|
||||
# Valid IPv6 addresses
|
||||
valid_ipv6_addresses = [
|
||||
"2001:db8::1", # Standard format
|
||||
"2001:0db8:0000:0000:0000:0000:0000:0001", # Full format
|
||||
"::", # All zeros
|
||||
"::1", # Loopback
|
||||
"fe80::1", # Link-local
|
||||
"2001:db8:85a3::8a2e:370:7334", # Mixed compression
|
||||
"::ffff:192.0.2.1", # IPv4-mapped
|
||||
]
|
||||
|
||||
for ipv6_addr in valid_ipv6_addresses:
|
||||
result = await session.call_tool("validate_dns_record", {
|
||||
"record_type": "AAAA",
|
||||
"name": "www",
|
||||
"data": ipv6_addr
|
||||
})
|
||||
assert result is not None
|
||||
# Parse the result to check validation passed
|
||||
import json
|
||||
parsed = json.loads(result[0].text.replace("'", '"'))
|
||||
assert parsed["validation"]["valid"] == True, f"Failed to validate {ipv6_addr}"
|
||||
|
||||
# Invalid IPv6 addresses
|
||||
invalid_ipv6_addresses = [
|
||||
"2001:db8::1::2", # Multiple ::
|
||||
"2001:db8:85a3::8a2e::7334", # Multiple ::
|
||||
"gggg::1", # Invalid hex
|
||||
"2001:db8:85a3:0:0:8a2e:370g:7334", # Invalid character
|
||||
"2001:db8:85a3:0:0:8a2e:370:7334:extra", # Too many groups
|
||||
"", # Empty
|
||||
"192.168.1.1", # IPv4 instead of IPv6
|
||||
]
|
||||
|
||||
for ipv6_addr in invalid_ipv6_addresses:
|
||||
result = await session.call_tool("validate_dns_record", {
|
||||
"record_type": "AAAA",
|
||||
"name": "www",
|
||||
"data": ipv6_addr
|
||||
})
|
||||
assert result is not None
|
||||
# Parse the result to check validation failed
|
||||
import json
|
||||
parsed = json.loads(result[0].text.replace("'", '"'))
|
||||
assert parsed["validation"]["valid"] == False, f"Should have failed to validate {ipv6_addr}"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ipv6_suggestions_and_warnings(self, mcp_server):
|
||||
"""Test that IPv6 validation provides helpful suggestions and warnings."""
|
||||
async with ClientSession(mcp_server) as session:
|
||||
# Test IPv4-mapped suggestion
|
||||
result = await session.call_tool("validate_dns_record", {
|
||||
"record_type": "AAAA",
|
||||
"name": "www",
|
||||
"data": "::ffff:192.0.2.1"
|
||||
})
|
||||
assert result is not None
|
||||
import json
|
||||
parsed = json.loads(result[0].text.replace("'", '"'))
|
||||
suggestions = parsed["validation"]["suggestions"]
|
||||
assert any("IPv4-mapped" in s for s in suggestions)
|
||||
|
||||
# Test compression suggestion
|
||||
result = await session.call_tool("validate_dns_record", {
|
||||
"record_type": "AAAA",
|
||||
"name": "www",
|
||||
"data": "2001:0db8:0000:0000:0000:0000:0000:0001"
|
||||
})
|
||||
assert result is not None
|
||||
parsed = json.loads(result[0].text.replace("'", '"'))
|
||||
suggestions = parsed["validation"]["suggestions"]
|
||||
assert any("compressed format" in s for s in suggestions)
|
||||
|
||||
# Test loopback warning
|
||||
result = await session.call_tool("validate_dns_record", {
|
||||
"record_type": "AAAA",
|
||||
"name": "www",
|
||||
"data": "::1"
|
||||
})
|
||||
assert result is not None
|
||||
parsed = json.loads(result[0].text.replace("'", '"'))
|
||||
warnings = parsed["validation"]["warnings"]
|
||||
assert any("loopback" in w for w in warnings)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__])
|
||||
|
@ -13,16 +13,16 @@ sys.path.insert(0, str(src_path))
|
||||
def test_package_imports():
|
||||
"""Test that all main package imports work correctly."""
|
||||
# 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 VultrDNSServer is not None
|
||||
assert create_mcp_server is not None
|
||||
|
||||
# Test individual module imports
|
||||
from vultr_dns_mcp.server import VultrDNSServer as ServerClass
|
||||
from vultr_dns_mcp.client import VultrDNSClient as ClientClass
|
||||
from vultr_dns_mcp.cli import main
|
||||
from vultr_dns_mcp._version import __version__
|
||||
from mcp_vultr.server import VultrDNSServer as ServerClass
|
||||
from mcp_vultr.client import VultrDNSClient as ClientClass
|
||||
from mcp_vultr.cli import main
|
||||
from mcp_vultr._version import __version__
|
||||
|
||||
assert ServerClass is not None
|
||||
assert ClientClass is not None
|
||||
@ -32,7 +32,7 @@ def test_package_imports():
|
||||
|
||||
def test_version_consistency():
|
||||
"""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
|
||||
pyproject_path = Path(__file__).parent.parent / "pyproject.toml"
|
||||
@ -58,7 +58,7 @@ def test_fastmcp_available():
|
||||
|
||||
def test_mcp_server_creation():
|
||||
"""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)
|
||||
server = create_mcp_server("test-api-key-for-testing")
|
||||
@ -71,7 +71,7 @@ def test_mcp_server_creation():
|
||||
|
||||
def test_cli_entry_points():
|
||||
"""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(server_command)
|
||||
@ -138,7 +138,7 @@ def test_environment_setup():
|
||||
|
||||
def test_package_structure():
|
||||
"""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
|
||||
expected_files = [
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
import pytest
|
||||
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:
|
||||
@ -103,14 +103,14 @@ class TestMCPServer:
|
||||
@pytest.fixture
|
||||
def mock_vultr_server():
|
||||
"""Fixture for mocked VultrDNSServer."""
|
||||
with patch('vultr_dns_mcp.server.VultrDNSServer') as mock:
|
||||
with patch('mcp_vultr.server.VultrDNSServer') as mock:
|
||||
yield mock
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validation_tool():
|
||||
"""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)
|
||||
with pytest.raises(ValueError):
|
||||
|
@ -3,7 +3,14 @@
|
||||
import pytest
|
||||
import httpx
|
||||
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
|
||||
@ -74,10 +81,11 @@ class TestVultrDNSServer:
|
||||
with patch('httpx.AsyncClient') as mock_client:
|
||||
mock_client.return_value.__aenter__.return_value.request.return_value = mock_response
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(VultrValidationError) as exc_info:
|
||||
await server._make_request("GET", "/test")
|
||||
|
||||
assert "Vultr API error 400: Bad Request" in str(exc_info.value)
|
||||
assert exc_info.value.status_code == 400
|
||||
assert "Bad Request" in str(exc_info.value)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_make_request_error_401(self, mock_api_key):
|
||||
@ -91,10 +99,11 @@ class TestVultrDNSServer:
|
||||
with patch('httpx.AsyncClient') as mock_client:
|
||||
mock_client.return_value.__aenter__.return_value.request.return_value = mock_response
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(VultrAuthError) as exc_info:
|
||||
await server._make_request("GET", "/test")
|
||||
|
||||
assert "Vultr API error 401: Unauthorized" in str(exc_info.value)
|
||||
assert exc_info.value.status_code == 401
|
||||
assert "Invalid API key" in str(exc_info.value)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_make_request_error_500(self, mock_api_key):
|
||||
@ -108,10 +117,11 @@ class TestVultrDNSServer:
|
||||
with patch('httpx.AsyncClient') as mock_client:
|
||||
mock_client.return_value.__aenter__.return_value.request.return_value = mock_response
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(VultrAPIError) as exc_info:
|
||||
await server._make_request("GET", "/test")
|
||||
|
||||
assert "Vultr API error 500: Internal Server Error" in str(exc_info.value)
|
||||
assert exc_info.value.status_code == 500
|
||||
assert "Internal Server Error" in str(exc_info.value)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@ -448,11 +458,106 @@ class TestErrorScenarios:
|
||||
with patch('httpx.AsyncClient') as mock_client:
|
||||
mock_client.return_value.__aenter__.return_value.request.return_value = mock_response
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
with pytest.raises(VultrRateLimitError) as exc_info:
|
||||
await server._make_request("GET", "/domains")
|
||||
|
||||
assert exc_info.value.status_code == 429
|
||||
assert "Rate limit exceeded" in str(exc_info.value)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_not_found_error(self, mock_api_key):
|
||||
"""Test handling of 404 Not Found error."""
|
||||
server = VultrDNSServer(mock_api_key)
|
||||
|
||||
mock_response = AsyncMock()
|
||||
mock_response.status_code = 404
|
||||
mock_response.text = "Domain not found"
|
||||
|
||||
with patch('httpx.AsyncClient') as mock_client:
|
||||
mock_client.return_value.__aenter__.return_value.request.return_value = mock_response
|
||||
|
||||
with pytest.raises(VultrResourceNotFoundError) as exc_info:
|
||||
await server._make_request("GET", "/domains/nonexistent.com")
|
||||
|
||||
assert exc_info.value.status_code == 404
|
||||
assert "Resource not found" in str(exc_info.value)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forbidden_error(self, mock_api_key):
|
||||
"""Test handling of 403 Forbidden error."""
|
||||
server = VultrDNSServer(mock_api_key)
|
||||
|
||||
mock_response = AsyncMock()
|
||||
mock_response.status_code = 403
|
||||
mock_response.text = "Forbidden"
|
||||
|
||||
with patch('httpx.AsyncClient') as mock_client:
|
||||
mock_client.return_value.__aenter__.return_value.request.return_value = mock_response
|
||||
|
||||
with pytest.raises(VultrAuthError) as exc_info:
|
||||
await server._make_request("GET", "/domains")
|
||||
|
||||
assert exc_info.value.status_code == 403
|
||||
assert "Insufficient permissions" in str(exc_info.value)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validation_error_422(self, mock_api_key):
|
||||
"""Test handling of 422 Unprocessable Entity error."""
|
||||
server = VultrDNSServer(mock_api_key)
|
||||
|
||||
mock_response = AsyncMock()
|
||||
mock_response.status_code = 422
|
||||
mock_response.text = "Invalid domain format"
|
||||
|
||||
with patch('httpx.AsyncClient') as mock_client:
|
||||
mock_client.return_value.__aenter__.return_value.request.return_value = mock_response
|
||||
|
||||
with pytest.raises(VultrValidationError) as exc_info:
|
||||
await server._make_request("POST", "/domains")
|
||||
|
||||
assert exc_info.value.status_code == 422
|
||||
assert "Invalid domain format" in str(exc_info.value)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestExceptionProperties:
|
||||
"""Test custom exception properties and behavior."""
|
||||
|
||||
def test_vultr_api_error_properties(self):
|
||||
"""Test VultrAPIError has correct properties."""
|
||||
error = VultrAPIError(500, "Server Error")
|
||||
assert error.status_code == 500
|
||||
assert error.message == "Server Error"
|
||||
assert str(error) == "Vultr API error 500: Server Error"
|
||||
|
||||
def test_vultr_auth_error_inheritance(self):
|
||||
"""Test VultrAuthError inherits from VultrAPIError."""
|
||||
error = VultrAuthError(401, "Unauthorized")
|
||||
assert isinstance(error, VultrAPIError)
|
||||
assert error.status_code == 401
|
||||
assert error.message == "Unauthorized"
|
||||
|
||||
def test_vultr_rate_limit_error_inheritance(self):
|
||||
"""Test VultrRateLimitError inherits from VultrAPIError."""
|
||||
error = VultrRateLimitError(429, "Too Many Requests")
|
||||
assert isinstance(error, VultrAPIError)
|
||||
assert error.status_code == 429
|
||||
assert error.message == "Too Many Requests"
|
||||
|
||||
def test_vultr_not_found_error_inheritance(self):
|
||||
"""Test VultrResourceNotFoundError inherits from VultrAPIError."""
|
||||
error = VultrResourceNotFoundError(404, "Not Found")
|
||||
assert isinstance(error, VultrAPIError)
|
||||
assert error.status_code == 404
|
||||
assert error.message == "Not Found"
|
||||
|
||||
def test_vultr_validation_error_inheritance(self):
|
||||
"""Test VultrValidationError inherits from VultrAPIError."""
|
||||
error = VultrValidationError(400, "Bad Request")
|
||||
assert isinstance(error, VultrAPIError)
|
||||
assert error.status_code == 400
|
||||
assert error.message == "Bad Request"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__])
|
||||
|
Loading…
x
Reference in New Issue
Block a user