Initial commit: Vultr MCP Server
This commit is contained in:
19
.env.example
Normal file
19
.env.example
Normal file
@@ -0,0 +1,19 @@
|
||||
# Vultr MCP Server Configuration
|
||||
|
||||
# Vultr API Key (required)
|
||||
# Get your API key from: https://my.vultr.com/settings/#settingsapi
|
||||
VULTR_API_KEY=your_vultr_api_key_here
|
||||
|
||||
# Server Configuration
|
||||
HOST=0.0.0.0
|
||||
PORT=8000
|
||||
LOG_LEVEL=info
|
||||
|
||||
# Optional: Debug settings
|
||||
DEBUG=false
|
||||
LOG_FILE=/var/log/vultr-mcp-server.log
|
||||
|
||||
# Optional: Rate limiting
|
||||
RATE_LIMIT_REQUESTS=100
|
||||
RATE_LIMIT_PERIOD=3600 # seconds
|
||||
|
||||
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.env
|
||||
.idea/
|
||||
.vscode/
|
||||
venv/
|
||||
env/
|
||||
dist/
|
||||
build/
|
||||
*.egg-info/
|
||||
275
README.md
Normal file
275
README.md
Normal file
@@ -0,0 +1,275 @@
|
||||
# Vultr MCP Server
|
||||
|
||||
Model Context Protocol (MCP) Server for Vultr Cloud Services
|
||||
|
||||
## Overview
|
||||
|
||||
This MCP server provides AI applications with access to Vultr cloud services through the Model Context Protocol. It enables AI assistants to manage Vultr infrastructure including instances, storage, networking, and more.
|
||||
|
||||
## Features
|
||||
|
||||
- **Account Management**: Get account information and balance
|
||||
- **Instance Management**: Create, list, start, stop, reboot, and delete instances
|
||||
- **Resource Discovery**: List available regions, plans, and operating systems
|
||||
- **MCP Protocol Compliance**: Full implementation of MCP v0.2+ protocol
|
||||
- **FastAPI Backend**: High-performance async API server
|
||||
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.8+
|
||||
- Vultr API key (from [Vultr API Settings](https://my.vultr.com/settings/#settingsapi))
|
||||
|
||||
### Setup
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd vultr-mcp-server
|
||||
```
|
||||
|
||||
2. Create virtual environment and install dependencies:
|
||||
```bash
|
||||
python -m venv venv
|
||||
source venv/bin/activate # On Windows: venv\Scripts\activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. Configure environment variables:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env and add your VULTR_API_KEY
|
||||
```
|
||||
|
||||
4. Run the server:
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Create a `.env` file with the following:
|
||||
|
||||
```env
|
||||
# Vultr API Key (required)
|
||||
VULTR_API_KEY=your_vultr_api_key_here
|
||||
|
||||
# Server Configuration (optional)
|
||||
HOST=0.0.0.0
|
||||
PORT=8000
|
||||
LOG_LEVEL=info
|
||||
```
|
||||
|
||||
### Getting Vultr API Key
|
||||
|
||||
1. Log in to your Vultr account
|
||||
2. Go to [API Settings](https://my.vultr.com/settings/#settingsapi)
|
||||
3. Click "Enable API" if not already enabled
|
||||
4. Generate a new API key or use an existing one
|
||||
5. Copy the API key to your `.env` file
|
||||
|
||||
## Usage
|
||||
|
||||
### Starting the Server
|
||||
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
The server will start on `http://0.0.0.0:8000` by default.
|
||||
|
||||
### MCP Endpoints
|
||||
|
||||
- `GET /` - Server information
|
||||
- `GET /tools` - List available tools
|
||||
- `GET /resources` - List available resources
|
||||
- `GET /prompts` - List available prompts
|
||||
- `POST /tools/call` - Execute a tool
|
||||
- `GET /resources/{uri}` - Get resource content
|
||||
|
||||
### Available Tools
|
||||
|
||||
#### Account Management
|
||||
- `get_account_info` - Get Vultr account information
|
||||
|
||||
#### Instance Management
|
||||
- `list_instances` - List all Vultr instances
|
||||
- `get_instance` - Get details of a specific instance
|
||||
- `create_instance` - Create a new Vultr instance
|
||||
- `delete_instance` - Delete a Vultr instance
|
||||
- `start_instance` - Start a Vultr instance
|
||||
- `stop_instance` - Stop a Vultr instance
|
||||
- `reboot_instance` - Reboot a Vultr instance
|
||||
|
||||
#### Resource Discovery
|
||||
- `list_regions` - List available Vultr regions
|
||||
- `list_plans` - List available Vultr plans
|
||||
- `list_os` - List available Vultr operating systems
|
||||
|
||||
### Example API Calls
|
||||
|
||||
#### Using curl
|
||||
|
||||
```bash
|
||||
# List available tools
|
||||
curl http://localhost:8000/tools
|
||||
|
||||
# Get account info
|
||||
curl -X POST http://localhost:8000/tools/call \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "get_account_info", "arguments": {}}'
|
||||
|
||||
# List instances
|
||||
curl -X POST http://localhost:8000/tools/call \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "list_instances", "arguments": {"per_page": 10}}'
|
||||
|
||||
# Create an instance
|
||||
curl -X POST http://localhost:8000/tools/call \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "create_instance",
|
||||
"arguments": {
|
||||
"region": "ams",
|
||||
"plan": "vc2-1c-1gb",
|
||||
"os_id": 387,
|
||||
"label": "my-web-server",
|
||||
"hostname": "web1"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
#### Using Python
|
||||
|
||||
```python
|
||||
import requests
|
||||
import json
|
||||
|
||||
# List tools
|
||||
response = requests.get("http://localhost:8000/tools")
|
||||
print(json.dumps(response.json(), indent=2))
|
||||
|
||||
# Create instance
|
||||
data = {
|
||||
"name": "create_instance",
|
||||
"arguments": {
|
||||
"region": "ams",
|
||||
"plan": "vc2-1c-1gb",
|
||||
"os_id": 387,
|
||||
"label": "test-server"
|
||||
}
|
||||
}
|
||||
response = requests.post("http://localhost:8000/tools/call", json=data)
|
||||
print(json.dumps(response.json(), indent=2))
|
||||
```
|
||||
|
||||
### Integration with AI Applications
|
||||
|
||||
#### Claude Desktop
|
||||
|
||||
Add to Claude Desktop configuration (`~/.config/claude/desktop_config.json` on macOS/Linux):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"vultr": {
|
||||
"command": "python",
|
||||
"args": [
|
||||
"/path/to/vultr-mcp-server/main.py"
|
||||
],
|
||||
"env": {
|
||||
"VULTR_API_KEY": "your_api_key_here"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Cursor
|
||||
|
||||
Add to Cursor MCP configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"vultr": {
|
||||
"command": "python",
|
||||
"args": [
|
||||
"/path/to/vultr-mcp-server/main.py"
|
||||
],
|
||||
"env": {
|
||||
"VULTR_API_KEY": "your_api_key_here"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
vultr-mcp-server/
|
||||
├── src/
|
||||
│ ├── vultr_mcp/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── client.py # Vultr API client
|
||||
│ │ └── server.py # MCP server implementation
|
||||
│ └── __init__.py
|
||||
├── tests/ # Test files
|
||||
├── examples/ # Example usage
|
||||
├── main.py # Entry point
|
||||
├── requirements.txt # Dependencies
|
||||
├── .env.example # Environment template
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
### Adding New Tools
|
||||
|
||||
1. Add tool definition in `server.py` in the `list_tools()` function
|
||||
2. Implement tool logic in `client.py`
|
||||
3. Add tool execution handler in `call_tool()` function in `server.py`
|
||||
|
||||
### Testing
|
||||
|
||||
Run the test suite:
|
||||
|
||||
```bash
|
||||
python -m pytest tests/
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- **API Key Protection**: Never commit `.env` file to version control
|
||||
- **Network Security**: Run server on localhost or secure network
|
||||
- **Access Control**: Implement authentication for production use
|
||||
- **Rate Limiting**: Vultr API has rate limits (see Vultr documentation)
|
||||
|
||||
## Limitations
|
||||
|
||||
- Requires valid Vultr API key with appropriate permissions
|
||||
- Rate limited by Vultr API (typically 500 requests per hour)
|
||||
- Some Vultr API endpoints not yet implemented
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Add tests
|
||||
5. Submit a pull request
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see LICENSE file for details
|
||||
|
||||
## Support
|
||||
|
||||
- Issues: [GitHub Issues](https://github.com/yourusername/vultr-mcp-server/issues)
|
||||
- Documentation: [Vultr API Docs](https://docs.vultr.com/api/)
|
||||
- MCP Protocol: [Model Context Protocol](https://modelcontextprotocol.io)
|
||||
|
||||
133
examples/basic_usage.py
Normal file
133
examples/basic_usage.py
Normal file
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Basic usage example for Vultr MCP Server
|
||||
"""
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
|
||||
# Server configuration
|
||||
SERVER_URL = "http://localhost:8000"
|
||||
|
||||
print("Vultr MCP Server - Basic Usage Example\n")
|
||||
print(f"Connecting to server at {SERVER_URL}\n")
|
||||
|
||||
def make_request(method, endpoint, data=None):
|
||||
"""Make HTTP request and handle errors"""
|
||||
url = f"{SERVER_URL}{endpoint}"
|
||||
try:
|
||||
if method == "GET":
|
||||
response = requests.get(url)
|
||||
elif method == "POST":
|
||||
response = requests.post(url, json=data)
|
||||
else:
|
||||
raise ValueError(f"Unsupported method: {method}")
|
||||
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.exceptions.ConnectionError:
|
||||
print(f"Error: Cannot connect to server at {url}")
|
||||
print("Make sure the server is running with: python main.py")
|
||||
return None
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Error: {e}")
|
||||
if hasattr(e.response, 'text'):
|
||||
print(f"Response: {e.response.text}")
|
||||
return None
|
||||
|
||||
# 1. Get server info
|
||||
print("1. Getting server information...")
|
||||
info = make_request("GET", "/")
|
||||
if info:
|
||||
print(f" Server: {info.get('name')}")
|
||||
print(f" Version: {info.get('version')}")
|
||||
print(f" Description: {info.get('description')}\n")
|
||||
|
||||
# 2. List available tools
|
||||
print("2. Listing available tools...")
|
||||
tools = make_request("GET", "/tools")
|
||||
if tools:
|
||||
print(f" Found {len(tools)} tools:")
|
||||
for tool in tools[:5]: # Show first 5 tools
|
||||
print(f" - {tool['name']}: {tool['description']}")
|
||||
if len(tools) > 5:
|
||||
print(f" ... and {len(tools) - 5} more tools\n")
|
||||
else:
|
||||
print()
|
||||
|
||||
# 3. List available resources
|
||||
print("3. Listing available resources...")
|
||||
resources = make_request("GET", "/resources")
|
||||
if resources:
|
||||
print(f" Found {len(resources)} resources:")
|
||||
for resource in resources:
|
||||
print(f" - {resource['name']}: {resource['uri']}")
|
||||
print()
|
||||
|
||||
# 4. List available prompts
|
||||
print("4. Listing available prompts...")
|
||||
prompts = make_request("GET", "/prompts")
|
||||
if prompts:
|
||||
print(f" Found {len(prompts)} prompts:")
|
||||
for prompt in prompts:
|
||||
print(f" - {prompt['name']}: {prompt['description']}")
|
||||
print()
|
||||
|
||||
# 5. Try to get account info (requires API key)
|
||||
print("5. Trying to get account info...")
|
||||
account_info = make_request("POST", "/tools/call", {
|
||||
"name": "get_account_info",
|
||||
"arguments": {}
|
||||
})
|
||||
if account_info:
|
||||
if account_info.get("isError"):
|
||||
print(" Error: Vultr API key not configured or invalid")
|
||||
print(" To fix this, add your VULTR_API_KEY to .env file\n")
|
||||
else:
|
||||
print(" Account info retrieved successfully!")
|
||||
# Pretty print the result
|
||||
content = account_info.get("content", [])
|
||||
if content and len(content) > 0:
|
||||
try:
|
||||
data = json.loads(content[0].get("text", "{}"))
|
||||
print(f" Account: {data.get('name', 'N/A')}")
|
||||
print(f" Email: {data.get('email', 'N/A')}")
|
||||
print(f" Balance: ${data.get('balance', 'N/A')}")
|
||||
except:
|
||||
print(f" Response: {content[0].get('text', 'No data')}")
|
||||
print()
|
||||
|
||||
# 6. List regions (doesn't require API key for some endpoints)
|
||||
print("6. Trying to list available regions...")
|
||||
regions = make_request("POST", "/tools/call", {
|
||||
"name": "list_regions",
|
||||
"arguments": {}
|
||||
})
|
||||
if regions:
|
||||
if regions.get("isError"):
|
||||
print(" Error: Could not list regions")
|
||||
error_text = regions.get("content", [{}])[0].get("text", "Unknown error")
|
||||
print(f" Details: {error_text}")
|
||||
else:
|
||||
content = regions.get("content", [])
|
||||
if content and len(content) > 0:
|
||||
try:
|
||||
regions_data = json.loads(content[0].get("text", "[]"))
|
||||
if isinstance(regions_data, list):
|
||||
print(f" Found {len(regions_data)} regions:")
|
||||
for region in regions_data[:3]: # Show first 3
|
||||
if isinstance(region, dict):
|
||||
print(f" - {region.get('city', 'N/A')}, {region.get('country', 'N/A')} ({region.get('id', 'N/A')})")
|
||||
if len(regions_data) > 3:
|
||||
print(f" ... and {len(regions_data) - 3} more regions")
|
||||
except:
|
||||
print(f" Response: {content[0].get('text', 'No data')}")
|
||||
print()
|
||||
|
||||
print("\nExample completed!")
|
||||
print("\nNext steps:")
|
||||
print("1. Add your VULTR_API_KEY to .env file")
|
||||
print("2. Start the server: python main.py")
|
||||
print("3. Run this example again to see full functionality")
|
||||
print("\nFor more examples, check the examples/ directory")
|
||||
|
||||
90
final_dns_test.py
Normal file
90
final_dns_test.py
Normal file
@@ -0,0 +1,90 @@
|
||||
import json
|
||||
|
||||
print("=== Testing DNS Functionality in Vultr MCP Server ===\n")
|
||||
|
||||
# Read server.py
|
||||
with open('src/vultr_mcp/server.py', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# 1. Check DNS tools in tools list
|
||||
tools_section_start = content.find('@app.get("/tools")')
|
||||
tools_section_end = content.find('return tools', tools_section_start)
|
||||
tools_section = content[tools_section_start:tools_section_end]
|
||||
|
||||
dns_tools_in_list = []
|
||||
for line in tools_section.split('\n'):
|
||||
if 'name="list_dns_domains"' in line or \
|
||||
'name="create_dns_domain"' in line or \
|
||||
'name="delete_dns_domain"' in line or \
|
||||
'name="get_dns_domain"' in line or \
|
||||
'name="list_dns_records"' in line or \
|
||||
'name="create_dns_record"' in line or \
|
||||
'name="update_dns_record"' in line or \
|
||||
'name="delete_dns_record"' in line:
|
||||
# Extract tool name
|
||||
if 'name=' in line:
|
||||
name = line.split('name=')[1].split('"')[1]
|
||||
dns_tools_in_list.append(name)
|
||||
|
||||
print(f"1. DNS Tools in /tools endpoint: {len(dns_tools_in_list)} tools")
|
||||
for tool in sorted(dns_tools_in_list):
|
||||
print(f" ✓ {tool}")
|
||||
|
||||
# 2. Check DNS tool handling in call_tool function
|
||||
dns_tools_in_handler = []
|
||||
lines = content.split('\n')
|
||||
for i, line in enumerate(lines):
|
||||
if 'elif request.name ==' in line and 'dns' in line.lower():
|
||||
tool_name = line.split('"')[1]
|
||||
dns_tools_in_handler.append(tool_name)
|
||||
|
||||
print(f"\n2. DNS Tools in call_tool handler: {len(dns_tools_in_handler)} tools")
|
||||
for tool in sorted(dns_tools_in_handler):
|
||||
print(f" ✓ {tool}")
|
||||
|
||||
# 3. Check DNS resources
|
||||
dns_resources = []
|
||||
for line in lines:
|
||||
if 'uri="vultr://dns' in line:
|
||||
uri = line.split('uri="')[1].split('"')[0]
|
||||
dns_resources.append(uri)
|
||||
|
||||
print(f"\n3. DNS Resources in /resources endpoint: {len(dns_resources)} resources")
|
||||
for resource in sorted(dns_resources):
|
||||
print(f" ✓ {resource}")
|
||||
|
||||
# 4. Check DNS prompts
|
||||
dns_prompts = []
|
||||
for line in lines:
|
||||
if 'name="manage_dns"' in line:
|
||||
dns_prompts.append('manage_dns')
|
||||
|
||||
print(f"\n4. DNS Prompts in /prompts endpoint: {len(dns_prompts)} prompts")
|
||||
for prompt in dns_prompts:
|
||||
print(f" ✓ {prompt}")
|
||||
|
||||
# 5. Verify all DNS tools are properly connected
|
||||
print("\n5. Verification Summary:")
|
||||
all_dns_tools = [
|
||||
'list_dns_domains',
|
||||
'create_dns_domain',
|
||||
'delete_dns_domain',
|
||||
'get_dns_domain',
|
||||
'list_dns_records',
|
||||
'create_dns_record',
|
||||
'update_dns_record',
|
||||
'delete_dns_record'
|
||||
]
|
||||
|
||||
missing_in_list = [t for t in all_dns_tools if t not in dns_tools_in_list]
|
||||
missing_in_handler = [t for t in all_dns_tools if t not in dns_tools_in_handler]
|
||||
|
||||
if not missing_in_list and not missing_in_handler:
|
||||
print(" ✓ All 8 DNS tools are properly implemented!")
|
||||
else:
|
||||
if missing_in_list:
|
||||
print(f" ✗ Missing in tools list: {missing_in_list}")
|
||||
if missing_in_handler:
|
||||
print(f" ✗ Missing in call_tool handler: {missing_in_handler}")
|
||||
|
||||
print("\n=== DNS Functionality Test Complete ===")
|
||||
205
fix_dns_tools.py
Normal file
205
fix_dns_tools.py
Normal file
@@ -0,0 +1,205 @@
|
||||
import re
|
||||
|
||||
# Read the server.py file
|
||||
with open('src/vultr_mcp/server.py', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Find the tools list section
|
||||
# Look for the list_os tool definition and insert DNS tools before the return
|
||||
list_os_tool_pattern = r"(Tool\(\s*name=\"list_os\",\s*description=\"List available Vultr operating systems\",\s*inputSchema=\{"type": "object", "properties": \{\}\s*\}\s*\)\s*,?\s*)"
|
||||
match = re.search(list_os_tool_pattern, content, re.DOTALL)
|
||||
|
||||
if match:
|
||||
list_os_tool = match.group(1)
|
||||
|
||||
# DNS tools definitions (from lines 512-651)
|
||||
dns_tools = ''' # DNS Management Tools
|
||||
Tool(
|
||||
name="list_dns_domains",
|
||||
description="List all DNS domains",
|
||||
inputSchema={"type": "object", "properties": {}}
|
||||
),
|
||||
Tool(
|
||||
name="create_dns_domain",
|
||||
description="Create a new DNS domain",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "Domain name (e.g., 'example.com')"
|
||||
},
|
||||
"ip": {
|
||||
"type": "string",
|
||||
"description": "IP address for the domain"
|
||||
}
|
||||
},
|
||||
"required": ["domain", "ip"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="delete_dns_domain",
|
||||
description="Delete a DNS domain",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "Domain name"
|
||||
}
|
||||
},
|
||||
"required": ["domain"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="get_dns_domain",
|
||||
description="Get DNS domain details",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "Domain name"
|
||||
}
|
||||
},
|
||||
"required": ["domain"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="list_dns_records",
|
||||
description="List DNS records for a domain",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "Domain name"
|
||||
}
|
||||
},
|
||||
"required": ["domain"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="create_dns_record",
|
||||
description="Create a new DNS record",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "Domain name"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Record type (A, AAAA, CNAME, MX, TXT, etc.)"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Record name (e.g., 'www', '@', 'mail')"
|
||||
},
|
||||
"data": {
|
||||
"type": "string",
|
||||
"description": "Record data (IP address, hostname, etc.)"
|
||||
},
|
||||
"ttl": {
|
||||
"type": "integer",
|
||||
"description": "Time to live in seconds",
|
||||
"default": 300
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer",
|
||||
"description": "Priority for MX records",
|
||||
"default": 0
|
||||
}
|
||||
},
|
||||
"required": ["domain", "type", "name", "data"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="update_dns_record",
|
||||
description="Update a DNS record",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "Domain name"
|
||||
},
|
||||
"record_id": {
|
||||
"type": "string",
|
||||
"description": "Record ID"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Record type (A, AAAA, CNAME, MX, TXT, etc.)"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Record name"
|
||||
},
|
||||
"data": {
|
||||
"type": "string",
|
||||
"description": "Record data"
|
||||
},
|
||||
"ttl": {
|
||||
"type": "integer",
|
||||
"description": "Time to live in seconds"
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer",
|
||||
"description": "Priority for MX records"
|
||||
}
|
||||
},
|
||||
"required": ["domain", "record_id"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="delete_dns_record",
|
||||
description="Delete a DNS record",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "Domain name"
|
||||
},
|
||||
"record_id": {
|
||||
"type": "string",
|
||||
"description": "Record ID"
|
||||
}
|
||||
},
|
||||
"required": ["domain", "record_id"]
|
||||
}
|
||||
),
|
||||
'''
|
||||
|
||||
# Insert DNS tools after list_os tool
|
||||
new_content = content.replace(list_os_tool, list_os_tool + dns_tools)
|
||||
|
||||
# Remove the duplicate DNS tools definitions (lines 512-651)
|
||||
# Find the start of duplicate DNS tools
|
||||
duplicate_start = new_content.find('# DNS Management Tools')
|
||||
if duplicate_start != -1:
|
||||
# Find the end of the duplicate section (look for next Tool definition or end of file)
|
||||
duplicate_end = duplicate_start
|
||||
lines = new_content[duplicate_start:].split('\n')
|
||||
in_tool = False
|
||||
for i, line in enumerate(lines):
|
||||
if 'Tool(' in line:
|
||||
in_tool = True
|
||||
if in_tool and line.strip().endswith(')'):
|
||||
in_tool = False
|
||||
if i > 100 and not in_tool and line.strip() and not line.strip().startswith('Tool('):
|
||||
duplicate_end = duplicate_start + sum(len(l) + 1 for l in lines[:i])
|
||||
break
|
||||
|
||||
if duplicate_end > duplicate_start:
|
||||
new_content = new_content[:duplicate_start] + new_content[duplicate_end:]
|
||||
|
||||
# Write the fixed content
|
||||
with open('src/vultr_mcp/server.py', 'w') as f:
|
||||
f.write(new_content)
|
||||
|
||||
print("DNS tools have been moved to the correct location in list_tools function")
|
||||
else:
|
||||
print("Could not find list_os tool definition")
|
||||
215
insert_dns_tools.py
Normal file
215
insert_dns_tools.py
Normal file
@@ -0,0 +1,215 @@
|
||||
import re
|
||||
|
||||
# Read the file
|
||||
with open('src/vultr_mcp/server.py', 'r') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
# Find the line with 'return tools'
|
||||
insert_line = -1
|
||||
for i, line in enumerate(lines):
|
||||
if 'return tools' in line:
|
||||
insert_line = i
|
||||
break
|
||||
|
||||
if insert_line == -1:
|
||||
print("Error: Could not find 'return tools'")
|
||||
exit(1)
|
||||
|
||||
# Find the list_os tool (should be a few lines before return tools)
|
||||
list_os_line = -1
|
||||
for i in range(insert_line-1, max(0, insert_line-10), -1):
|
||||
if 'list_os' in lines[i]:
|
||||
list_os_line = i
|
||||
break
|
||||
|
||||
if list_os_line == -1:
|
||||
print("Error: Could not find 'list_os' tool")
|
||||
exit(1)
|
||||
|
||||
print(f"Found list_os at line {list_os_line+1}")
|
||||
print(f"Found return tools at line {insert_line+1}")
|
||||
|
||||
# Find the end of the list_os tool definition
|
||||
# Look for the closing parenthesis and comma
|
||||
end_of_list_os = -1
|
||||
for i in range(list_os_line, min(len(lines), list_os_line + 10)):
|
||||
if '),' in lines[i] or ')' in lines[i] and i < insert_line:
|
||||
end_of_list_os = i
|
||||
break
|
||||
|
||||
if end_of_list_os == -1:
|
||||
print("Error: Could not find end of list_os tool")
|
||||
exit(1)
|
||||
|
||||
print(f"End of list_os tool at line {end_of_list_os+1}")
|
||||
|
||||
# DNS tools to insert
|
||||
dns_tools = ''' ),
|
||||
# DNS Management Tools
|
||||
Tool(
|
||||
name="list_dns_domains",
|
||||
description="List all DNS domains",
|
||||
inputSchema={"type": "object", "properties": {}}
|
||||
),
|
||||
Tool(
|
||||
name="create_dns_domain",
|
||||
description="Create a new DNS domain",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "Domain name (e.g., 'example.com')"
|
||||
},
|
||||
"ip": {
|
||||
"type": "string",
|
||||
"description": "IP address for the domain"
|
||||
}
|
||||
},
|
||||
"required": ["domain", "ip"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="delete_dns_domain",
|
||||
description="Delete a DNS domain",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "Domain name"
|
||||
}
|
||||
},
|
||||
"required": ["domain"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="get_dns_domain",
|
||||
description="Get DNS domain details",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "Domain name"
|
||||
}
|
||||
},
|
||||
"required": ["domain"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="list_dns_records",
|
||||
description="List DNS records for a domain",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "Domain name"
|
||||
}
|
||||
},
|
||||
"required": ["domain"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="create_dns_record",
|
||||
description="Create a new DNS record",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "Domain name"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Record type (A, AAAA, CNAME, MX, TXT, etc.)"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Record name (e.g., 'www', '@', 'mail')"
|
||||
},
|
||||
"data": {
|
||||
"type": "string",
|
||||
"description": "Record data (IP address, hostname, etc.)"
|
||||
},
|
||||
"ttl": {
|
||||
"type": "integer",
|
||||
"description": "Time to live in seconds",
|
||||
"default": 300
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer",
|
||||
"description": "Priority for MX records",
|
||||
"default": 0
|
||||
}
|
||||
},
|
||||
"required": ["domain", "type", "name", "data"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="update_dns_record",
|
||||
description="Update a DNS record",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "Domain name"
|
||||
},
|
||||
"record_id": {
|
||||
"type": "string",
|
||||
"description": "Record ID"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Record type (A, AAAA, CNAME, MX, TXT, etc.)"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Record name"
|
||||
},
|
||||
"data": {
|
||||
"type": "string",
|
||||
"description": "Record data"
|
||||
},
|
||||
"ttl": {
|
||||
"type": "integer",
|
||||
"description": "Time to live in seconds"
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer",
|
||||
"description": "Priority for MX records"
|
||||
}
|
||||
},
|
||||
"required": ["domain", "record_id"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="delete_dns_record",
|
||||
description="Delete a DNS record",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "Domain name"
|
||||
},
|
||||
"record_id": {
|
||||
"type": "string",
|
||||
"description": "Record ID"
|
||||
}
|
||||
},
|
||||
"required": ["domain", "record_id"]
|
||||
}
|
||||
)'''
|
||||
|
||||
# Insert the DNS tools after the list_os tool
|
||||
new_lines = lines[:end_of_list_os+1] + [dns_tools + '\n'] + lines[end_of_list_os+1:]
|
||||
|
||||
# Write the updated file
|
||||
with open('src/vultr_mcp/server.py', 'w') as f:
|
||||
f.writelines(new_lines)
|
||||
|
||||
print("DNS tools have been successfully inserted into the tools list!")
|
||||
print(f"Total lines in file: {len(new_lines)}")
|
||||
29
main.py
Normal file
29
main.py
Normal file
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Vultr MCP Server - Main Entry Point
|
||||
"""
|
||||
import uvicorn
|
||||
import logging
|
||||
from src.vultr_mcp.server import app
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if __name__ == "__main__":
|
||||
logger.info("Starting Vultr MCP Server...")
|
||||
logger.info("Server will be available at http://0.0.0.0:8000")
|
||||
logger.info("MCP endpoints:")
|
||||
logger.info(" GET / - Server info")
|
||||
logger.info(" GET /tools - List available tools")
|
||||
logger.info(" GET /resources - List available resources")
|
||||
logger.info(" GET /prompts - List available prompts")
|
||||
logger.info(" POST /tools/call - Execute a tool")
|
||||
logger.info(" GET /resources/{uri} - Get resource content")
|
||||
|
||||
uvicorn.run(
|
||||
app,
|
||||
host="0.0.0.0",
|
||||
port=8000,
|
||||
log_level="info"
|
||||
)
|
||||
|
||||
6
requirements.txt
Normal file
6
requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
vultr-python-client>=1.0.0
|
||||
mcp>=1.0.0
|
||||
python-dotenv>=1.0.0
|
||||
fastapi>=0.104.0
|
||||
uvicorn>=0.24.0
|
||||
httpx>=0.25.0
|
||||
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
0
src/vultr_mcp/__init__.py
Normal file
0
src/vultr_mcp/__init__.py
Normal file
309
src/vultr_mcp/client.py
Normal file
309
src/vultr_mcp/client.py
Normal file
@@ -0,0 +1,309 @@
|
||||
"""
|
||||
Vultr API Client for MCP Server
|
||||
"""
|
||||
import os
|
||||
from typing import Dict, List, Any, Optional
|
||||
import vultr_python_client
|
||||
from vultr_python_client import ApiClient, Configuration
|
||||
from vultr_python_client.apis.tag_to_api import (
|
||||
InstancesApi, AccountApi, BaremetalApi, BlockApi,
|
||||
DnsApi, FirewallApi, KubernetesApi, LoadBalancerApi,
|
||||
S3Api, ReservedIpApi, SnapshotApi, SshApi,
|
||||
StartupApi, UsersApi, VPCsApi
|
||||
)
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
class VultrClient:
|
||||
"""Client for interacting with Vultr API"""
|
||||
|
||||
def __init__(self, api_key: Optional[str] = None):
|
||||
"""Initialize Vultr client with API key"""
|
||||
self.api_key = api_key or os.getenv('VULTR_API_KEY')
|
||||
if not self.api_key:
|
||||
raise ValueError("VULTR_API_KEY not found in environment variables")
|
||||
|
||||
# Configure API client
|
||||
configuration = Configuration()
|
||||
configuration.api_key['Authorization'] = self.api_key
|
||||
configuration.api_key_prefix['Authorization'] = 'Bearer'
|
||||
|
||||
self.api_client = ApiClient(configuration)
|
||||
|
||||
# Initialize API instances
|
||||
self.instances = InstancesApi(self.api_client)
|
||||
self.account = AccountApi(self.api_client)
|
||||
self.bare_metal = BaremetalApi(self.api_client)
|
||||
self.block_storage = BlockApi(self.api_client)
|
||||
self.dns = DnsApi(self.api_client)
|
||||
self.firewall = FirewallApi(self.api_client)
|
||||
self.kubernetes = KubernetesApi(self.api_client)
|
||||
self.load_balancer = LoadBalancerApi(self.api_client)
|
||||
self.object_storage = S3Api(self.api_client)
|
||||
self.reserved_ip = ReservedIpApi(self.api_client)
|
||||
self.snapshot = SnapshotApi(self.api_client)
|
||||
self.ssh_key = SshApi(self.api_client)
|
||||
self.startup_script = StartupApi(self.api_client)
|
||||
self.user = UsersApi(self.api_client)
|
||||
self.vpc = VPCsApi(self.api_client)
|
||||
|
||||
# Account methods
|
||||
def get_account_info(self) -> Dict[str, Any]:
|
||||
"""Get account information"""
|
||||
try:
|
||||
account = self.account.get_account()
|
||||
return {
|
||||
'name': account.account.name,
|
||||
'email': account.account.email,
|
||||
'balance': account.account.balance,
|
||||
'pending_charges': account.account.pending_charges,
|
||||
}
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
|
||||
# Instance methods
|
||||
def list_instances(self, per_page: int = 100) -> List[Dict[str, Any]]:
|
||||
"""List all instances"""
|
||||
try:
|
||||
instances = self.instances.list_instances(per_page=per_page)
|
||||
result = []
|
||||
for instance in instances.instances:
|
||||
result.append({
|
||||
'id': instance.id,
|
||||
'label': instance.label,
|
||||
'region': instance.region,
|
||||
'plan': instance.plan,
|
||||
'status': instance.status,
|
||||
'power_status': instance.power_status,
|
||||
'os': instance.os,
|
||||
'ram': instance.ram,
|
||||
'disk': instance.disk,
|
||||
'main_ip': instance.main_ip,
|
||||
'created': instance.date_created,
|
||||
})
|
||||
return result
|
||||
except Exception as e:
|
||||
return [{'error': str(e)}]
|
||||
|
||||
def create_instance(self, **kwargs) -> Dict[str, Any]:
|
||||
"""Create a new instance"""
|
||||
try:
|
||||
# Required parameters: region, plan, os_id
|
||||
instance = self.instances.create_instance(**kwargs)
|
||||
return {
|
||||
'id': instance.instance.id,
|
||||
'message': 'Instance created successfully',
|
||||
'details': instance.instance.to_dict()
|
||||
}
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
|
||||
def get_instance(self, instance_id: str) -> Dict[str, Any]:
|
||||
"""Get instance details"""
|
||||
try:
|
||||
instance = self.instances.get_instance(instance_id)
|
||||
return instance.instance.to_dict()
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
|
||||
def delete_instance(self, instance_id: str) -> Dict[str, Any]:
|
||||
"""Delete an instance"""
|
||||
try:
|
||||
self.instances.delete_instance(instance_id)
|
||||
return {'message': f'Instance {instance_id} deleted successfully'}
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
|
||||
def start_instance(self, instance_id: str) -> Dict[str, Any]:
|
||||
"""Start an instance"""
|
||||
try:
|
||||
self.instances.start_instance(instance_id)
|
||||
return {'message': f'Instance {instance_id} started successfully'}
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
|
||||
def stop_instance(self, instance_id: str) -> Dict[str, Any]:
|
||||
"""Stop an instance"""
|
||||
try:
|
||||
self.instances.stop_instance(instance_id)
|
||||
return {'message': f'Instance {instance_id} stopped successfully'}
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
|
||||
def reboot_instance(self, instance_id: str) -> Dict[str, Any]:
|
||||
"""Reboot an instance"""
|
||||
try:
|
||||
self.instances.reboot_instance(instance_id)
|
||||
return {'message': f'Instance {instance_id} rebooted successfully'}
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
|
||||
# Resource discovery methods
|
||||
def list_regions(self) -> List[Dict[str, Any]]:
|
||||
"""List available regions"""
|
||||
try:
|
||||
regions = self.instances.list_regions()
|
||||
result = []
|
||||
for region in regions.regions:
|
||||
result.append({
|
||||
'id': region.id,
|
||||
'city': region.city,
|
||||
'country': region.country,
|
||||
'continent': region.continent,
|
||||
'options': region.options,
|
||||
})
|
||||
return result
|
||||
except Exception as e:
|
||||
return [{'error': str(e)}]
|
||||
|
||||
def list_plans(self) -> List[Dict[str, Any]]:
|
||||
"""List available plans"""
|
||||
try:
|
||||
plans = self.instances.list_plans()
|
||||
result = []
|
||||
for plan in plans.plans:
|
||||
result.append({
|
||||
'id': plan.id,
|
||||
'vcpu_count': plan.vcpu_count,
|
||||
'ram': plan.ram,
|
||||
'disk': plan.disk,
|
||||
'disk_count': plan.disk_count,
|
||||
'bandwidth': plan.bandwidth,
|
||||
'monthly_cost': plan.monthly_cost,
|
||||
'type': plan.type,
|
||||
})
|
||||
return result
|
||||
except Exception as e:
|
||||
return [{'error': str(e)}]
|
||||
|
||||
def list_os(self) -> List[Dict[str, Any]]:
|
||||
"""List available operating systems"""
|
||||
try:
|
||||
os_list = self.instances.list_os()
|
||||
result = []
|
||||
for os_item in os_list.os:
|
||||
result.append({
|
||||
'id': os_item.id,
|
||||
'name': os_item.name,
|
||||
'arch': os_item.arch,
|
||||
'family': os_item.family,
|
||||
})
|
||||
return result
|
||||
except Exception as e:
|
||||
return [{'error': str(e)}]
|
||||
|
||||
# DNS Management Methods
|
||||
def list_dns_domains(self) -> List[Dict[str, Any]]:
|
||||
"""List all DNS domains"""
|
||||
try:
|
||||
domains = self.dns.list_dns_domains()
|
||||
result = []
|
||||
for domain in domains.domains:
|
||||
result.append({
|
||||
'domain': domain.domain,
|
||||
'date_created': domain.date_created,
|
||||
})
|
||||
return result
|
||||
except Exception as e:
|
||||
return [{'error': str(e)}]
|
||||
|
||||
def create_dns_domain(self, domain: str, ip: str) -> Dict[str, Any]:
|
||||
"""Create a new DNS domain"""
|
||||
try:
|
||||
result = self.dns.create_dns_domain({
|
||||
'domain': domain,
|
||||
'ip': ip
|
||||
})
|
||||
return {
|
||||
'message': f'DNS domain {domain} created successfully',
|
||||
'domain': domain,
|
||||
'ip': ip
|
||||
}
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
|
||||
def delete_dns_domain(self, domain: str) -> Dict[str, Any]:
|
||||
"""Delete a DNS domain"""
|
||||
try:
|
||||
self.dns.delete_dns_domain(domain)
|
||||
return {'message': f'DNS domain {domain} deleted successfully'}
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
|
||||
def get_dns_domain(self, domain: str) -> Dict[str, Any]:
|
||||
"""Get DNS domain details"""
|
||||
try:
|
||||
domain_info = self.dns.get_dns_domain(domain)
|
||||
return {
|
||||
'domain': domain_info.dns_domain.domain,
|
||||
'date_created': domain_info.dns_domain.date_created,
|
||||
}
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
|
||||
def list_dns_records(self, domain: str) -> List[Dict[str, Any]]:
|
||||
"""List DNS records for a domain"""
|
||||
try:
|
||||
records = self.dns.list_dns_domain_records(domain)
|
||||
result = []
|
||||
for record in records.records:
|
||||
result.append({
|
||||
'id': record.id,
|
||||
'type': record.type,
|
||||
'name': record.name,
|
||||
'data': record.data,
|
||||
'priority': record.priority,
|
||||
'ttl': record.ttl,
|
||||
})
|
||||
return result
|
||||
except Exception as e:
|
||||
return [{'error': str(e)}]
|
||||
|
||||
def create_dns_record(self, domain: str, record_type: str, name: str, data: str,
|
||||
ttl: int = 300, priority: int = 0) -> Dict[str, Any]:
|
||||
"""Create a new DNS record"""
|
||||
try:
|
||||
result = self.dns.create_dns_domain_record(domain, {
|
||||
'type': record_type,
|
||||
'name': name,
|
||||
'data': data,
|
||||
'ttl': ttl,
|
||||
'priority': priority
|
||||
})
|
||||
return {
|
||||
'message': f'DNS record created successfully',
|
||||
'record_id': result.record.id,
|
||||
'domain': domain,
|
||||
'type': record_type,
|
||||
'name': name,
|
||||
'data': data
|
||||
}
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
|
||||
def update_dns_record(self, domain: str, record_id: str, **kwargs) -> Dict[str, Any]:
|
||||
"""Update a DNS record"""
|
||||
try:
|
||||
self.dns.update_dns_domain_record(domain, record_id, kwargs)
|
||||
return {
|
||||
'message': f'DNS record {record_id} updated successfully',
|
||||
'domain': domain,
|
||||
'record_id': record_id
|
||||
}
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
|
||||
def delete_dns_record(self, domain: str, record_id: str) -> Dict[str, Any]:
|
||||
"""Delete a DNS record"""
|
||||
try:
|
||||
self.dns.delete_dns_domain_record(domain, record_id)
|
||||
return {
|
||||
'message': f'DNS record {record_id} deleted successfully',
|
||||
'domain': domain,
|
||||
'record_id': record_id
|
||||
}
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
|
||||
827
src/vultr_mcp/server.py
Normal file
827
src/vultr_mcp/server.py
Normal file
@@ -0,0 +1,827 @@
|
||||
"""
|
||||
MCP Server for Vultr Cloud Services
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from typing import Dict, List, Any, Optional
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from .client import VultrClient
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = FastAPI(title="Vultr MCP Server", description="MCP Server for Vultr Cloud Services")
|
||||
|
||||
# Initialize Vultr client
|
||||
vultr_client = None
|
||||
|
||||
try:
|
||||
vultr_client = VultrClient()
|
||||
except ValueError as e:
|
||||
logger.warning(f"Vultr client initialization failed: {e}")
|
||||
logger.warning("Server will start but Vultr operations will fail until API key is configured")
|
||||
|
||||
|
||||
# MCP Protocol Models
|
||||
class Tool(BaseModel):
|
||||
"""MCP Tool definition"""
|
||||
name: str
|
||||
description: str
|
||||
inputSchema: Dict[str, Any]
|
||||
|
||||
|
||||
class Resource(BaseModel):
|
||||
"""MCP Resource definition"""
|
||||
uri: str
|
||||
name: str
|
||||
description: str
|
||||
mimeType: str = "text/plain"
|
||||
|
||||
|
||||
class Prompt(BaseModel):
|
||||
"""MCP Prompt definition"""
|
||||
name: str
|
||||
description: str
|
||||
arguments: List[Dict[str, Any]] = []
|
||||
|
||||
|
||||
# Request/Response Models
|
||||
class ToolCallRequest(BaseModel):
|
||||
"""Request for tool execution"""
|
||||
name: str
|
||||
arguments: Dict[str, Any] = {}
|
||||
|
||||
|
||||
class ToolCallResponse(BaseModel):
|
||||
"""Response from tool execution"""
|
||||
content: List[Dict[str, Any]]
|
||||
isError: bool = False
|
||||
|
||||
|
||||
# MCP Protocol Endpoints
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""Root endpoint"""
|
||||
return {
|
||||
"name": "Vultr MCP Server",
|
||||
"version": "1.0.0",
|
||||
"description": "Model Context Protocol Server for Vultr Cloud Services"
|
||||
}
|
||||
|
||||
|
||||
@app.get("/tools")
|
||||
async def list_tools() -> List[Tool]:
|
||||
"""List available tools"""
|
||||
tools = [
|
||||
Tool(
|
||||
name="get_account_info",
|
||||
description="Get Vultr account information",
|
||||
inputSchema={"type": "object", "properties": {}}
|
||||
),
|
||||
Tool(
|
||||
name="list_instances",
|
||||
description="List all Vultr instances",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"per_page": {
|
||||
"type": "integer",
|
||||
"description": "Number of instances per page",
|
||||
"default": 100
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="get_instance",
|
||||
description="Get details of a specific instance",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"instance_id": {
|
||||
"type": "string",
|
||||
"description": "ID of the instance"
|
||||
}
|
||||
},
|
||||
"required": ["instance_id"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="create_instance",
|
||||
description="Create a new Vultr instance",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"region": {
|
||||
"type": "string",
|
||||
"description": "Region code (e.g., 'ams')"
|
||||
},
|
||||
"plan": {
|
||||
"type": "string",
|
||||
"description": "Plan ID (e.g., 'vc2-1c-1gb')"
|
||||
},
|
||||
"os_id": {
|
||||
"type": "integer",
|
||||
"description": "Operating System ID"
|
||||
},
|
||||
"label": {
|
||||
"type": "string",
|
||||
"description": "Instance label",
|
||||
"default": ""
|
||||
},
|
||||
"hostname": {
|
||||
"type": "string",
|
||||
"description": "Instance hostname",
|
||||
"default": ""
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Tags for the instance",
|
||||
"default": []
|
||||
}
|
||||
},
|
||||
"required": ["region", "plan", "os_id"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="delete_instance",
|
||||
description="Delete a Vultr instance",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"instance_id": {
|
||||
"type": "string",
|
||||
"description": "ID of the instance to delete"
|
||||
}
|
||||
},
|
||||
"required": ["instance_id"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="start_instance",
|
||||
description="Start a Vultr instance",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"instance_id": {
|
||||
"type": "string",
|
||||
"description": "ID of the instance to start"
|
||||
}
|
||||
},
|
||||
"required": ["instance_id"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="stop_instance",
|
||||
description="Stop a Vultr instance",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"instance_id": {
|
||||
"type": "string",
|
||||
"description": "ID of the instance to stop"
|
||||
}
|
||||
},
|
||||
"required": ["instance_id"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="reboot_instance",
|
||||
description="Reboot a Vultr instance",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"instance_id": {
|
||||
"type": "string",
|
||||
"description": "ID of the instance to reboot"
|
||||
}
|
||||
},
|
||||
"required": ["instance_id"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="list_regions",
|
||||
description="List available Vultr regions",
|
||||
inputSchema={"type": "object", "properties": {}}
|
||||
),
|
||||
Tool(
|
||||
name="list_plans",
|
||||
description="List available Vultr plans",
|
||||
inputSchema={"type": "object", "properties": {}}
|
||||
),
|
||||
Tool(
|
||||
name="list_os",
|
||||
description="List available Vultr operating systems",
|
||||
inputSchema={"type": "object", "properties": {}}
|
||||
),
|
||||
),
|
||||
# DNS Management Tools
|
||||
Tool(
|
||||
name="list_dns_domains",
|
||||
description="List all DNS domains",
|
||||
inputSchema={"type": "object", "properties": {}}
|
||||
),
|
||||
Tool(
|
||||
name="create_dns_domain",
|
||||
description="Create a new DNS domain",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "Domain name (e.g., 'example.com')"
|
||||
},
|
||||
"ip": {
|
||||
"type": "string",
|
||||
"description": "IP address for the domain"
|
||||
}
|
||||
},
|
||||
"required": ["domain", "ip"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="delete_dns_domain",
|
||||
description="Delete a DNS domain",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "Domain name"
|
||||
}
|
||||
},
|
||||
"required": ["domain"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="get_dns_domain",
|
||||
description="Get DNS domain details",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "Domain name"
|
||||
}
|
||||
},
|
||||
"required": ["domain"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="list_dns_records",
|
||||
description="List DNS records for a domain",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "Domain name"
|
||||
}
|
||||
},
|
||||
"required": ["domain"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="create_dns_record",
|
||||
description="Create a new DNS record",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "Domain name"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Record type (A, AAAA, CNAME, MX, TXT, etc.)"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Record name (e.g., 'www', '@', 'mail')"
|
||||
},
|
||||
"data": {
|
||||
"type": "string",
|
||||
"description": "Record data (IP address, hostname, etc.)"
|
||||
},
|
||||
"ttl": {
|
||||
"type": "integer",
|
||||
"description": "Time to live in seconds",
|
||||
"default": 300
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer",
|
||||
"description": "Priority for MX records",
|
||||
"default": 0
|
||||
}
|
||||
},
|
||||
"required": ["domain", "type", "name", "data"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="update_dns_record",
|
||||
description="Update a DNS record",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "Domain name"
|
||||
},
|
||||
"record_id": {
|
||||
"type": "string",
|
||||
"description": "Record ID"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Record type (A, AAAA, CNAME, MX, TXT, etc.)"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Record name"
|
||||
},
|
||||
"data": {
|
||||
"type": "string",
|
||||
"description": "Record data"
|
||||
},
|
||||
"ttl": {
|
||||
"type": "integer",
|
||||
"description": "Time to live in seconds"
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer",
|
||||
"description": "Priority for MX records"
|
||||
}
|
||||
},
|
||||
"required": ["domain", "record_id"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="delete_dns_record",
|
||||
description="Delete a DNS record",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "Domain name"
|
||||
},
|
||||
"record_id": {
|
||||
"type": "string",
|
||||
"description": "Record ID"
|
||||
}
|
||||
},
|
||||
"required": ["domain", "record_id"]
|
||||
}
|
||||
)
|
||||
]
|
||||
return tools
|
||||
|
||||
|
||||
@app.get("/resources")
|
||||
async def list_resources() -> List[Resource]:
|
||||
"""List available resources"""
|
||||
resources = [
|
||||
Resource(
|
||||
uri="vultr://account/info",
|
||||
name="Vultr Account Info",
|
||||
description="Vultr account information"
|
||||
),
|
||||
Resource(
|
||||
uri="vultr://instances/list",
|
||||
name="Vultr Instances",
|
||||
description="List of Vultr instances"
|
||||
),
|
||||
Resource(
|
||||
uri="vultr://regions/list",
|
||||
name="Vultr Regions",
|
||||
description="List of available Vultr regions"
|
||||
),
|
||||
Resource(
|
||||
uri="vultr://plans/list",
|
||||
name="Vultr Plans",
|
||||
description="List of available Vultr plans"
|
||||
),
|
||||
Resource(
|
||||
uri="vultr://os/list",
|
||||
name="Vultr Operating Systems",
|
||||
description="List of available Vultr operating systems"
|
||||
),
|
||||
Resource(
|
||||
uri="vultr://dns/domains",
|
||||
name="Vultr DNS Domains",
|
||||
description="List of DNS domains"
|
||||
),
|
||||
Resource(
|
||||
uri="vultr://dns/records",
|
||||
name="Vultr DNS Records",
|
||||
description="DNS records for a domain"
|
||||
),
|
||||
]
|
||||
return resources
|
||||
|
||||
|
||||
@app.get("/prompts")
|
||||
async def list_prompts() -> List[Prompt]:
|
||||
"""List available prompts"""
|
||||
prompts = [
|
||||
Prompt(
|
||||
name="deploy_web_server",
|
||||
description="Deploy a web server on Vultr",
|
||||
arguments=[
|
||||
{"name": "region", "description": "Region to deploy in", "required": True},
|
||||
{"name": "plan", "description": "Server plan", "required": True},
|
||||
{"name": "domain", "description": "Domain name", "required": False},
|
||||
]
|
||||
),
|
||||
Prompt(
|
||||
name="manage_instances",
|
||||
description="Manage Vultr instances",
|
||||
arguments=[
|
||||
{"name": "action", "description": "Action to perform (start, stop, reboot, delete)", "required": True},
|
||||
{"name": "instance_id", "description": "Instance ID", "required": True},
|
||||
]
|
||||
),
|
||||
Prompt(
|
||||
name="monitor_resources",
|
||||
description="Monitor Vultr resources",
|
||||
arguments=[
|
||||
{"name": "resource_type", "description": "Type of resource to monitor", "required": False},
|
||||
]
|
||||
),
|
||||
]
|
||||
return prompts
|
||||
|
||||
|
||||
@app.post("/tools/call")
|
||||
async def call_tool(request: ToolCallRequest) -> ToolCallResponse:
|
||||
"""Execute a tool"""
|
||||
if not vultr_client:
|
||||
return ToolCallResponse(
|
||||
content=[{"type": "text", "text": "Vultr client not initialized. Please set VULTR_API_KEY environment variable."}],
|
||||
isError=True
|
||||
)
|
||||
|
||||
try:
|
||||
result = None
|
||||
|
||||
if request.name == "get_account_info":
|
||||
result = vultr_client.get_account_info()
|
||||
|
||||
elif request.name == "list_instances":
|
||||
per_page = request.arguments.get("per_page", 100)
|
||||
result = vultr_client.list_instances(per_page=per_page)
|
||||
|
||||
elif request.name == "get_instance":
|
||||
instance_id = request.arguments.get("instance_id")
|
||||
if not instance_id:
|
||||
raise ValueError("instance_id is required")
|
||||
result = vultr_client.get_instance(instance_id)
|
||||
|
||||
elif request.name == "create_instance":
|
||||
result = vultr_client.create_instance(**request.arguments)
|
||||
|
||||
elif request.name == "delete_instance":
|
||||
instance_id = request.arguments.get("instance_id")
|
||||
if not instance_id:
|
||||
raise ValueError("instance_id is required")
|
||||
result = vultr_client.delete_instance(instance_id)
|
||||
|
||||
elif request.name == "start_instance":
|
||||
instance_id = request.arguments.get("instance_id")
|
||||
if not instance_id:
|
||||
raise ValueError("instance_id is required")
|
||||
result = vultr_client.start_instance(instance_id)
|
||||
|
||||
elif request.name == "stop_instance":
|
||||
instance_id = request.arguments.get("instance_id")
|
||||
if not instance_id:
|
||||
raise ValueError("instance_id is required")
|
||||
result = vultr_client.stop_instance(instance_id)
|
||||
|
||||
elif request.name == "reboot_instance":
|
||||
instance_id = request.arguments.get("instance_id")
|
||||
if not instance_id:
|
||||
raise ValueError("instance_id is required")
|
||||
result = vultr_client.reboot_instance(instance_id)
|
||||
|
||||
elif request.name == "list_regions":
|
||||
result = vultr_client.list_regions()
|
||||
|
||||
elif request.name == "list_plans":
|
||||
result = vultr_client.list_plans()
|
||||
|
||||
elif request.name == "list_os":
|
||||
result = vultr_client.list_os()
|
||||
|
||||
elif request.name == "list_dns_domains":
|
||||
result = vultr_client.list_dns_domains()
|
||||
|
||||
elif request.name == "create_dns_domain":
|
||||
domain = request.arguments.get("domain")
|
||||
ip = request.arguments.get("ip")
|
||||
if not domain or not ip:
|
||||
raise ValueError("domain and ip are required")
|
||||
result = vultr_client.create_dns_domain(domain, ip)
|
||||
|
||||
elif request.name == "delete_dns_domain":
|
||||
domain = request.arguments.get("domain")
|
||||
if not domain:
|
||||
raise ValueError("domain is required")
|
||||
result = vultr_client.delete_dns_domain(domain)
|
||||
|
||||
elif request.name == "get_dns_domain":
|
||||
domain = request.arguments.get("domain")
|
||||
if not domain:
|
||||
raise ValueError("domain is required")
|
||||
result = vultr_client.get_dns_domain(domain)
|
||||
|
||||
elif request.name == "list_dns_records":
|
||||
domain = request.arguments.get("domain")
|
||||
if not domain:
|
||||
raise ValueError("domain is required")
|
||||
result = vultr_client.list_dns_records(domain)
|
||||
|
||||
elif request.name == "create_dns_record":
|
||||
domain = request.arguments.get("domain")
|
||||
record_type = request.arguments.get("type")
|
||||
name = request.arguments.get("name")
|
||||
data = request.arguments.get("data")
|
||||
ttl = request.arguments.get("ttl", 300)
|
||||
priority = request.arguments.get("priority", 0)
|
||||
if not domain or not record_type or not name or not data:
|
||||
raise ValueError("domain, type, name, and data are required")
|
||||
result = vultr_client.create_dns_record(domain, record_type, name, data, ttl, priority)
|
||||
|
||||
elif request.name == "update_dns_record":
|
||||
domain = request.arguments.get("domain")
|
||||
record_id = request.arguments.get("record_id")
|
||||
if not domain or not record_id:
|
||||
raise ValueError("domain and record_id are required")
|
||||
# Filter out None values
|
||||
update_args = {k: v for k, v in request.arguments.items()
|
||||
if k not in ["domain", "record_id"] and v is not None}
|
||||
result = vultr_client.update_dns_record(domain, record_id, **update_args)
|
||||
|
||||
elif request.name == "delete_dns_record":
|
||||
domain = request.arguments.get("domain")
|
||||
record_id = request.arguments.get("record_id")
|
||||
if not domain or not record_id:
|
||||
raise ValueError("domain and record_id are required")
|
||||
result = vultr_client.delete_dns_record(domain, record_id)
|
||||
|
||||
else:
|
||||
return ToolCallResponse(
|
||||
content=[{"type": "text", "text": f"Unknown tool: {request.name}"}],
|
||||
isError=True
|
||||
)
|
||||
|
||||
# Format the result
|
||||
if isinstance(result, dict) and 'error' in result:
|
||||
return ToolCallResponse(
|
||||
content=[{"type": "text", "text": f"Error: {result['error']}"}],
|
||||
isError=True
|
||||
)
|
||||
|
||||
# Convert result to JSON string for display
|
||||
result_str = json.dumps(result, indent=2, default=str)
|
||||
return ToolCallResponse(
|
||||
content=[{"type": "text", "text": result_str}]
|
||||
)
|
||||
|
||||
Prompt(
|
||||
name="manage_dns",
|
||||
description="Manage DNS domains and records",
|
||||
arguments=[
|
||||
{"name": "action", "description": "Action to perform (create_domain, delete_domain, list_records, create_record, update_record, delete_record)", "required": True},
|
||||
{"name": "domain", "description": "Domain name", "required": True},
|
||||
{"name": "record_id", "description": "Record ID (for update/delete)", "required": False},
|
||||
{"name": "record_type", "description": "Record type (A, AAAA, CNAME, etc.)", "required": False},
|
||||
{"name": "name", "description": "Record name", "required": False},
|
||||
{"name": "data", "description": "Record data", "required": False},
|
||||
{"name": "ip", "description": "IP address (for create domain)", "required": False},
|
||||
]
|
||||
),
|
||||
except Exception as e:
|
||||
logger.error(f"Error executing tool {request.name}: {e}", exc_info=True)
|
||||
return ToolCallResponse(
|
||||
content=[{"type": "text", "text": f"Error: {str(e)}"}],
|
||||
isError=True
|
||||
)
|
||||
|
||||
|
||||
@app.get("/resources/{resource_uri:path}")
|
||||
async def get_resource(resource_uri: str):
|
||||
"""Get resource content"""
|
||||
if not vultr_client:
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"error": "Vultr client not initialized"}
|
||||
)
|
||||
|
||||
try:
|
||||
result = None
|
||||
|
||||
if resource_uri == "vultr://account/info":
|
||||
result = vultr_client.get_account_info()
|
||||
elif resource_uri == "vultr://instances/list":
|
||||
result = vultr_client.list_instances()
|
||||
elif resource_uri == "vultr://regions/list":
|
||||
result = vultr_client.list_regions()
|
||||
elif resource_uri == "vultr://plans/list":
|
||||
result = vultr_client.list_plans()
|
||||
elif resource_uri == "vultr://os/list":
|
||||
result = vultr_client.list_os()
|
||||
elif resource_uri == "vultr://dns/domains":
|
||||
result = vultr_client.list_dns_domains()
|
||||
elif resource_uri == "vultr://dns/records":
|
||||
# This requires a domain parameter, so we'll return instructions
|
||||
result = {"message": "Please use the list_dns_records tool with domain parameter to get DNS records"}
|
||||
else:
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
content={"error": f"Resource not found: {resource_uri}"}
|
||||
)
|
||||
|
||||
if isinstance(result, dict) and 'error' in result:
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"error": result['error']}
|
||||
)
|
||||
|
||||
return JSONResponse(content=result)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting resource {resource_uri}: {e}", exc_info=True)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"error": str(e)}
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
|
||||
# DNS Management Tools
|
||||
Tool(
|
||||
name="list_dns_domains",
|
||||
description="List all DNS domains",
|
||||
inputSchema={"type": "object", "properties": {}}
|
||||
),
|
||||
Tool(
|
||||
name="create_dns_domain",
|
||||
description="Create a new DNS domain",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "Domain name (e.g., 'example.com')"
|
||||
},
|
||||
"ip": {
|
||||
"type": "string",
|
||||
"description": "IP address for the domain"
|
||||
}
|
||||
},
|
||||
"required": ["domain", "ip"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="delete_dns_domain",
|
||||
description="Delete a DNS domain",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "Domain name"
|
||||
}
|
||||
},
|
||||
"required": ["domain"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="get_dns_domain",
|
||||
description="Get DNS domain details",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "Domain name"
|
||||
}
|
||||
},
|
||||
"required": ["domain"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="list_dns_records",
|
||||
description="List DNS records for a domain",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "Domain name"
|
||||
}
|
||||
},
|
||||
"required": ["domain"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="create_dns_record",
|
||||
description="Create a new DNS record",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "Domain name"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Record type (A, AAAA, CNAME, MX, TXT, etc.)"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Record name (e.g., 'www', '@', 'mail')"
|
||||
},
|
||||
"data": {
|
||||
"type": "string",
|
||||
"description": "Record data (IP address, hostname, etc.)"
|
||||
},
|
||||
"ttl": {
|
||||
"type": "integer",
|
||||
"description": "Time to live in seconds",
|
||||
"default": 300
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer",
|
||||
"description": "Priority for MX records",
|
||||
"default": 0
|
||||
}
|
||||
},
|
||||
"required": ["domain", "type", "name", "data"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="update_dns_record",
|
||||
description="Update a DNS record",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "Domain name"
|
||||
},
|
||||
"record_id": {
|
||||
"type": "string",
|
||||
"description": "Record ID"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Record type (A, AAAA, CNAME, MX, TXT, etc.)"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Record name"
|
||||
},
|
||||
"data": {
|
||||
"type": "string",
|
||||
"description": "Record data"
|
||||
},
|
||||
"ttl": {
|
||||
"type": "integer",
|
||||
"description": "Time to live in seconds"
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer",
|
||||
"description": "Priority for MX records"
|
||||
}
|
||||
},
|
||||
"required": ["domain", "record_id"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="delete_dns_record",
|
||||
description="Delete a DNS record",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "Domain name"
|
||||
},
|
||||
"record_id": {
|
||||
"type": "string",
|
||||
"description": "Record ID"
|
||||
}
|
||||
},
|
||||
"required": ["domain", "record_id"]
|
||||
}
|
||||
),
|
||||
63
test_dns_tools.py
Normal file
63
test_dns_tools.py
Normal file
@@ -0,0 +1,63 @@
|
||||
import json
|
||||
|
||||
# Read server.py to check DNS tools
|
||||
with open('src/vultr_mcp/server.py', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Check for DNS tools in tools list
|
||||
dns_tools_in_list = []
|
||||
lines = content.split('\n')
|
||||
in_tools_section = False
|
||||
for i, line in enumerate(lines):
|
||||
if '@app.get("/tools")' in line:
|
||||
in_tools_section = True
|
||||
elif in_tools_section and 'return tools' in line:
|
||||
in_tools_section = False
|
||||
|
||||
if in_tools_section and 'name="list_dns_domains"' in line:
|
||||
# Find the tool name
|
||||
for j in range(max(0, i-10), min(len(lines), i+10)):
|
||||
if 'Tool(' in lines[j]:
|
||||
# Extract tool name
|
||||
for k in range(j, min(len(lines), j+20)):
|
||||
if 'name=' in lines[k]:
|
||||
name = lines[k].split('name=')[1].split('"')[1]
|
||||
dns_tools_in_list.append(name)
|
||||
break
|
||||
break
|
||||
|
||||
print(f"DNS tools found in tools list: {len(dns_tools_in_list)}")
|
||||
for tool in dns_tools_in_list:
|
||||
print(f" - {tool}")
|
||||
|
||||
# Check for DNS tool handling in call_tool
|
||||
dns_tools_in_handler = []
|
||||
for line in lines:
|
||||
if 'elif request.name ==' in line and 'dns' in line.lower():
|
||||
tool_name = line.split('"')[1]
|
||||
dns_tools_in_handler.append(tool_name)
|
||||
|
||||
print(f"\nDNS tools found in call_tool handler: {len(dns_tools_in_handler)}")
|
||||
for tool in dns_tools_in_handler:
|
||||
print(f" - {tool}")
|
||||
|
||||
# Check for DNS resources
|
||||
dns_resources = []
|
||||
for line in lines:
|
||||
if 'uri="vultr://dns' in line:
|
||||
uri = line.split('uri="')[1].split('"')[0]
|
||||
dns_resources.append(uri)
|
||||
|
||||
print(f"\nDNS resources found: {len(dns_resources)}")
|
||||
for resource in dns_resources:
|
||||
print(f" - {resource}")
|
||||
|
||||
# Check for DNS prompts
|
||||
dns_prompts = []
|
||||
for line in lines:
|
||||
if 'name="manage_dns"' in line:
|
||||
dns_prompts.append('manage_dns')
|
||||
|
||||
print(f"\nDNS prompts found: {len(dns_prompts)}")
|
||||
for prompt in dns_prompts:
|
||||
print(f" - {prompt}")
|
||||
173
test_server.py
Normal file
173
test_server.py
Normal file
@@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for Vultr MCP Server
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from src.vultr_mcp.server import app
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
# Create test client
|
||||
client = TestClient(app)
|
||||
|
||||
def test_root_endpoint():
|
||||
"""Test root endpoint"""
|
||||
print("Testing root endpoint...")
|
||||
response = client.get("/")
|
||||
assert response.status_code == 200, f"Expected 200, got {response.status_code}"
|
||||
data = response.json()
|
||||
assert "name" in data, "Missing 'name' in response"
|
||||
assert "Vultr MCP Server" in data["name"], f"Unexpected name: {data.get('name')}"
|
||||
print(f" ✓ Root endpoint: {data['name']} v{data.get('version', 'N/A')}")
|
||||
return True
|
||||
|
||||
def test_tools_endpoint():
|
||||
"""Test tools endpoint"""
|
||||
print("\nTesting tools endpoint...")
|
||||
response = client.get("/tools")
|
||||
assert response.status_code == 200, f"Expected 200, got {response.status_code}"
|
||||
tools = response.json()
|
||||
assert isinstance(tools, list), f"Expected list, got {type(tools)}"
|
||||
assert len(tools) > 0, "No tools found"
|
||||
|
||||
print(f" ✓ Found {len(tools)} tools:")
|
||||
for i, tool in enumerate(tools[:5], 1): # Show first 5
|
||||
print(f" {i}. {tool['name']}: {tool['description'][:50]}...")
|
||||
if len(tools) > 5:
|
||||
print(f" ... and {len(tools) - 5} more")
|
||||
|
||||
# Verify some expected tools
|
||||
tool_names = [t['name'] for t in tools]
|
||||
expected_tools = ['get_account_info', 'list_instances', 'create_instance']
|
||||
for expected in expected_tools:
|
||||
assert expected in tool_names, f"Missing expected tool: {expected}"
|
||||
|
||||
return True
|
||||
|
||||
def test_resources_endpoint():
|
||||
"""Test resources endpoint"""
|
||||
print("\nTesting resources endpoint...")
|
||||
response = client.get("/resources")
|
||||
assert response.status_code == 200, f"Expected 200, got {response.status_code}"
|
||||
resources = response.json()
|
||||
assert isinstance(resources, list), f"Expected list, got {type(resources)}"
|
||||
|
||||
print(f" ✓ Found {len(resources)} resources:")
|
||||
for resource in resources:
|
||||
print(f" - {resource['name']}: {resource['uri']}")
|
||||
|
||||
return True
|
||||
|
||||
def test_prompts_endpoint():
|
||||
"""Test prompts endpoint"""
|
||||
print("\nTesting prompts endpoint...")
|
||||
response = client.get("/prompts")
|
||||
assert response.status_code == 200, f"Expected 200, got {response.status_code}"
|
||||
prompts = response.json()
|
||||
assert isinstance(prompts, list), f"Expected list, got {type(prompts)}"
|
||||
|
||||
print(f" ✓ Found {len(prompts)} prompts:")
|
||||
for prompt in prompts:
|
||||
print(f" - {prompt['name']}: {prompt['description']}")
|
||||
|
||||
return True
|
||||
|
||||
def test_tool_call_without_api_key():
|
||||
"""Test tool call without API key (should fail gracefully)"""
|
||||
print("\nTesting tool call without API key...")
|
||||
response = client.post("/tools/call", json={
|
||||
"name": "get_account_info",
|
||||
"arguments": {}
|
||||
})
|
||||
assert response.status_code == 200, f"Expected 200, got {response.status_code}"
|
||||
result = response.json()
|
||||
|
||||
# Should either be an error or contain content
|
||||
assert "content" in result, "Missing 'content' in response"
|
||||
assert isinstance(result["content"], list), "Content should be a list"
|
||||
|
||||
if result.get("isError", False):
|
||||
print(" ✓ Tool call correctly returned error (no API key configured)")
|
||||
if result["content"]:
|
||||
error_text = result["content"][0].get("text", "")
|
||||
print(f" Error message: {error_text[:100]}...")
|
||||
else:
|
||||
print(" ✓ Tool call succeeded (API key might be configured)")
|
||||
|
||||
return True
|
||||
|
||||
def test_server_info_endpoints():
|
||||
"""Test that server info endpoints work"""
|
||||
print("\nTesting server info endpoints...")
|
||||
|
||||
# Test that we can get the OpenAPI schema
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200, f"OpenAPI schema failed: {response.status_code}"
|
||||
print(" ✓ OpenAPI schema available")
|
||||
|
||||
# Test docs endpoint
|
||||
response = client.get("/docs")
|
||||
assert response.status_code == 200, f"Docs endpoint failed: {response.status_code}"
|
||||
print(" ✓ API documentation available")
|
||||
|
||||
return True
|
||||
|
||||
def main():
|
||||
"""Run all tests"""
|
||||
print("=" * 60)
|
||||
print("Vultr MCP Server - Test Suite")
|
||||
print("=" * 60)
|
||||
print("Note: These tests don't require a Vultr API key")
|
||||
print(" They only test the server structure and endpoints\n")
|
||||
|
||||
tests = [
|
||||
test_root_endpoint,
|
||||
test_tools_endpoint,
|
||||
test_resources_endpoint,
|
||||
test_prompts_endpoint,
|
||||
test_tool_call_without_api_key,
|
||||
test_server_info_endpoints,
|
||||
]
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
failed_tests = []
|
||||
|
||||
for test in tests:
|
||||
try:
|
||||
if test():
|
||||
passed += 1
|
||||
except AssertionError as e:
|
||||
print(f" ✗ Test failed: {e}")
|
||||
failed += 1
|
||||
failed_tests.append(test.__name__)
|
||||
except Exception as e:
|
||||
print(f" ✗ Unexpected error in {test.__name__}: {e}")
|
||||
failed += 1
|
||||
failed_tests.append(test.__name__)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("TEST RESULTS")
|
||||
print("=" * 60)
|
||||
print(f"Passed: {passed}")
|
||||
print(f"Failed: {failed}")
|
||||
|
||||
if failed == 0:
|
||||
print("\n✅ All tests passed! Server is ready to run.")
|
||||
print("\nNext steps:")
|
||||
print(" 1. Copy .env.example to .env")
|
||||
print(" 2. Add your VULTR_API_KEY to .env")
|
||||
print(" 3. Run the server: python main.py")
|
||||
print(" 4. Test with example: python examples/basic_usage.py")
|
||||
return 0
|
||||
else:
|
||||
print(f"\n❌ {failed} test(s) failed: {', '.join(failed_tests)}")
|
||||
print("\nCheck the implementation and try again.")
|
||||
return 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
||||
133
update_server.py
Normal file
133
update_server.py
Normal file
@@ -0,0 +1,133 @@
|
||||
import re
|
||||
|
||||
# Read the server.py file
|
||||
with open('src/vultr_mcp/server.py', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Add DNS tool handling to call_tool function
|
||||
# Find the list_os handling and insert DNS handling after it
|
||||
list_os_pattern = r"(elif request\.name == \"list_os\":\s*result = vultr_client\.list_os\(\)\s*)"
|
||||
match = re.search(list_os_pattern, content)
|
||||
if match:
|
||||
list_os_section = match.group(1)
|
||||
dns_handling = ''' elif request.name == "list_dns_domains":
|
||||
result = vultr_client.list_dns_domains()
|
||||
|
||||
elif request.name == "create_dns_domain":
|
||||
domain = request.arguments.get("domain")
|
||||
ip = request.arguments.get("ip")
|
||||
if not domain or not ip:
|
||||
raise ValueError("domain and ip are required")
|
||||
result = vultr_client.create_dns_domain(domain, ip)
|
||||
|
||||
elif request.name == "delete_dns_domain":
|
||||
domain = request.arguments.get("domain")
|
||||
if not domain:
|
||||
raise ValueError("domain is required")
|
||||
result = vultr_client.delete_dns_domain(domain)
|
||||
|
||||
elif request.name == "get_dns_domain":
|
||||
domain = request.arguments.get("domain")
|
||||
if not domain:
|
||||
raise ValueError("domain is required")
|
||||
result = vultr_client.get_dns_domain(domain)
|
||||
|
||||
elif request.name == "list_dns_records":
|
||||
domain = request.arguments.get("domain")
|
||||
if not domain:
|
||||
raise ValueError("domain is required")
|
||||
result = vultr_client.list_dns_records(domain)
|
||||
|
||||
elif request.name == "create_dns_record":
|
||||
domain = request.arguments.get("domain")
|
||||
record_type = request.arguments.get("type")
|
||||
name = request.arguments.get("name")
|
||||
data = request.arguments.get("data")
|
||||
ttl = request.arguments.get("ttl", 300)
|
||||
priority = request.arguments.get("priority", 0)
|
||||
if not domain or not record_type or not name or not data:
|
||||
raise ValueError("domain, type, name, and data are required")
|
||||
result = vultr_client.create_dns_record(domain, record_type, name, data, ttl, priority)
|
||||
|
||||
elif request.name == "update_dns_record":
|
||||
domain = request.arguments.get("domain")
|
||||
record_id = request.arguments.get("record_id")
|
||||
if not domain or not record_id:
|
||||
raise ValueError("domain and record_id are required")
|
||||
# Filter out None values
|
||||
update_args = {k: v for k, v in request.arguments.items()
|
||||
if k not in ["domain", "record_id"] and v is not None}
|
||||
result = vultr_client.update_dns_record(domain, record_id, **update_args)
|
||||
|
||||
elif request.name == "delete_dns_record":
|
||||
domain = request.arguments.get("domain")
|
||||
record_id = request.arguments.get("record_id")
|
||||
if not domain or not record_id:
|
||||
raise ValueError("domain and record_id are required")
|
||||
result = vultr_client.delete_dns_record(domain, record_id)
|
||||
|
||||
'''
|
||||
# Insert DNS handling after list_os
|
||||
new_content = content.replace(list_os_section, list_os_section + dns_handling)
|
||||
content = new_content
|
||||
|
||||
# Add DNS resources to list_resources function
|
||||
resources_pattern = r"(Resource\(\s*uri=\"vultr://os/list\",\s*name=\"Vultr Operating Systems\",\s*description=\"List of available Vultr operating systems\"\s*\)\s*,?\s*)"
|
||||
match = re.search(resources_pattern, content, re.DOTALL)
|
||||
if match:
|
||||
os_resource = match.group(1)
|
||||
dns_resources = ''' Resource(
|
||||
uri="vultr://dns/domains",
|
||||
name="Vultr DNS Domains",
|
||||
description="List of DNS domains"
|
||||
),
|
||||
Resource(
|
||||
uri="vultr://dns/records",
|
||||
name="Vultr DNS Records",
|
||||
description="DNS records for a domain"
|
||||
),
|
||||
'''
|
||||
new_content = content.replace(os_resource, os_resource + dns_resources)
|
||||
content = new_content
|
||||
|
||||
# Add DNS prompts to list_prompts function
|
||||
prompts_pattern = r"(Prompt\(\s*name=\"monitor_resources\",\s*description=\"Monitor Vultr resources\",\s*arguments=\[\s*\{.*?\}\s*\]\s*\)\s*,?\s*)"
|
||||
match = re.search(prompts_pattern, content, re.DOTALL)
|
||||
if match:
|
||||
monitor_prompt = match.group(1)
|
||||
dns_prompts = ''' Prompt(
|
||||
name="manage_dns",
|
||||
description="Manage DNS domains and records",
|
||||
arguments=[
|
||||
{"name": "action", "description": "Action to perform (create_domain, delete_domain, list_records, create_record, update_record, delete_record)", "required": True},
|
||||
{"name": "domain", "description": "Domain name", "required": True},
|
||||
{"name": "record_id", "description": "Record ID (for update/delete)", "required": False},
|
||||
{"name": "record_type", "description": "Record type (A, AAAA, CNAME, etc.)", "required": False},
|
||||
{"name": "name", "description": "Record name", "required": False},
|
||||
{"name": "data", "description": "Record data", "required": False},
|
||||
{"name": "ip", "description": "IP address (for create domain)", "required": False},
|
||||
]
|
||||
),
|
||||
'''
|
||||
new_content = content.replace(monitor_prompt, monitor_prompt + dns_prompts)
|
||||
content = new_content
|
||||
|
||||
# Add DNS resource handling to get_resource function
|
||||
get_resource_pattern = r"(elif resource_uri == \"vultr://os/list\":\s*result = vultr_client\.list_os\(\)\s*)"
|
||||
match = re.search(get_resource_pattern, content)
|
||||
if match:
|
||||
os_resource_handling = match.group(1)
|
||||
dns_resource_handling = ''' elif resource_uri == "vultr://dns/domains":
|
||||
result = vultr_client.list_dns_domains()
|
||||
elif resource_uri == "vultr://dns/records":
|
||||
# This requires a domain parameter, so we'll return instructions
|
||||
result = {"message": "Please use the list_dns_records tool with domain parameter to get DNS records"}
|
||||
'''
|
||||
new_content = content.replace(os_resource_handling, os_resource_handling + dns_resource_handling)
|
||||
content = new_content
|
||||
|
||||
# Write the updated content back
|
||||
with open('src/vultr_mcp/server.py', 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
print("Server.py updated successfully with DNS functionality")
|
||||
95
verify_server.py
Normal file
95
verify_server.py
Normal file
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple verification script for Vultr MCP Server
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
print("Vultr MCP Server - Verification Script")
|
||||
print("=" * 50)
|
||||
|
||||
# 1. Check if we can import the modules
|
||||
print("\n1. Importing modules...")
|
||||
try:
|
||||
from src.vultr_mcp.client import VultrClient
|
||||
print(" ✓ VultrClient imported successfully")
|
||||
except ImportError as e:
|
||||
print(f" ✗ Failed to import VultrClient: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
from src.vultr_mcp.server import app
|
||||
print(" ✓ FastAPI app imported successfully")
|
||||
except ImportError as e:
|
||||
print(f" ✗ Failed to import FastAPI app: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# 2. Check app structure
|
||||
print("\n2. Checking app structure...")
|
||||
if hasattr(app, 'routes'):
|
||||
print(f" ✓ App has {len(app.routes)} routes")
|
||||
else:
|
||||
print(" ✗ App doesn't have routes attribute")
|
||||
|
||||
# 3. Check that we can create a VultrClient instance (will fail without API key)
|
||||
print("\n3. Testing VultrClient initialization...")
|
||||
try:
|
||||
# This should fail since we don't have VULTR_API_KEY
|
||||
client = VultrClient()
|
||||
print(" ✗ Unexpected: VultrClient initialized without API key")
|
||||
except ValueError as e:
|
||||
print(f" ✓ VultrClient correctly requires API key: {e}")
|
||||
except Exception as e:
|
||||
print(f" ✗ Unexpected error: {e}")
|
||||
|
||||
# 4. Check main entry point
|
||||
print("\n4. Checking main entry point...")
|
||||
try:
|
||||
with open('main.py', 'r') as f:
|
||||
content = f.read()
|
||||
if 'uvicorn.run' in content:
|
||||
print(" ✓ main.py contains uvicorn.run")
|
||||
else:
|
||||
print(" ✗ main.py doesn't contain uvicorn.run")
|
||||
except Exception as e:
|
||||
print(f" ✗ Error reading main.py: {e}")
|
||||
|
||||
# 5. Check requirements
|
||||
print("\n5. Checking requirements.txt...")
|
||||
required_packages = ['fastapi', 'uvicorn', 'vultr-python-client', 'python-dotenv']
|
||||
missing = []
|
||||
|
||||
for package in required_packages:
|
||||
try:
|
||||
__import__(package.replace('-', '_'))
|
||||
print(f" ✓ {package} is installed")
|
||||
except ImportError:
|
||||
missing.append(package)
|
||||
print(f" ✗ {package} is NOT installed")
|
||||
|
||||
if missing:
|
||||
print(f"\n Warning: Missing packages: {', '.join(missing)}")
|
||||
print(" Install with: pip install {' '.join(missing)}")
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("VERIFICATION COMPLETE")
|
||||
print("=" * 50)
|
||||
|
||||
if len(missing) == 0:
|
||||
print("\n✅ Server implementation is ready!")
|
||||
print("\nTo use the Vultr MCP Server:")
|
||||
print("1. Get your Vultr API key from: https://my.vultr.com/settings/#settingsapi")
|
||||
print("2. Copy .env.example to .env and add your API key")
|
||||
print("3. Start the server: python main.py")
|
||||
print("4. Test with: python examples/basic_usage.py")
|
||||
print("\nThe server will run on http://localhost:8000")
|
||||
print("MCP endpoints will be available at:")
|
||||
print(" - GET /tools - List available tools")
|
||||
print(" - POST /tools/call - Execute Vultr operations")
|
||||
print(" - GET /resources - List available resources")
|
||||
print(" - GET /prompts - List available prompts")
|
||||
else:
|
||||
print("\n⚠️ Some packages are missing. Please install them first.")
|
||||
|
||||
Reference in New Issue
Block a user