From a31447e28f50c7c1b219d2be2b0b0f88ba9956bd Mon Sep 17 00:00:00 2001 From: lmiranda Date: Wed, 3 Dec 2025 12:15:17 -0500 Subject: [PATCH 1/2] feat: add NetBox MCP server for infrastructure management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Comprehensive MCP server covering the entire NetBox REST API: - DCIM: sites, racks, devices, interfaces, cables, power - IPAM: prefixes, IP addresses, VLANs, VRFs, ASNs - Circuits: providers, circuits, terminations - Virtualization: clusters, VMs, VM interfaces - Tenancy: tenants, contacts, contact assignments - VPN: tunnels, IKE/IPSec policies, L2VPN - Wireless: WLANs, links, groups - Extras: tags, custom fields, webhooks, audit log Features: - 100+ MCP tools for full CRUD operations - Auto-pagination handling - Hybrid config (system + project level) - Available prefix/IP allocation support πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- mcp-servers/netbox/README.md | 297 ++++ mcp-servers/netbox/mcp_server/__init__.py | 1 + mcp-servers/netbox/mcp_server/config.py | 108 ++ .../netbox/mcp_server/netbox_client.py | 294 ++++ mcp-servers/netbox/mcp_server/server.py | 1365 +++++++++++++++++ .../netbox/mcp_server/tools/__init__.py | 20 + .../netbox/mcp_server/tools/circuits.py | 373 +++++ mcp-servers/netbox/mcp_server/tools/dcim.py | 935 +++++++++++ mcp-servers/netbox/mcp_server/tools/extras.py | 560 +++++++ mcp-servers/netbox/mcp_server/tools/ipam.py | 718 +++++++++ .../netbox/mcp_server/tools/tenancy.py | 281 ++++ .../netbox/mcp_server/tools/virtualization.py | 296 ++++ mcp-servers/netbox/mcp_server/tools/vpn.py | 428 ++++++ .../netbox/mcp_server/tools/wireless.py | 166 ++ mcp-servers/netbox/requirements.txt | 6 + mcp-servers/netbox/tests/__init__.py | 1 + 16 files changed, 5849 insertions(+) create mode 100644 mcp-servers/netbox/README.md create mode 100644 mcp-servers/netbox/mcp_server/__init__.py create mode 100644 mcp-servers/netbox/mcp_server/config.py create mode 100644 mcp-servers/netbox/mcp_server/netbox_client.py create mode 100644 mcp-servers/netbox/mcp_server/server.py create mode 100644 mcp-servers/netbox/mcp_server/tools/__init__.py create mode 100644 mcp-servers/netbox/mcp_server/tools/circuits.py create mode 100644 mcp-servers/netbox/mcp_server/tools/dcim.py create mode 100644 mcp-servers/netbox/mcp_server/tools/extras.py create mode 100644 mcp-servers/netbox/mcp_server/tools/ipam.py create mode 100644 mcp-servers/netbox/mcp_server/tools/tenancy.py create mode 100644 mcp-servers/netbox/mcp_server/tools/virtualization.py create mode 100644 mcp-servers/netbox/mcp_server/tools/vpn.py create mode 100644 mcp-servers/netbox/mcp_server/tools/wireless.py create mode 100644 mcp-servers/netbox/requirements.txt create mode 100644 mcp-servers/netbox/tests/__init__.py diff --git a/mcp-servers/netbox/README.md b/mcp-servers/netbox/README.md new file mode 100644 index 0000000..588bbf8 --- /dev/null +++ b/mcp-servers/netbox/README.md @@ -0,0 +1,297 @@ +# NetBox MCP Server + +MCP (Model Context Protocol) server for comprehensive NetBox API integration with Claude Code. + +## Overview + +This MCP server provides Claude Code with full access to the NetBox REST API, enabling infrastructure management, documentation, and automation workflows. It covers all major NetBox application areas: + +- **DCIM** - Sites, Locations, Racks, Devices, Interfaces, Cables, Power +- **IPAM** - IP Addresses, Prefixes, VLANs, VRFs, ASNs, Services +- **Circuits** - Providers, Circuits, Terminations +- **Virtualization** - Clusters, Virtual Machines, VM Interfaces +- **Tenancy** - Tenants, Contacts, Contact Assignments +- **VPN** - Tunnels, IKE/IPSec Policies, L2VPN +- **Wireless** - Wireless LANs, Links, Groups +- **Extras** - Tags, Custom Fields, Webhooks, Config Contexts, Audit Log + +## Installation + +### 1. Clone and Setup + +```bash +cd /path/to/mcp-servers/netbox +python -m venv .venv +source .venv/bin/activate # On Windows: .venv\Scripts\activate +pip install -r requirements.txt +``` + +### 2. Configure Credentials + +Create the system-level configuration file: + +```bash +mkdir -p ~/.config/claude +cat > ~/.config/claude/netbox.env << 'EOF' +NETBOX_API_URL=https://your-netbox-instance/api +NETBOX_API_TOKEN=your-api-token-here +NETBOX_VERIFY_SSL=true +NETBOX_TIMEOUT=30 +EOF +``` + +**Getting a NetBox API Token:** +1. Log into your NetBox instance +2. Navigate to your profile (top-right menu) +3. Go to "API Tokens" +4. Click "Add a token" +5. Copy the generated token + +### 3. Register with Claude Code + +Add to your Claude Code MCP configuration (`~/.config/claude/mcp.json` or project `.mcp.json`): + +```json +{ + "mcpServers": { + "netbox": { + "command": "/path/to/mcp-servers/netbox/.venv/bin/python", + "args": ["-m", "mcp_server.server"], + "cwd": "/path/to/mcp-servers/netbox" + } + } +} +``` + +## Configuration + +### Environment Variables + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `NETBOX_API_URL` | Yes | - | Full URL to NetBox API (e.g., `https://netbox.example.com/api`) | +| `NETBOX_API_TOKEN` | Yes | - | API authentication token | +| `NETBOX_VERIFY_SSL` | No | `true` | Verify SSL certificates | +| `NETBOX_TIMEOUT` | No | `30` | Request timeout in seconds | + +### Configuration Hierarchy + +1. **System-level** (`~/.config/claude/netbox.env`): Credentials and defaults +2. **Project-level** (`.env` in current directory): Optional overrides + +## Available Tools + +### DCIM (Data Center Infrastructure Management) + +| Tool | Description | +|------|-------------| +| `dcim_list_sites` | List all sites | +| `dcim_get_site` | Get site details | +| `dcim_create_site` | Create a new site | +| `dcim_update_site` | Update a site | +| `dcim_delete_site` | Delete a site | +| `dcim_list_devices` | List all devices | +| `dcim_get_device` | Get device details | +| `dcim_create_device` | Create a new device | +| `dcim_update_device` | Update a device | +| `dcim_delete_device` | Delete a device | +| `dcim_list_interfaces` | List device interfaces | +| `dcim_create_interface` | Create an interface | +| `dcim_list_racks` | List all racks | +| `dcim_create_rack` | Create a new rack | +| `dcim_list_cables` | List all cables | +| `dcim_create_cable` | Create a cable connection | +| ... and many more | + +### IPAM (IP Address Management) + +| Tool | Description | +|------|-------------| +| `ipam_list_prefixes` | List IP prefixes | +| `ipam_create_prefix` | Create a prefix | +| `ipam_list_available_prefixes` | List available child prefixes | +| `ipam_create_available_prefix` | Auto-allocate a prefix | +| `ipam_list_ip_addresses` | List IP addresses | +| `ipam_create_ip_address` | Create an IP address | +| `ipam_list_available_ips` | List available IPs in prefix | +| `ipam_create_available_ip` | Auto-allocate an IP | +| `ipam_list_vlans` | List VLANs | +| `ipam_create_vlan` | Create a VLAN | +| `ipam_list_vrfs` | List VRFs | +| ... and many more | + +### Circuits + +| Tool | Description | +|------|-------------| +| `circuits_list_providers` | List circuit providers | +| `circuits_create_provider` | Create a provider | +| `circuits_list_circuits` | List circuits | +| `circuits_create_circuit` | Create a circuit | +| `circuits_list_circuit_terminations` | List terminations | +| ... and more | + +### Virtualization + +| Tool | Description | +|------|-------------| +| `virtualization_list_clusters` | List clusters | +| `virtualization_create_cluster` | Create a cluster | +| `virtualization_list_virtual_machines` | List VMs | +| `virtualization_create_virtual_machine` | Create a VM | +| `virtualization_list_vm_interfaces` | List VM interfaces | +| ... and more | + +### Tenancy + +| Tool | Description | +|------|-------------| +| `tenancy_list_tenants` | List tenants | +| `tenancy_create_tenant` | Create a tenant | +| `tenancy_list_contacts` | List contacts | +| `tenancy_create_contact` | Create a contact | +| ... and more | + +### VPN + +| Tool | Description | +|------|-------------| +| `vpn_list_tunnels` | List VPN tunnels | +| `vpn_create_tunnel` | Create a tunnel | +| `vpn_list_l2vpns` | List L2VPNs | +| `vpn_list_ike_policies` | List IKE policies | +| `vpn_list_ipsec_policies` | List IPSec policies | +| ... and more | + +### Wireless + +| Tool | Description | +|------|-------------| +| `wireless_list_wireless_lans` | List wireless LANs | +| `wireless_create_wireless_lan` | Create a WLAN | +| `wireless_list_wireless_links` | List wireless links | +| ... and more | + +### Extras + +| Tool | Description | +|------|-------------| +| `extras_list_tags` | List all tags | +| `extras_create_tag` | Create a tag | +| `extras_list_custom_fields` | List custom fields | +| `extras_list_webhooks` | List webhooks | +| `extras_list_journal_entries` | List journal entries | +| `extras_create_journal_entry` | Create journal entry | +| `extras_list_object_changes` | View audit log | +| `extras_list_config_contexts` | List config contexts | +| ... and more | + +## Usage Examples + +### List all devices at a site + +``` +Use the dcim_list_devices tool with site_id filter to see all devices at site 5 +``` + +### Create a new prefix and allocate IPs + +``` +1. Use ipam_create_prefix to create 10.0.1.0/24 +2. Use ipam_list_available_ips with the prefix ID to see available addresses +3. Use ipam_create_available_ip to auto-allocate the next IP +``` + +### Document a new server + +``` +1. Use dcim_create_device to create the device +2. Use dcim_create_interface to add network interfaces +3. Use ipam_create_ip_address to assign IPs to interfaces +4. Use extras_create_journal_entry to add notes +``` + +### Audit recent changes + +``` +Use extras_list_object_changes to see recent modifications in NetBox +``` + +## Architecture + +``` +mcp-servers/netbox/ +β”œβ”€β”€ mcp_server/ +β”‚ β”œβ”€β”€ __init__.py +β”‚ β”œβ”€β”€ config.py # Configuration loader +β”‚ β”œβ”€β”€ netbox_client.py # Generic HTTP client +β”‚ β”œβ”€β”€ server.py # MCP server entry point +β”‚ └── tools/ +β”‚ β”œβ”€β”€ __init__.py +β”‚ β”œβ”€β”€ dcim.py # DCIM operations +β”‚ β”œβ”€β”€ ipam.py # IPAM operations +β”‚ β”œβ”€β”€ circuits.py # Circuits operations +β”‚ β”œβ”€β”€ virtualization.py +β”‚ β”œβ”€β”€ tenancy.py +β”‚ β”œβ”€β”€ vpn.py +β”‚ β”œβ”€β”€ wireless.py +β”‚ └── extras.py +β”œβ”€β”€ tests/ +β”‚ └── __init__.py +β”œβ”€β”€ requirements.txt +└── README.md +``` + +## API Coverage + +This MCP server provides comprehensive coverage of the NetBox REST API v4.x: + +- Full CRUD operations for all major models +- Filtering and search capabilities +- Special endpoints (available prefixes, available IPs) +- Pagination handling (automatic) +- Error handling with detailed messages + +## Error Handling + +The server returns detailed error messages from the NetBox API, including: +- Validation errors +- Authentication failures +- Not found errors +- Permission errors + +## Security Notes + +- API tokens should be kept secure and not committed to version control +- Use environment variables or the system config file for credentials +- SSL verification is enabled by default +- Consider using read-only tokens for query-only workflows + +## Troubleshooting + +### Common Issues + +1. **Connection refused**: Check `NETBOX_API_URL` is correct and accessible +2. **401 Unauthorized**: Verify your API token is valid +3. **SSL errors**: Set `NETBOX_VERIFY_SSL=false` for self-signed certs (not recommended for production) +4. **Timeout errors**: Increase `NETBOX_TIMEOUT` for slow connections + +### Debug Mode + +Enable debug logging: + +```python +import logging +logging.basicConfig(level=logging.DEBUG) +``` + +## Contributing + +1. Follow the existing code patterns +2. Add tests for new functionality +3. Update documentation for new tools +4. Ensure compatibility with NetBox 4.x API + +## License + +Part of the claude-code-hhl-toolkit project. diff --git a/mcp-servers/netbox/mcp_server/__init__.py b/mcp-servers/netbox/mcp_server/__init__.py new file mode 100644 index 0000000..786cdac --- /dev/null +++ b/mcp-servers/netbox/mcp_server/__init__.py @@ -0,0 +1 @@ +"""NetBox MCP Server for Claude Code integration.""" diff --git a/mcp-servers/netbox/mcp_server/config.py b/mcp-servers/netbox/mcp_server/config.py new file mode 100644 index 0000000..584025e --- /dev/null +++ b/mcp-servers/netbox/mcp_server/config.py @@ -0,0 +1,108 @@ +""" +Configuration loader for NetBox MCP Server. + +Implements hybrid configuration system: +- System-level: ~/.config/claude/netbox.env (credentials) +- Project-level: .env (optional overrides) +""" +from pathlib import Path +from dotenv import load_dotenv +import os +import logging +from typing import Dict, Optional + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class NetBoxConfig: + """Configuration loader for NetBox MCP Server""" + + def __init__(self): + self.api_url: Optional[str] = None + self.api_token: Optional[str] = None + self.verify_ssl: bool = True + self.timeout: int = 30 + + def load(self) -> Dict[str, any]: + """ + Load configuration from system and project levels. + Project-level configuration overrides system-level. + + Returns: + Dict containing api_url, api_token, verify_ssl, timeout + + Raises: + FileNotFoundError: If system config is missing + ValueError: If required configuration is missing + """ + # Load system config + system_config = Path.home() / '.config' / 'claude' / 'netbox.env' + if system_config.exists(): + load_dotenv(system_config) + logger.info(f"Loaded system configuration from {system_config}") + else: + raise FileNotFoundError( + f"System config not found: {system_config}\n" + "Create it with:\n" + " mkdir -p ~/.config/claude\n" + " cat > ~/.config/claude/netbox.env << EOF\n" + " NETBOX_API_URL=https://your-netbox-instance/api\n" + " NETBOX_API_TOKEN=your-api-token\n" + " EOF" + ) + + # Load project config (overrides system) + project_config = Path.cwd() / '.env' + if project_config.exists(): + load_dotenv(project_config, override=True) + logger.info(f"Loaded project configuration from {project_config}") + + # Extract values + self.api_url = os.getenv('NETBOX_API_URL') + self.api_token = os.getenv('NETBOX_API_TOKEN') + + # Optional settings with defaults + verify_ssl_str = os.getenv('NETBOX_VERIFY_SSL', 'true').lower() + self.verify_ssl = verify_ssl_str in ('true', '1', 'yes') + + timeout_str = os.getenv('NETBOX_TIMEOUT', '30') + try: + self.timeout = int(timeout_str) + except ValueError: + self.timeout = 30 + logger.warning(f"Invalid NETBOX_TIMEOUT value '{timeout_str}', using default 30") + + # Validate required variables + self._validate() + + # Normalize API URL (remove trailing slash) + if self.api_url and self.api_url.endswith('/'): + self.api_url = self.api_url.rstrip('/') + + return { + 'api_url': self.api_url, + 'api_token': self.api_token, + 'verify_ssl': self.verify_ssl, + 'timeout': self.timeout + } + + def _validate(self) -> None: + """ + Validate that required configuration is present. + + Raises: + ValueError: If required configuration is missing + """ + required = { + 'NETBOX_API_URL': self.api_url, + 'NETBOX_API_TOKEN': self.api_token + } + + missing = [key for key, value in required.items() if not value] + + if missing: + raise ValueError( + f"Missing required configuration: {', '.join(missing)}\n" + "Check your ~/.config/claude/netbox.env file" + ) diff --git a/mcp-servers/netbox/mcp_server/netbox_client.py b/mcp-servers/netbox/mcp_server/netbox_client.py new file mode 100644 index 0000000..9e226b4 --- /dev/null +++ b/mcp-servers/netbox/mcp_server/netbox_client.py @@ -0,0 +1,294 @@ +""" +NetBox API client for interacting with NetBox REST API. + +Provides a generic HTTP client with methods for all standard REST operations. +Individual tool modules use this client for their specific endpoints. +""" +import requests +import logging +from typing import List, Dict, Optional, Any, Union +from urllib.parse import urljoin +from .config import NetBoxConfig + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class NetBoxClient: + """Generic client for interacting with NetBox REST API""" + + def __init__(self): + """Initialize NetBox client with configuration""" + config = NetBoxConfig() + config_dict = config.load() + + self.base_url = config_dict['api_url'] + self.token = config_dict['api_token'] + self.verify_ssl = config_dict['verify_ssl'] + self.timeout = config_dict['timeout'] + + self.session = requests.Session() + self.session.headers.update({ + 'Authorization': f'Token {self.token}', + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }) + self.session.verify = self.verify_ssl + + logger.info(f"NetBox client initialized for {self.base_url}") + + def _build_url(self, endpoint: str) -> str: + """ + Build full URL for API endpoint. + + Args: + endpoint: API endpoint path (e.g., 'dcim/devices/') + + Returns: + Full URL + """ + # Ensure endpoint starts with / + if not endpoint.startswith('/'): + endpoint = '/' + endpoint + # Ensure endpoint ends with / + if not endpoint.endswith('/'): + endpoint = endpoint + '/' + return f"{self.base_url}{endpoint}" + + def _handle_response(self, response: requests.Response) -> Any: + """ + Handle API response and raise appropriate errors. + + Args: + response: requests Response object + + Returns: + Parsed JSON response + + Raises: + requests.HTTPError: If request failed + """ + try: + response.raise_for_status() + except requests.HTTPError as e: + # Try to get error details from response + try: + error_detail = response.json() + logger.error(f"API error: {error_detail}") + except Exception: + logger.error(f"API error: {response.text}") + raise e + + # Handle empty responses (e.g., DELETE) + if response.status_code == 204 or not response.content: + return None + + return response.json() + + def list( + self, + endpoint: str, + params: Optional[Dict[str, Any]] = None, + paginate: bool = True, + limit: int = 50 + ) -> List[Dict]: + """ + List objects from an endpoint with optional pagination. + + Args: + endpoint: API endpoint path + params: Query parameters for filtering + paginate: Whether to handle pagination automatically + limit: Number of results per page + + Returns: + List of objects + """ + url = self._build_url(endpoint) + params = params or {} + params['limit'] = limit + + logger.info(f"Listing objects from {endpoint}") + + if not paginate: + response = self.session.get(url, params=params, timeout=self.timeout) + result = self._handle_response(response) + return result.get('results', []) if isinstance(result, dict) else result + + # Handle pagination + all_results = [] + while url: + response = self.session.get(url, params=params, timeout=self.timeout) + result = self._handle_response(response) + + if isinstance(result, dict): + all_results.extend(result.get('results', [])) + url = result.get('next') + params = {} # Next URL already contains params + else: + all_results.extend(result) + break + + return all_results + + def get(self, endpoint: str, id: Union[int, str]) -> Dict: + """ + Get a single object by ID. + + Args: + endpoint: API endpoint path + id: Object ID + + Returns: + Object dictionary + """ + url = self._build_url(f"{endpoint}/{id}") + logger.info(f"Getting object {id} from {endpoint}") + response = self.session.get(url, timeout=self.timeout) + return self._handle_response(response) + + def create(self, endpoint: str, data: Dict) -> Dict: + """ + Create a new object. + + Args: + endpoint: API endpoint path + data: Object data + + Returns: + Created object dictionary + """ + url = self._build_url(endpoint) + logger.info(f"Creating object in {endpoint}") + response = self.session.post(url, json=data, timeout=self.timeout) + return self._handle_response(response) + + def create_bulk(self, endpoint: str, data: List[Dict]) -> List[Dict]: + """ + Create multiple objects in bulk. + + Args: + endpoint: API endpoint path + data: List of object data + + Returns: + List of created objects + """ + url = self._build_url(endpoint) + logger.info(f"Bulk creating {len(data)} objects in {endpoint}") + response = self.session.post(url, json=data, timeout=self.timeout) + return self._handle_response(response) + + def update(self, endpoint: str, id: Union[int, str], data: Dict) -> Dict: + """ + Update an existing object (full update). + + Args: + endpoint: API endpoint path + id: Object ID + data: Updated object data + + Returns: + Updated object dictionary + """ + url = self._build_url(f"{endpoint}/{id}") + logger.info(f"Updating object {id} in {endpoint}") + response = self.session.put(url, json=data, timeout=self.timeout) + return self._handle_response(response) + + def patch(self, endpoint: str, id: Union[int, str], data: Dict) -> Dict: + """ + Partially update an existing object. + + Args: + endpoint: API endpoint path + id: Object ID + data: Fields to update + + Returns: + Updated object dictionary + """ + url = self._build_url(f"{endpoint}/{id}") + logger.info(f"Patching object {id} in {endpoint}") + response = self.session.patch(url, json=data, timeout=self.timeout) + return self._handle_response(response) + + def delete(self, endpoint: str, id: Union[int, str]) -> None: + """ + Delete an object. + + Args: + endpoint: API endpoint path + id: Object ID + """ + url = self._build_url(f"{endpoint}/{id}") + logger.info(f"Deleting object {id} from {endpoint}") + response = self.session.delete(url, timeout=self.timeout) + self._handle_response(response) + + def delete_bulk(self, endpoint: str, ids: List[Union[int, str]]) -> None: + """ + Delete multiple objects in bulk. + + Args: + endpoint: API endpoint path + ids: List of object IDs + """ + url = self._build_url(endpoint) + data = [{'id': id} for id in ids] + logger.info(f"Bulk deleting {len(ids)} objects from {endpoint}") + response = self.session.delete(url, json=data, timeout=self.timeout) + self._handle_response(response) + + def options(self, endpoint: str) -> Dict: + """ + Get available options for an endpoint (schema info). + + Args: + endpoint: API endpoint path + + Returns: + Options/schema dictionary + """ + url = self._build_url(endpoint) + logger.info(f"Getting options for {endpoint}") + response = self.session.options(url, timeout=self.timeout) + return self._handle_response(response) + + def search( + self, + endpoint: str, + query: str, + params: Optional[Dict[str, Any]] = None + ) -> List[Dict]: + """ + Search objects using the 'q' parameter. + + Args: + endpoint: API endpoint path + query: Search query string + params: Additional filter parameters + + Returns: + List of matching objects + """ + params = params or {} + params['q'] = query + return self.list(endpoint, params=params) + + def filter( + self, + endpoint: str, + **filters + ) -> List[Dict]: + """ + Filter objects by various fields. + + Args: + endpoint: API endpoint path + **filters: Filter parameters (field=value) + + Returns: + List of matching objects + """ + return self.list(endpoint, params=filters) diff --git a/mcp-servers/netbox/mcp_server/server.py b/mcp-servers/netbox/mcp_server/server.py new file mode 100644 index 0000000..24b1b3a --- /dev/null +++ b/mcp-servers/netbox/mcp_server/server.py @@ -0,0 +1,1365 @@ +""" +MCP Server entry point for NetBox integration. + +Provides comprehensive NetBox tools to Claude Code via JSON-RPC 2.0 over stdio. +Covers the entire NetBox REST API: DCIM, IPAM, Circuits, Virtualization, +Tenancy, VPN, Wireless, and Extras. +""" +import asyncio +import logging +import json +from mcp.server import Server +from mcp.server.stdio import stdio_server +from mcp.types import Tool, TextContent + +from .config import NetBoxConfig +from .netbox_client import NetBoxClient +from .tools.dcim import DCIMTools +from .tools.ipam import IPAMTools +from .tools.circuits import CircuitsTools +from .tools.virtualization import VirtualizationTools +from .tools.tenancy import TenancyTools +from .tools.vpn import VPNTools +from .tools.wireless import WirelessTools +from .tools.extras import ExtrasTools + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +# Tool definitions organized by category +TOOL_DEFINITIONS = { + # ==================== DCIM Tools ==================== + 'dcim_list_regions': { + 'description': 'List all regions in NetBox', + 'properties': { + 'name': {'type': 'string', 'description': 'Filter by name'}, + 'slug': {'type': 'string', 'description': 'Filter by slug'} + } + }, + 'dcim_get_region': { + 'description': 'Get a specific region by ID', + 'properties': {'id': {'type': 'integer', 'description': 'Region ID'}}, + 'required': ['id'] + }, + 'dcim_create_region': { + 'description': 'Create a new region', + 'properties': { + 'name': {'type': 'string', 'description': 'Region name'}, + 'slug': {'type': 'string', 'description': 'URL-friendly slug'}, + 'parent': {'type': 'integer', 'description': 'Parent region ID'} + }, + 'required': ['name', 'slug'] + }, + 'dcim_update_region': { + 'description': 'Update an existing region', + 'properties': { + 'id': {'type': 'integer', 'description': 'Region ID'}, + 'name': {'type': 'string', 'description': 'New name'}, + 'slug': {'type': 'string', 'description': 'New slug'} + }, + 'required': ['id'] + }, + 'dcim_delete_region': { + 'description': 'Delete a region', + 'properties': {'id': {'type': 'integer', 'description': 'Region ID'}}, + 'required': ['id'] + }, + 'dcim_list_sites': { + 'description': 'List all sites in NetBox', + 'properties': { + 'name': {'type': 'string', 'description': 'Filter by name'}, + 'status': {'type': 'string', 'description': 'Filter by status (active, planned, staging, decommissioning, retired)'}, + 'region_id': {'type': 'integer', 'description': 'Filter by region ID'}, + 'tenant_id': {'type': 'integer', 'description': 'Filter by tenant ID'} + } + }, + 'dcim_get_site': { + 'description': 'Get a specific site by ID', + 'properties': {'id': {'type': 'integer', 'description': 'Site ID'}}, + 'required': ['id'] + }, + 'dcim_create_site': { + 'description': 'Create a new site', + 'properties': { + 'name': {'type': 'string', 'description': 'Site name'}, + 'slug': {'type': 'string', 'description': 'URL-friendly slug'}, + 'status': {'type': 'string', 'description': 'Site status'}, + 'region': {'type': 'integer', 'description': 'Region ID'}, + 'tenant': {'type': 'integer', 'description': 'Tenant ID'}, + 'facility': {'type': 'string', 'description': 'Facility name'}, + 'time_zone': {'type': 'string', 'description': 'Time zone'}, + 'description': {'type': 'string', 'description': 'Description'}, + 'physical_address': {'type': 'string', 'description': 'Physical address'}, + 'shipping_address': {'type': 'string', 'description': 'Shipping address'} + }, + 'required': ['name', 'slug'] + }, + 'dcim_update_site': { + 'description': 'Update an existing site', + 'properties': { + 'id': {'type': 'integer', 'description': 'Site ID'}, + 'name': {'type': 'string', 'description': 'New name'}, + 'status': {'type': 'string', 'description': 'New status'} + }, + 'required': ['id'] + }, + 'dcim_delete_site': { + 'description': 'Delete a site', + 'properties': {'id': {'type': 'integer', 'description': 'Site ID'}}, + 'required': ['id'] + }, + 'dcim_list_locations': { + 'description': 'List all locations in NetBox', + 'properties': { + 'name': {'type': 'string', 'description': 'Filter by name'}, + 'site_id': {'type': 'integer', 'description': 'Filter by site ID'} + } + }, + 'dcim_get_location': { + 'description': 'Get a specific location by ID', + 'properties': {'id': {'type': 'integer', 'description': 'Location ID'}}, + 'required': ['id'] + }, + 'dcim_create_location': { + 'description': 'Create a new location', + 'properties': { + 'name': {'type': 'string', 'description': 'Location name'}, + 'slug': {'type': 'string', 'description': 'URL-friendly slug'}, + 'site': {'type': 'integer', 'description': 'Site ID'}, + 'parent': {'type': 'integer', 'description': 'Parent location ID'} + }, + 'required': ['name', 'slug', 'site'] + }, + 'dcim_update_location': { + 'description': 'Update an existing location', + 'properties': {'id': {'type': 'integer', 'description': 'Location ID'}}, + 'required': ['id'] + }, + 'dcim_delete_location': { + 'description': 'Delete a location', + 'properties': {'id': {'type': 'integer', 'description': 'Location ID'}}, + 'required': ['id'] + }, + 'dcim_list_racks': { + 'description': 'List all racks in NetBox', + 'properties': { + 'name': {'type': 'string', 'description': 'Filter by name'}, + 'site_id': {'type': 'integer', 'description': 'Filter by site ID'}, + 'location_id': {'type': 'integer', 'description': 'Filter by location ID'}, + 'status': {'type': 'string', 'description': 'Filter by status'} + } + }, + 'dcim_get_rack': { + 'description': 'Get a specific rack by ID', + 'properties': {'id': {'type': 'integer', 'description': 'Rack ID'}}, + 'required': ['id'] + }, + 'dcim_create_rack': { + 'description': 'Create a new rack', + 'properties': { + 'name': {'type': 'string', 'description': 'Rack name'}, + 'site': {'type': 'integer', 'description': 'Site ID'}, + 'location': {'type': 'integer', 'description': 'Location ID'}, + 'status': {'type': 'string', 'description': 'Rack status'}, + 'u_height': {'type': 'integer', 'description': 'Rack height in U'} + }, + 'required': ['name', 'site'] + }, + 'dcim_update_rack': { + 'description': 'Update an existing rack', + 'properties': {'id': {'type': 'integer', 'description': 'Rack ID'}}, + 'required': ['id'] + }, + 'dcim_delete_rack': { + 'description': 'Delete a rack', + 'properties': {'id': {'type': 'integer', 'description': 'Rack ID'}}, + 'required': ['id'] + }, + 'dcim_list_manufacturers': { + 'description': 'List all manufacturers in NetBox', + 'properties': {'name': {'type': 'string', 'description': 'Filter by name'}} + }, + 'dcim_get_manufacturer': { + 'description': 'Get a specific manufacturer by ID', + 'properties': {'id': {'type': 'integer', 'description': 'Manufacturer ID'}}, + 'required': ['id'] + }, + 'dcim_create_manufacturer': { + 'description': 'Create a new manufacturer', + 'properties': { + 'name': {'type': 'string', 'description': 'Manufacturer name'}, + 'slug': {'type': 'string', 'description': 'URL-friendly slug'} + }, + 'required': ['name', 'slug'] + }, + 'dcim_update_manufacturer': { + 'description': 'Update an existing manufacturer', + 'properties': {'id': {'type': 'integer', 'description': 'Manufacturer ID'}}, + 'required': ['id'] + }, + 'dcim_delete_manufacturer': { + 'description': 'Delete a manufacturer', + 'properties': {'id': {'type': 'integer', 'description': 'Manufacturer ID'}}, + 'required': ['id'] + }, + 'dcim_list_device_types': { + 'description': 'List all device types in NetBox', + 'properties': { + 'manufacturer_id': {'type': 'integer', 'description': 'Filter by manufacturer ID'}, + 'model': {'type': 'string', 'description': 'Filter by model name'} + } + }, + 'dcim_get_device_type': { + 'description': 'Get a specific device type by ID', + 'properties': {'id': {'type': 'integer', 'description': 'Device type ID'}}, + 'required': ['id'] + }, + 'dcim_create_device_type': { + 'description': 'Create a new device type', + 'properties': { + 'manufacturer': {'type': 'integer', 'description': 'Manufacturer ID'}, + 'model': {'type': 'string', 'description': 'Model name'}, + 'slug': {'type': 'string', 'description': 'URL-friendly slug'}, + 'u_height': {'type': 'number', 'description': 'Height in rack units'} + }, + 'required': ['manufacturer', 'model', 'slug'] + }, + 'dcim_update_device_type': { + 'description': 'Update an existing device type', + 'properties': {'id': {'type': 'integer', 'description': 'Device type ID'}}, + 'required': ['id'] + }, + 'dcim_delete_device_type': { + 'description': 'Delete a device type', + 'properties': {'id': {'type': 'integer', 'description': 'Device type ID'}}, + 'required': ['id'] + }, + 'dcim_list_device_roles': { + 'description': 'List all device roles in NetBox', + 'properties': {'name': {'type': 'string', 'description': 'Filter by name'}} + }, + 'dcim_get_device_role': { + 'description': 'Get a specific device role by ID', + 'properties': {'id': {'type': 'integer', 'description': 'Device role ID'}}, + 'required': ['id'] + }, + 'dcim_create_device_role': { + 'description': 'Create a new device role', + 'properties': { + 'name': {'type': 'string', 'description': 'Role name'}, + 'slug': {'type': 'string', 'description': 'URL-friendly slug'}, + 'color': {'type': 'string', 'description': 'Hex color code'}, + 'vm_role': {'type': 'boolean', 'description': 'Can be assigned to VMs'} + }, + 'required': ['name', 'slug'] + }, + 'dcim_update_device_role': { + 'description': 'Update an existing device role', + 'properties': {'id': {'type': 'integer', 'description': 'Device role ID'}}, + 'required': ['id'] + }, + 'dcim_delete_device_role': { + 'description': 'Delete a device role', + 'properties': {'id': {'type': 'integer', 'description': 'Device role ID'}}, + 'required': ['id'] + }, + 'dcim_list_platforms': { + 'description': 'List all platforms in NetBox', + 'properties': { + 'name': {'type': 'string', 'description': 'Filter by name'}, + 'manufacturer_id': {'type': 'integer', 'description': 'Filter by manufacturer ID'} + } + }, + 'dcim_get_platform': { + 'description': 'Get a specific platform by ID', + 'properties': {'id': {'type': 'integer', 'description': 'Platform ID'}}, + 'required': ['id'] + }, + 'dcim_create_platform': { + 'description': 'Create a new platform', + 'properties': { + 'name': {'type': 'string', 'description': 'Platform name'}, + 'slug': {'type': 'string', 'description': 'URL-friendly slug'}, + 'manufacturer': {'type': 'integer', 'description': 'Manufacturer ID'} + }, + 'required': ['name', 'slug'] + }, + 'dcim_update_platform': { + 'description': 'Update an existing platform', + 'properties': {'id': {'type': 'integer', 'description': 'Platform ID'}}, + 'required': ['id'] + }, + 'dcim_delete_platform': { + 'description': 'Delete a platform', + 'properties': {'id': {'type': 'integer', 'description': 'Platform ID'}}, + 'required': ['id'] + }, + 'dcim_list_devices': { + 'description': 'List all devices in NetBox', + 'properties': { + 'name': {'type': 'string', 'description': 'Filter by name'}, + 'site_id': {'type': 'integer', 'description': 'Filter by site ID'}, + 'rack_id': {'type': 'integer', 'description': 'Filter by rack ID'}, + 'status': {'type': 'string', 'description': 'Filter by status'}, + 'role_id': {'type': 'integer', 'description': 'Filter by role ID'}, + 'device_type_id': {'type': 'integer', 'description': 'Filter by device type ID'}, + 'manufacturer_id': {'type': 'integer', 'description': 'Filter by manufacturer ID'}, + 'serial': {'type': 'string', 'description': 'Filter by serial number'} + } + }, + 'dcim_get_device': { + 'description': 'Get a specific device by ID', + 'properties': {'id': {'type': 'integer', 'description': 'Device ID'}}, + 'required': ['id'] + }, + 'dcim_create_device': { + 'description': 'Create a new device', + 'properties': { + 'name': {'type': 'string', 'description': 'Device name'}, + 'device_type': {'type': 'integer', 'description': 'Device type ID'}, + 'role': {'type': 'integer', 'description': 'Device role ID'}, + 'site': {'type': 'integer', 'description': 'Site ID'}, + 'status': {'type': 'string', 'description': 'Device status'}, + 'rack': {'type': 'integer', 'description': 'Rack ID'}, + 'position': {'type': 'number', 'description': 'Position in rack'}, + 'serial': {'type': 'string', 'description': 'Serial number'} + }, + 'required': ['name', 'device_type', 'role', 'site'] + }, + 'dcim_update_device': { + 'description': 'Update an existing device', + 'properties': { + 'id': {'type': 'integer', 'description': 'Device ID'}, + 'name': {'type': 'string', 'description': 'New name'}, + 'status': {'type': 'string', 'description': 'New status'} + }, + 'required': ['id'] + }, + 'dcim_delete_device': { + 'description': 'Delete a device', + 'properties': {'id': {'type': 'integer', 'description': 'Device ID'}}, + 'required': ['id'] + }, + 'dcim_list_interfaces': { + 'description': 'List all device interfaces in NetBox', + 'properties': { + 'device_id': {'type': 'integer', 'description': 'Filter by device ID'}, + 'name': {'type': 'string', 'description': 'Filter by name'}, + 'type': {'type': 'string', 'description': 'Filter by interface type'} + } + }, + 'dcim_get_interface': { + 'description': 'Get a specific interface by ID', + 'properties': {'id': {'type': 'integer', 'description': 'Interface ID'}}, + 'required': ['id'] + }, + 'dcim_create_interface': { + 'description': 'Create a new device interface', + 'properties': { + 'device': {'type': 'integer', 'description': 'Device ID'}, + 'name': {'type': 'string', 'description': 'Interface name'}, + 'type': {'type': 'string', 'description': 'Interface type (e.g., 1000base-t, 10gbase-x-sfpp)'}, + 'enabled': {'type': 'boolean', 'description': 'Interface enabled'}, + 'mac_address': {'type': 'string', 'description': 'MAC address'} + }, + 'required': ['device', 'name', 'type'] + }, + 'dcim_update_interface': { + 'description': 'Update an existing interface', + 'properties': {'id': {'type': 'integer', 'description': 'Interface ID'}}, + 'required': ['id'] + }, + 'dcim_delete_interface': { + 'description': 'Delete an interface', + 'properties': {'id': {'type': 'integer', 'description': 'Interface ID'}}, + 'required': ['id'] + }, + 'dcim_list_cables': { + 'description': 'List all cables in NetBox', + 'properties': { + 'site_id': {'type': 'integer', 'description': 'Filter by site ID'}, + 'device_id': {'type': 'integer', 'description': 'Filter by device ID'}, + 'status': {'type': 'string', 'description': 'Filter by status'} + } + }, + 'dcim_get_cable': { + 'description': 'Get a specific cable by ID', + 'properties': {'id': {'type': 'integer', 'description': 'Cable ID'}}, + 'required': ['id'] + }, + 'dcim_create_cable': { + 'description': 'Create a new cable connection', + 'properties': { + 'a_terminations': {'type': 'array', 'description': 'A-side terminations [{object_type, object_id}]'}, + 'b_terminations': {'type': 'array', 'description': 'B-side terminations [{object_type, object_id}]'}, + 'type': {'type': 'string', 'description': 'Cable type'}, + 'status': {'type': 'string', 'description': 'Cable status'}, + 'label': {'type': 'string', 'description': 'Cable label'} + }, + 'required': ['a_terminations', 'b_terminations'] + }, + 'dcim_update_cable': { + 'description': 'Update an existing cable', + 'properties': {'id': {'type': 'integer', 'description': 'Cable ID'}}, + 'required': ['id'] + }, + 'dcim_delete_cable': { + 'description': 'Delete a cable', + 'properties': {'id': {'type': 'integer', 'description': 'Cable ID'}}, + 'required': ['id'] + }, + 'dcim_list_power_panels': { + 'description': 'List all power panels in NetBox', + 'properties': {'site_id': {'type': 'integer', 'description': 'Filter by site ID'}} + }, + 'dcim_get_power_panel': { + 'description': 'Get a specific power panel by ID', + 'properties': {'id': {'type': 'integer', 'description': 'Power panel ID'}}, + 'required': ['id'] + }, + 'dcim_create_power_panel': { + 'description': 'Create a new power panel', + 'properties': { + 'site': {'type': 'integer', 'description': 'Site ID'}, + 'name': {'type': 'string', 'description': 'Panel name'}, + 'location': {'type': 'integer', 'description': 'Location ID'} + }, + 'required': ['site', 'name'] + }, + 'dcim_list_power_feeds': { + 'description': 'List all power feeds in NetBox', + 'properties': {'power_panel_id': {'type': 'integer', 'description': 'Filter by power panel ID'}} + }, + 'dcim_get_power_feed': { + 'description': 'Get a specific power feed by ID', + 'properties': {'id': {'type': 'integer', 'description': 'Power feed ID'}}, + 'required': ['id'] + }, + 'dcim_create_power_feed': { + 'description': 'Create a new power feed', + 'properties': { + 'power_panel': {'type': 'integer', 'description': 'Power panel ID'}, + 'name': {'type': 'string', 'description': 'Feed name'}, + 'voltage': {'type': 'integer', 'description': 'Voltage'}, + 'amperage': {'type': 'integer', 'description': 'Amperage'} + }, + 'required': ['power_panel', 'name'] + }, + 'dcim_list_virtual_chassis': { + 'description': 'List all virtual chassis in NetBox', + 'properties': {} + }, + 'dcim_get_virtual_chassis': { + 'description': 'Get a specific virtual chassis by ID', + 'properties': {'id': {'type': 'integer', 'description': 'Virtual chassis ID'}}, + 'required': ['id'] + }, + 'dcim_list_inventory_items': { + 'description': 'List all inventory items in NetBox', + 'properties': {'device_id': {'type': 'integer', 'description': 'Filter by device ID'}} + }, + 'dcim_get_inventory_item': { + 'description': 'Get a specific inventory item by ID', + 'properties': {'id': {'type': 'integer', 'description': 'Inventory item ID'}}, + 'required': ['id'] + }, + + # ==================== IPAM Tools ==================== + 'ipam_list_vrfs': { + 'description': 'List all VRFs in NetBox', + 'properties': { + 'name': {'type': 'string', 'description': 'Filter by name'}, + 'rd': {'type': 'string', 'description': 'Filter by route distinguisher'} + } + }, + 'ipam_get_vrf': { + 'description': 'Get a specific VRF by ID', + 'properties': {'id': {'type': 'integer', 'description': 'VRF ID'}}, + 'required': ['id'] + }, + 'ipam_create_vrf': { + 'description': 'Create a new VRF', + 'properties': { + 'name': {'type': 'string', 'description': 'VRF name'}, + 'rd': {'type': 'string', 'description': 'Route distinguisher'}, + 'tenant': {'type': 'integer', 'description': 'Tenant ID'} + }, + 'required': ['name'] + }, + 'ipam_update_vrf': { + 'description': 'Update an existing VRF', + 'properties': {'id': {'type': 'integer', 'description': 'VRF ID'}}, + 'required': ['id'] + }, + 'ipam_delete_vrf': { + 'description': 'Delete a VRF', + 'properties': {'id': {'type': 'integer', 'description': 'VRF ID'}}, + 'required': ['id'] + }, + 'ipam_list_prefixes': { + 'description': 'List all IP prefixes in NetBox', + 'properties': { + 'prefix': {'type': 'string', 'description': 'Filter by prefix (CIDR)'}, + 'site_id': {'type': 'integer', 'description': 'Filter by site ID'}, + 'vrf_id': {'type': 'integer', 'description': 'Filter by VRF ID'}, + 'vlan_id': {'type': 'integer', 'description': 'Filter by VLAN ID'}, + 'status': {'type': 'string', 'description': 'Filter by status'}, + 'within': {'type': 'string', 'description': 'Find prefixes within this prefix'} + } + }, + 'ipam_get_prefix': { + 'description': 'Get a specific prefix by ID', + 'properties': {'id': {'type': 'integer', 'description': 'Prefix ID'}}, + 'required': ['id'] + }, + 'ipam_create_prefix': { + 'description': 'Create a new IP prefix', + 'properties': { + 'prefix': {'type': 'string', 'description': 'Prefix in CIDR notation'}, + 'status': {'type': 'string', 'description': 'Status (active, container, reserved, deprecated)'}, + 'site': {'type': 'integer', 'description': 'Site ID'}, + 'vrf': {'type': 'integer', 'description': 'VRF ID'}, + 'vlan': {'type': 'integer', 'description': 'VLAN ID'}, + 'role': {'type': 'integer', 'description': 'Role ID'}, + 'is_pool': {'type': 'boolean', 'description': 'Is a pool'} + }, + 'required': ['prefix'] + }, + 'ipam_update_prefix': { + 'description': 'Update an existing prefix', + 'properties': {'id': {'type': 'integer', 'description': 'Prefix ID'}}, + 'required': ['id'] + }, + 'ipam_delete_prefix': { + 'description': 'Delete a prefix', + 'properties': {'id': {'type': 'integer', 'description': 'Prefix ID'}}, + 'required': ['id'] + }, + 'ipam_list_available_prefixes': { + 'description': 'List available child prefixes within a prefix', + 'properties': {'id': {'type': 'integer', 'description': 'Parent prefix ID'}}, + 'required': ['id'] + }, + 'ipam_create_available_prefix': { + 'description': 'Create a new prefix from available space', + 'properties': { + 'id': {'type': 'integer', 'description': 'Parent prefix ID'}, + 'prefix_length': {'type': 'integer', 'description': 'Desired prefix length'} + }, + 'required': ['id', 'prefix_length'] + }, + 'ipam_list_ip_addresses': { + 'description': 'List all IP addresses in NetBox', + 'properties': { + 'address': {'type': 'string', 'description': 'Filter by address'}, + 'vrf_id': {'type': 'integer', 'description': 'Filter by VRF ID'}, + 'device_id': {'type': 'integer', 'description': 'Filter by device ID'}, + 'virtual_machine_id': {'type': 'integer', 'description': 'Filter by VM ID'}, + 'status': {'type': 'string', 'description': 'Filter by status'}, + 'dns_name': {'type': 'string', 'description': 'Filter by DNS name'} + } + }, + 'ipam_get_ip_address': { + 'description': 'Get a specific IP address by ID', + 'properties': {'id': {'type': 'integer', 'description': 'IP address ID'}}, + 'required': ['id'] + }, + 'ipam_create_ip_address': { + 'description': 'Create a new IP address', + 'properties': { + 'address': {'type': 'string', 'description': 'IP address with prefix length'}, + 'status': {'type': 'string', 'description': 'Status'}, + 'vrf': {'type': 'integer', 'description': 'VRF ID'}, + 'dns_name': {'type': 'string', 'description': 'DNS name'}, + 'assigned_object_type': {'type': 'string', 'description': 'Object type to assign to'}, + 'assigned_object_id': {'type': 'integer', 'description': 'Object ID to assign to'} + }, + 'required': ['address'] + }, + 'ipam_update_ip_address': { + 'description': 'Update an existing IP address', + 'properties': {'id': {'type': 'integer', 'description': 'IP address ID'}}, + 'required': ['id'] + }, + 'ipam_delete_ip_address': { + 'description': 'Delete an IP address', + 'properties': {'id': {'type': 'integer', 'description': 'IP address ID'}}, + 'required': ['id'] + }, + 'ipam_list_available_ips': { + 'description': 'List available IP addresses within a prefix', + 'properties': {'id': {'type': 'integer', 'description': 'Prefix ID'}}, + 'required': ['id'] + }, + 'ipam_create_available_ip': { + 'description': 'Create a new IP address from available space in prefix', + 'properties': {'id': {'type': 'integer', 'description': 'Prefix ID'}}, + 'required': ['id'] + }, + 'ipam_list_vlan_groups': { + 'description': 'List all VLAN groups in NetBox', + 'properties': {'name': {'type': 'string', 'description': 'Filter by name'}} + }, + 'ipam_get_vlan_group': { + 'description': 'Get a specific VLAN group by ID', + 'properties': {'id': {'type': 'integer', 'description': 'VLAN group ID'}}, + 'required': ['id'] + }, + 'ipam_create_vlan_group': { + 'description': 'Create a new VLAN group', + 'properties': { + 'name': {'type': 'string', 'description': 'Group name'}, + 'slug': {'type': 'string', 'description': 'URL-friendly slug'}, + 'min_vid': {'type': 'integer', 'description': 'Minimum VLAN ID'}, + 'max_vid': {'type': 'integer', 'description': 'Maximum VLAN ID'} + }, + 'required': ['name', 'slug'] + }, + 'ipam_list_vlans': { + 'description': 'List all VLANs in NetBox', + 'properties': { + 'vid': {'type': 'integer', 'description': 'Filter by VLAN ID'}, + 'name': {'type': 'string', 'description': 'Filter by name'}, + 'site_id': {'type': 'integer', 'description': 'Filter by site ID'}, + 'group_id': {'type': 'integer', 'description': 'Filter by VLAN group ID'}, + 'status': {'type': 'string', 'description': 'Filter by status'} + } + }, + 'ipam_get_vlan': { + 'description': 'Get a specific VLAN by ID', + 'properties': {'id': {'type': 'integer', 'description': 'VLAN ID'}}, + 'required': ['id'] + }, + 'ipam_create_vlan': { + 'description': 'Create a new VLAN', + 'properties': { + 'vid': {'type': 'integer', 'description': 'VLAN ID number'}, + 'name': {'type': 'string', 'description': 'VLAN name'}, + 'status': {'type': 'string', 'description': 'Status'}, + 'site': {'type': 'integer', 'description': 'Site ID'}, + 'group': {'type': 'integer', 'description': 'VLAN group ID'} + }, + 'required': ['vid', 'name'] + }, + 'ipam_update_vlan': { + 'description': 'Update an existing VLAN', + 'properties': {'id': {'type': 'integer', 'description': 'VLAN ID'}}, + 'required': ['id'] + }, + 'ipam_delete_vlan': { + 'description': 'Delete a VLAN', + 'properties': {'id': {'type': 'integer', 'description': 'VLAN ID'}}, + 'required': ['id'] + }, + 'ipam_list_asns': { + 'description': 'List all ASNs in NetBox', + 'properties': { + 'asn': {'type': 'integer', 'description': 'Filter by ASN number'}, + 'rir_id': {'type': 'integer', 'description': 'Filter by RIR ID'} + } + }, + 'ipam_get_asn': { + 'description': 'Get a specific ASN by ID', + 'properties': {'id': {'type': 'integer', 'description': 'ASN ID'}}, + 'required': ['id'] + }, + 'ipam_create_asn': { + 'description': 'Create a new ASN', + 'properties': { + 'asn': {'type': 'integer', 'description': 'ASN number'}, + 'rir': {'type': 'integer', 'description': 'RIR ID'} + }, + 'required': ['asn', 'rir'] + }, + 'ipam_list_rirs': { + 'description': 'List all RIRs in NetBox', + 'properties': {'name': {'type': 'string', 'description': 'Filter by name'}} + }, + 'ipam_get_rir': { + 'description': 'Get a specific RIR by ID', + 'properties': {'id': {'type': 'integer', 'description': 'RIR ID'}}, + 'required': ['id'] + }, + 'ipam_list_aggregates': { + 'description': 'List all aggregates in NetBox', + 'properties': { + 'prefix': {'type': 'string', 'description': 'Filter by prefix'}, + 'rir_id': {'type': 'integer', 'description': 'Filter by RIR ID'} + } + }, + 'ipam_get_aggregate': { + 'description': 'Get a specific aggregate by ID', + 'properties': {'id': {'type': 'integer', 'description': 'Aggregate ID'}}, + 'required': ['id'] + }, + 'ipam_create_aggregate': { + 'description': 'Create a new aggregate', + 'properties': { + 'prefix': {'type': 'string', 'description': 'Prefix in CIDR notation'}, + 'rir': {'type': 'integer', 'description': 'RIR ID'} + }, + 'required': ['prefix', 'rir'] + }, + 'ipam_list_services': { + 'description': 'List all services in NetBox', + 'properties': { + 'device_id': {'type': 'integer', 'description': 'Filter by device ID'}, + 'virtual_machine_id': {'type': 'integer', 'description': 'Filter by VM ID'}, + 'name': {'type': 'string', 'description': 'Filter by name'} + } + }, + 'ipam_get_service': { + 'description': 'Get a specific service by ID', + 'properties': {'id': {'type': 'integer', 'description': 'Service ID'}}, + 'required': ['id'] + }, + 'ipam_create_service': { + 'description': 'Create a new service', + 'properties': { + 'name': {'type': 'string', 'description': 'Service name'}, + 'ports': {'type': 'array', 'description': 'List of ports'}, + 'protocol': {'type': 'string', 'description': 'Protocol (tcp/udp)'}, + 'device': {'type': 'integer', 'description': 'Device ID'}, + 'virtual_machine': {'type': 'integer', 'description': 'VM ID'} + }, + 'required': ['name', 'ports', 'protocol'] + }, + + # ==================== Circuits Tools ==================== + 'circuits_list_providers': { + 'description': 'List all circuit providers in NetBox', + 'properties': {'name': {'type': 'string', 'description': 'Filter by name'}} + }, + 'circuits_get_provider': { + 'description': 'Get a specific provider by ID', + 'properties': {'id': {'type': 'integer', 'description': 'Provider ID'}}, + 'required': ['id'] + }, + 'circuits_create_provider': { + 'description': 'Create a new circuit provider', + 'properties': { + 'name': {'type': 'string', 'description': 'Provider name'}, + 'slug': {'type': 'string', 'description': 'URL-friendly slug'} + }, + 'required': ['name', 'slug'] + }, + 'circuits_update_provider': { + 'description': 'Update an existing provider', + 'properties': {'id': {'type': 'integer', 'description': 'Provider ID'}}, + 'required': ['id'] + }, + 'circuits_delete_provider': { + 'description': 'Delete a provider', + 'properties': {'id': {'type': 'integer', 'description': 'Provider ID'}}, + 'required': ['id'] + }, + 'circuits_list_circuit_types': { + 'description': 'List all circuit types in NetBox', + 'properties': {'name': {'type': 'string', 'description': 'Filter by name'}} + }, + 'circuits_get_circuit_type': { + 'description': 'Get a specific circuit type by ID', + 'properties': {'id': {'type': 'integer', 'description': 'Circuit type ID'}}, + 'required': ['id'] + }, + 'circuits_create_circuit_type': { + 'description': 'Create a new circuit type', + 'properties': { + 'name': {'type': 'string', 'description': 'Type name'}, + 'slug': {'type': 'string', 'description': 'URL-friendly slug'} + }, + 'required': ['name', 'slug'] + }, + 'circuits_list_circuits': { + 'description': 'List all circuits in NetBox', + 'properties': { + 'cid': {'type': 'string', 'description': 'Filter by circuit ID'}, + 'provider_id': {'type': 'integer', 'description': 'Filter by provider ID'}, + 'type_id': {'type': 'integer', 'description': 'Filter by type ID'}, + 'status': {'type': 'string', 'description': 'Filter by status'} + } + }, + 'circuits_get_circuit': { + 'description': 'Get a specific circuit by ID', + 'properties': {'id': {'type': 'integer', 'description': 'Circuit ID'}}, + 'required': ['id'] + }, + 'circuits_create_circuit': { + 'description': 'Create a new circuit', + 'properties': { + 'cid': {'type': 'string', 'description': 'Circuit ID'}, + 'provider': {'type': 'integer', 'description': 'Provider ID'}, + 'type': {'type': 'integer', 'description': 'Circuit type ID'}, + 'status': {'type': 'string', 'description': 'Status'}, + 'tenant': {'type': 'integer', 'description': 'Tenant ID'} + }, + 'required': ['cid', 'provider', 'type'] + }, + 'circuits_update_circuit': { + 'description': 'Update an existing circuit', + 'properties': {'id': {'type': 'integer', 'description': 'Circuit ID'}}, + 'required': ['id'] + }, + 'circuits_delete_circuit': { + 'description': 'Delete a circuit', + 'properties': {'id': {'type': 'integer', 'description': 'Circuit ID'}}, + 'required': ['id'] + }, + 'circuits_list_circuit_terminations': { + 'description': 'List all circuit terminations in NetBox', + 'properties': { + 'circuit_id': {'type': 'integer', 'description': 'Filter by circuit ID'}, + 'site_id': {'type': 'integer', 'description': 'Filter by site ID'} + } + }, + 'circuits_get_circuit_termination': { + 'description': 'Get a specific circuit termination by ID', + 'properties': {'id': {'type': 'integer', 'description': 'Termination ID'}}, + 'required': ['id'] + }, + 'circuits_create_circuit_termination': { + 'description': 'Create a new circuit termination', + 'properties': { + 'circuit': {'type': 'integer', 'description': 'Circuit ID'}, + 'term_side': {'type': 'string', 'description': 'Termination side (A/Z)'}, + 'site': {'type': 'integer', 'description': 'Site ID'} + }, + 'required': ['circuit', 'term_side'] + }, + + # ==================== Virtualization Tools ==================== + 'virtualization_list_cluster_types': { + 'description': 'List all cluster types in NetBox', + 'properties': {'name': {'type': 'string', 'description': 'Filter by name'}} + }, + 'virtualization_get_cluster_type': { + 'description': 'Get a specific cluster type by ID', + 'properties': {'id': {'type': 'integer', 'description': 'Cluster type ID'}}, + 'required': ['id'] + }, + 'virtualization_create_cluster_type': { + 'description': 'Create a new cluster type', + 'properties': { + 'name': {'type': 'string', 'description': 'Type name'}, + 'slug': {'type': 'string', 'description': 'URL-friendly slug'} + }, + 'required': ['name', 'slug'] + }, + 'virtualization_list_cluster_groups': { + 'description': 'List all cluster groups in NetBox', + 'properties': {'name': {'type': 'string', 'description': 'Filter by name'}} + }, + 'virtualization_get_cluster_group': { + 'description': 'Get a specific cluster group by ID', + 'properties': {'id': {'type': 'integer', 'description': 'Cluster group ID'}}, + 'required': ['id'] + }, + 'virtualization_create_cluster_group': { + 'description': 'Create a new cluster group', + 'properties': { + 'name': {'type': 'string', 'description': 'Group name'}, + 'slug': {'type': 'string', 'description': 'URL-friendly slug'} + }, + 'required': ['name', 'slug'] + }, + 'virtualization_list_clusters': { + 'description': 'List all clusters in NetBox', + 'properties': { + 'name': {'type': 'string', 'description': 'Filter by name'}, + 'type_id': {'type': 'integer', 'description': 'Filter by type ID'}, + 'group_id': {'type': 'integer', 'description': 'Filter by group ID'}, + 'site_id': {'type': 'integer', 'description': 'Filter by site ID'} + } + }, + 'virtualization_get_cluster': { + 'description': 'Get a specific cluster by ID', + 'properties': {'id': {'type': 'integer', 'description': 'Cluster ID'}}, + 'required': ['id'] + }, + 'virtualization_create_cluster': { + 'description': 'Create a new cluster', + 'properties': { + 'name': {'type': 'string', 'description': 'Cluster name'}, + 'type': {'type': 'integer', 'description': 'Cluster type ID'}, + 'group': {'type': 'integer', 'description': 'Cluster group ID'}, + 'site': {'type': 'integer', 'description': 'Site ID'}, + 'status': {'type': 'string', 'description': 'Status'} + }, + 'required': ['name', 'type'] + }, + 'virtualization_update_cluster': { + 'description': 'Update an existing cluster', + 'properties': {'id': {'type': 'integer', 'description': 'Cluster ID'}}, + 'required': ['id'] + }, + 'virtualization_delete_cluster': { + 'description': 'Delete a cluster', + 'properties': {'id': {'type': 'integer', 'description': 'Cluster ID'}}, + 'required': ['id'] + }, + 'virtualization_list_virtual_machines': { + 'description': 'List all virtual machines in NetBox', + 'properties': { + 'name': {'type': 'string', 'description': 'Filter by name'}, + 'cluster_id': {'type': 'integer', 'description': 'Filter by cluster ID'}, + 'site_id': {'type': 'integer', 'description': 'Filter by site ID'}, + 'status': {'type': 'string', 'description': 'Filter by status'} + } + }, + 'virtualization_get_virtual_machine': { + 'description': 'Get a specific virtual machine by ID', + 'properties': {'id': {'type': 'integer', 'description': 'VM ID'}}, + 'required': ['id'] + }, + 'virtualization_create_virtual_machine': { + 'description': 'Create a new virtual machine', + 'properties': { + 'name': {'type': 'string', 'description': 'VM name'}, + 'cluster': {'type': 'integer', 'description': 'Cluster ID'}, + 'status': {'type': 'string', 'description': 'Status'}, + 'role': {'type': 'integer', 'description': 'Role ID'}, + 'vcpus': {'type': 'number', 'description': 'Number of vCPUs'}, + 'memory': {'type': 'integer', 'description': 'Memory in MB'}, + 'disk': {'type': 'integer', 'description': 'Disk in GB'} + }, + 'required': ['name'] + }, + 'virtualization_update_virtual_machine': { + 'description': 'Update an existing virtual machine', + 'properties': {'id': {'type': 'integer', 'description': 'VM ID'}}, + 'required': ['id'] + }, + 'virtualization_delete_virtual_machine': { + 'description': 'Delete a virtual machine', + 'properties': {'id': {'type': 'integer', 'description': 'VM ID'}}, + 'required': ['id'] + }, + 'virtualization_list_vm_interfaces': { + 'description': 'List all VM interfaces in NetBox', + 'properties': { + 'virtual_machine_id': {'type': 'integer', 'description': 'Filter by VM ID'}, + 'name': {'type': 'string', 'description': 'Filter by name'} + } + }, + 'virtualization_get_vm_interface': { + 'description': 'Get a specific VM interface by ID', + 'properties': {'id': {'type': 'integer', 'description': 'Interface ID'}}, + 'required': ['id'] + }, + 'virtualization_create_vm_interface': { + 'description': 'Create a new VM interface', + 'properties': { + 'virtual_machine': {'type': 'integer', 'description': 'VM ID'}, + 'name': {'type': 'string', 'description': 'Interface name'}, + 'enabled': {'type': 'boolean', 'description': 'Enabled'} + }, + 'required': ['virtual_machine', 'name'] + }, + + # ==================== Tenancy Tools ==================== + 'tenancy_list_tenant_groups': { + 'description': 'List all tenant groups in NetBox', + 'properties': {'name': {'type': 'string', 'description': 'Filter by name'}} + }, + 'tenancy_get_tenant_group': { + 'description': 'Get a specific tenant group by ID', + 'properties': {'id': {'type': 'integer', 'description': 'Tenant group ID'}}, + 'required': ['id'] + }, + 'tenancy_create_tenant_group': { + 'description': 'Create a new tenant group', + 'properties': { + 'name': {'type': 'string', 'description': 'Group name'}, + 'slug': {'type': 'string', 'description': 'URL-friendly slug'} + }, + 'required': ['name', 'slug'] + }, + 'tenancy_list_tenants': { + 'description': 'List all tenants in NetBox', + 'properties': { + 'name': {'type': 'string', 'description': 'Filter by name'}, + 'group_id': {'type': 'integer', 'description': 'Filter by group ID'} + } + }, + 'tenancy_get_tenant': { + 'description': 'Get a specific tenant by ID', + 'properties': {'id': {'type': 'integer', 'description': 'Tenant ID'}}, + 'required': ['id'] + }, + 'tenancy_create_tenant': { + 'description': 'Create a new tenant', + 'properties': { + 'name': {'type': 'string', 'description': 'Tenant name'}, + 'slug': {'type': 'string', 'description': 'URL-friendly slug'}, + 'group': {'type': 'integer', 'description': 'Tenant group ID'} + }, + 'required': ['name', 'slug'] + }, + 'tenancy_update_tenant': { + 'description': 'Update an existing tenant', + 'properties': {'id': {'type': 'integer', 'description': 'Tenant ID'}}, + 'required': ['id'] + }, + 'tenancy_delete_tenant': { + 'description': 'Delete a tenant', + 'properties': {'id': {'type': 'integer', 'description': 'Tenant ID'}}, + 'required': ['id'] + }, + 'tenancy_list_contacts': { + 'description': 'List all contacts in NetBox', + 'properties': { + 'name': {'type': 'string', 'description': 'Filter by name'}, + 'email': {'type': 'string', 'description': 'Filter by email'} + } + }, + 'tenancy_get_contact': { + 'description': 'Get a specific contact by ID', + 'properties': {'id': {'type': 'integer', 'description': 'Contact ID'}}, + 'required': ['id'] + }, + 'tenancy_create_contact': { + 'description': 'Create a new contact', + 'properties': { + 'name': {'type': 'string', 'description': 'Contact name'}, + 'email': {'type': 'string', 'description': 'Email address'}, + 'phone': {'type': 'string', 'description': 'Phone number'} + }, + 'required': ['name'] + }, + + # ==================== VPN Tools ==================== + 'vpn_list_tunnels': { + 'description': 'List all VPN tunnels in NetBox', + 'properties': { + 'name': {'type': 'string', 'description': 'Filter by name'}, + 'status': {'type': 'string', 'description': 'Filter by status'} + } + }, + 'vpn_get_tunnel': { + 'description': 'Get a specific tunnel by ID', + 'properties': {'id': {'type': 'integer', 'description': 'Tunnel ID'}}, + 'required': ['id'] + }, + 'vpn_create_tunnel': { + 'description': 'Create a new VPN tunnel', + 'properties': { + 'name': {'type': 'string', 'description': 'Tunnel name'}, + 'status': {'type': 'string', 'description': 'Status'}, + 'encapsulation': {'type': 'string', 'description': 'Encapsulation type'} + }, + 'required': ['name'] + }, + 'vpn_list_l2vpns': { + 'description': 'List all L2VPNs in NetBox', + 'properties': { + 'name': {'type': 'string', 'description': 'Filter by name'}, + 'type': {'type': 'string', 'description': 'Filter by type'} + } + }, + 'vpn_get_l2vpn': { + 'description': 'Get a specific L2VPN by ID', + 'properties': {'id': {'type': 'integer', 'description': 'L2VPN ID'}}, + 'required': ['id'] + }, + 'vpn_create_l2vpn': { + 'description': 'Create a new L2VPN', + 'properties': { + 'name': {'type': 'string', 'description': 'L2VPN name'}, + 'slug': {'type': 'string', 'description': 'URL-friendly slug'}, + 'type': {'type': 'string', 'description': 'Type'} + }, + 'required': ['name', 'slug', 'type'] + }, + 'vpn_list_ike_policies': { + 'description': 'List all IKE policies in NetBox', + 'properties': {'name': {'type': 'string', 'description': 'Filter by name'}} + }, + 'vpn_list_ipsec_policies': { + 'description': 'List all IPSec policies in NetBox', + 'properties': {'name': {'type': 'string', 'description': 'Filter by name'}} + }, + 'vpn_list_ipsec_profiles': { + 'description': 'List all IPSec profiles in NetBox', + 'properties': {'name': {'type': 'string', 'description': 'Filter by name'}} + }, + + # ==================== Wireless Tools ==================== + 'wireless_list_wireless_lan_groups': { + 'description': 'List all wireless LAN groups in NetBox', + 'properties': {'name': {'type': 'string', 'description': 'Filter by name'}} + }, + 'wireless_get_wireless_lan_group': { + 'description': 'Get a specific wireless LAN group by ID', + 'properties': {'id': {'type': 'integer', 'description': 'WLAN group ID'}}, + 'required': ['id'] + }, + 'wireless_create_wireless_lan_group': { + 'description': 'Create a new wireless LAN group', + 'properties': { + 'name': {'type': 'string', 'description': 'Group name'}, + 'slug': {'type': 'string', 'description': 'URL-friendly slug'} + }, + 'required': ['name', 'slug'] + }, + 'wireless_list_wireless_lans': { + 'description': 'List all wireless LANs in NetBox', + 'properties': { + 'ssid': {'type': 'string', 'description': 'Filter by SSID'}, + 'group_id': {'type': 'integer', 'description': 'Filter by group ID'}, + 'status': {'type': 'string', 'description': 'Filter by status'} + } + }, + 'wireless_get_wireless_lan': { + 'description': 'Get a specific wireless LAN by ID', + 'properties': {'id': {'type': 'integer', 'description': 'WLAN ID'}}, + 'required': ['id'] + }, + 'wireless_create_wireless_lan': { + 'description': 'Create a new wireless LAN', + 'properties': { + 'ssid': {'type': 'string', 'description': 'SSID'}, + 'status': {'type': 'string', 'description': 'Status'}, + 'group': {'type': 'integer', 'description': 'Group ID'}, + 'vlan': {'type': 'integer', 'description': 'VLAN ID'} + }, + 'required': ['ssid'] + }, + 'wireless_list_wireless_links': { + 'description': 'List all wireless links in NetBox', + 'properties': { + 'ssid': {'type': 'string', 'description': 'Filter by SSID'}, + 'status': {'type': 'string', 'description': 'Filter by status'} + } + }, + 'wireless_get_wireless_link': { + 'description': 'Get a specific wireless link by ID', + 'properties': {'id': {'type': 'integer', 'description': 'Link ID'}}, + 'required': ['id'] + }, + + # ==================== Extras Tools ==================== + 'extras_list_tags': { + 'description': 'List all tags in NetBox', + 'properties': { + 'name': {'type': 'string', 'description': 'Filter by name'}, + 'slug': {'type': 'string', 'description': 'Filter by slug'} + } + }, + 'extras_get_tag': { + 'description': 'Get a specific tag by ID', + 'properties': {'id': {'type': 'integer', 'description': 'Tag ID'}}, + 'required': ['id'] + }, + 'extras_create_tag': { + 'description': 'Create a new tag', + 'properties': { + 'name': {'type': 'string', 'description': 'Tag name'}, + 'slug': {'type': 'string', 'description': 'URL-friendly slug'}, + 'color': {'type': 'string', 'description': 'Hex color code'} + }, + 'required': ['name', 'slug'] + }, + 'extras_update_tag': { + 'description': 'Update an existing tag', + 'properties': {'id': {'type': 'integer', 'description': 'Tag ID'}}, + 'required': ['id'] + }, + 'extras_delete_tag': { + 'description': 'Delete a tag', + 'properties': {'id': {'type': 'integer', 'description': 'Tag ID'}}, + 'required': ['id'] + }, + 'extras_list_custom_fields': { + 'description': 'List all custom fields in NetBox', + 'properties': { + 'name': {'type': 'string', 'description': 'Filter by name'}, + 'type': {'type': 'string', 'description': 'Filter by type'} + } + }, + 'extras_get_custom_field': { + 'description': 'Get a specific custom field by ID', + 'properties': {'id': {'type': 'integer', 'description': 'Custom field ID'}}, + 'required': ['id'] + }, + 'extras_list_webhooks': { + 'description': 'List all webhooks in NetBox', + 'properties': {'name': {'type': 'string', 'description': 'Filter by name'}} + }, + 'extras_get_webhook': { + 'description': 'Get a specific webhook by ID', + 'properties': {'id': {'type': 'integer', 'description': 'Webhook ID'}}, + 'required': ['id'] + }, + 'extras_list_journal_entries': { + 'description': 'List all journal entries in NetBox', + 'properties': { + 'assigned_object_type': {'type': 'string', 'description': 'Filter by object type'}, + 'assigned_object_id': {'type': 'integer', 'description': 'Filter by object ID'} + } + }, + 'extras_get_journal_entry': { + 'description': 'Get a specific journal entry by ID', + 'properties': {'id': {'type': 'integer', 'description': 'Journal entry ID'}}, + 'required': ['id'] + }, + 'extras_create_journal_entry': { + 'description': 'Create a new journal entry', + 'properties': { + 'assigned_object_type': {'type': 'string', 'description': 'Object type'}, + 'assigned_object_id': {'type': 'integer', 'description': 'Object ID'}, + 'comments': {'type': 'string', 'description': 'Comments'}, + 'kind': {'type': 'string', 'description': 'Kind (info, success, warning, danger)'} + }, + 'required': ['assigned_object_type', 'assigned_object_id', 'comments'] + }, + 'extras_list_config_contexts': { + 'description': 'List all config contexts in NetBox', + 'properties': {'name': {'type': 'string', 'description': 'Filter by name'}} + }, + 'extras_get_config_context': { + 'description': 'Get a specific config context by ID', + 'properties': {'id': {'type': 'integer', 'description': 'Config context ID'}}, + 'required': ['id'] + }, + 'extras_list_object_changes': { + 'description': 'List all object changes (audit log) in NetBox', + 'properties': { + 'user_id': {'type': 'integer', 'description': 'Filter by user ID'}, + 'changed_object_type': {'type': 'string', 'description': 'Filter by object type'}, + 'action': {'type': 'string', 'description': 'Filter by action (create, update, delete)'} + } + }, + 'extras_get_object_change': { + 'description': 'Get a specific object change by ID', + 'properties': {'id': {'type': 'integer', 'description': 'Object change ID'}}, + 'required': ['id'] + }, +} + + +class NetBoxMCPServer: + """MCP Server for NetBox integration""" + + def __init__(self): + self.server = Server("netbox-mcp") + self.config = None + self.client = None + self.dcim_tools = None + self.ipam_tools = None + self.circuits_tools = None + self.virtualization_tools = None + self.tenancy_tools = None + self.vpn_tools = None + self.wireless_tools = None + self.extras_tools = None + + async def initialize(self): + """Initialize server and load configuration.""" + try: + config_loader = NetBoxConfig() + self.config = config_loader.load() + + self.client = NetBoxClient() + self.dcim_tools = DCIMTools(self.client) + self.ipam_tools = IPAMTools(self.client) + self.circuits_tools = CircuitsTools(self.client) + self.virtualization_tools = VirtualizationTools(self.client) + self.tenancy_tools = TenancyTools(self.client) + self.vpn_tools = VPNTools(self.client) + self.wireless_tools = WirelessTools(self.client) + self.extras_tools = ExtrasTools(self.client) + + logger.info(f"NetBox MCP Server initialized for {self.config['api_url']}") + except Exception as e: + logger.error(f"Failed to initialize: {e}") + raise + + def setup_tools(self): + """Register all available tools with the MCP server""" + + @self.server.list_tools() + async def list_tools() -> list[Tool]: + """Return list of available tools""" + tools = [] + for name, definition in TOOL_DEFINITIONS.items(): + tools.append(Tool( + name=name, + description=definition['description'], + inputSchema={ + 'type': 'object', + 'properties': definition.get('properties', {}), + 'required': definition.get('required', []) + } + )) + return tools + + @self.server.call_tool() + async def call_tool(name: str, arguments: dict) -> list[TextContent]: + """Handle tool invocation.""" + try: + result = await self._route_tool(name, arguments) + return [TextContent( + type="text", + text=json.dumps(result, indent=2, default=str) + )] + except Exception as e: + logger.error(f"Tool {name} failed: {e}") + return [TextContent( + type="text", + text=f"Error: {str(e)}" + )] + + async def _route_tool(self, name: str, arguments: dict): + """Route tool call to appropriate handler.""" + parts = name.split('_', 1) + if len(parts) != 2: + raise ValueError(f"Invalid tool name format: {name}") + + category, method_name = parts[0], parts[1] + + # Map category to tool class + tool_map = { + 'dcim': self.dcim_tools, + 'ipam': self.ipam_tools, + 'circuits': self.circuits_tools, + 'virtualization': self.virtualization_tools, + 'tenancy': self.tenancy_tools, + 'vpn': self.vpn_tools, + 'wireless': self.wireless_tools, + 'extras': self.extras_tools + } + + tool_class = tool_map.get(category) + if not tool_class: + raise ValueError(f"Unknown tool category: {category}") + + # Get the method + method = getattr(tool_class, method_name, None) + if not method: + raise ValueError(f"Unknown method: {method_name} in {category}") + + # Call the method + return await method(**arguments) + + async def run(self): + """Run the MCP server""" + await self.initialize() + self.setup_tools() + + async with stdio_server() as (read_stream, write_stream): + await self.server.run( + read_stream, + write_stream, + self.server.create_initialization_options() + ) + + +async def main(): + """Main entry point""" + server = NetBoxMCPServer() + await server.run() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/mcp-servers/netbox/mcp_server/tools/__init__.py b/mcp-servers/netbox/mcp_server/tools/__init__.py new file mode 100644 index 0000000..bdf9cc3 --- /dev/null +++ b/mcp-servers/netbox/mcp_server/tools/__init__.py @@ -0,0 +1,20 @@ +"""NetBox MCP tools package.""" +from .dcim import DCIMTools +from .ipam import IPAMTools +from .circuits import CircuitsTools +from .virtualization import VirtualizationTools +from .tenancy import TenancyTools +from .vpn import VPNTools +from .wireless import WirelessTools +from .extras import ExtrasTools + +__all__ = [ + 'DCIMTools', + 'IPAMTools', + 'CircuitsTools', + 'VirtualizationTools', + 'TenancyTools', + 'VPNTools', + 'WirelessTools', + 'ExtrasTools', +] diff --git a/mcp-servers/netbox/mcp_server/tools/circuits.py b/mcp-servers/netbox/mcp_server/tools/circuits.py new file mode 100644 index 0000000..3e8c428 --- /dev/null +++ b/mcp-servers/netbox/mcp_server/tools/circuits.py @@ -0,0 +1,373 @@ +""" +Circuits tools for NetBox MCP Server. + +Covers: Providers, Circuits, Circuit Types, Circuit Terminations, and related models. +""" +import logging +from typing import List, Dict, Optional, Any +from ..netbox_client import NetBoxClient + +logger = logging.getLogger(__name__) + + +class CircuitsTools: + """Tools for Circuits operations in NetBox""" + + def __init__(self, client: NetBoxClient): + self.client = client + self.base_endpoint = 'circuits' + + # ==================== Providers ==================== + + async def list_providers( + self, + name: Optional[str] = None, + slug: Optional[str] = None, + **kwargs + ) -> List[Dict]: + """List all circuit providers.""" + params = {k: v for k, v in {'name': name, 'slug': slug, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/providers', params=params) + + async def get_provider(self, id: int) -> Dict: + """Get a specific provider by ID.""" + return self.client.get(f'{self.base_endpoint}/providers', id) + + async def create_provider( + self, + name: str, + slug: str, + asns: Optional[List[int]] = None, + description: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new provider.""" + data = {'name': name, 'slug': slug, **kwargs} + if asns: + data['asns'] = asns + if description: + data['description'] = description + return self.client.create(f'{self.base_endpoint}/providers', data) + + async def update_provider(self, id: int, **kwargs) -> Dict: + """Update a provider.""" + return self.client.patch(f'{self.base_endpoint}/providers', id, kwargs) + + async def delete_provider(self, id: int) -> None: + """Delete a provider.""" + self.client.delete(f'{self.base_endpoint}/providers', id) + + # ==================== Provider Accounts ==================== + + async def list_provider_accounts( + self, + provider_id: Optional[int] = None, + name: Optional[str] = None, + account: Optional[str] = None, + **kwargs + ) -> List[Dict]: + """List all provider accounts.""" + params = {k: v for k, v in { + 'provider_id': provider_id, 'name': name, 'account': account, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/provider-accounts', params=params) + + async def get_provider_account(self, id: int) -> Dict: + """Get a specific provider account by ID.""" + return self.client.get(f'{self.base_endpoint}/provider-accounts', id) + + async def create_provider_account( + self, + provider: int, + account: str, + name: Optional[str] = None, + description: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new provider account.""" + data = {'provider': provider, 'account': account, **kwargs} + if name: + data['name'] = name + if description: + data['description'] = description + return self.client.create(f'{self.base_endpoint}/provider-accounts', data) + + async def update_provider_account(self, id: int, **kwargs) -> Dict: + """Update a provider account.""" + return self.client.patch(f'{self.base_endpoint}/provider-accounts', id, kwargs) + + async def delete_provider_account(self, id: int) -> None: + """Delete a provider account.""" + self.client.delete(f'{self.base_endpoint}/provider-accounts', id) + + # ==================== Provider Networks ==================== + + async def list_provider_networks( + self, + provider_id: Optional[int] = None, + name: Optional[str] = None, + **kwargs + ) -> List[Dict]: + """List all provider networks.""" + params = {k: v for k, v in { + 'provider_id': provider_id, 'name': name, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/provider-networks', params=params) + + async def get_provider_network(self, id: int) -> Dict: + """Get a specific provider network by ID.""" + return self.client.get(f'{self.base_endpoint}/provider-networks', id) + + async def create_provider_network( + self, + provider: int, + name: str, + service_id: Optional[str] = None, + description: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new provider network.""" + data = {'provider': provider, 'name': name, **kwargs} + if service_id: + data['service_id'] = service_id + if description: + data['description'] = description + return self.client.create(f'{self.base_endpoint}/provider-networks', data) + + async def update_provider_network(self, id: int, **kwargs) -> Dict: + """Update a provider network.""" + return self.client.patch(f'{self.base_endpoint}/provider-networks', id, kwargs) + + async def delete_provider_network(self, id: int) -> None: + """Delete a provider network.""" + self.client.delete(f'{self.base_endpoint}/provider-networks', id) + + # ==================== Circuit Types ==================== + + async def list_circuit_types( + self, + name: Optional[str] = None, + slug: Optional[str] = None, + **kwargs + ) -> List[Dict]: + """List all circuit types.""" + params = {k: v for k, v in {'name': name, 'slug': slug, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/circuit-types', params=params) + + async def get_circuit_type(self, id: int) -> Dict: + """Get a specific circuit type by ID.""" + return self.client.get(f'{self.base_endpoint}/circuit-types', id) + + async def create_circuit_type( + self, + name: str, + slug: str, + color: Optional[str] = None, + description: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new circuit type.""" + data = {'name': name, 'slug': slug, **kwargs} + if color: + data['color'] = color + if description: + data['description'] = description + return self.client.create(f'{self.base_endpoint}/circuit-types', data) + + async def update_circuit_type(self, id: int, **kwargs) -> Dict: + """Update a circuit type.""" + return self.client.patch(f'{self.base_endpoint}/circuit-types', id, kwargs) + + async def delete_circuit_type(self, id: int) -> None: + """Delete a circuit type.""" + self.client.delete(f'{self.base_endpoint}/circuit-types', id) + + # ==================== Circuit Groups ==================== + + async def list_circuit_groups( + self, + name: Optional[str] = None, + slug: Optional[str] = None, + **kwargs + ) -> List[Dict]: + """List all circuit groups.""" + params = {k: v for k, v in {'name': name, 'slug': slug, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/circuit-groups', params=params) + + async def get_circuit_group(self, id: int) -> Dict: + """Get a specific circuit group by ID.""" + return self.client.get(f'{self.base_endpoint}/circuit-groups', id) + + async def create_circuit_group( + self, + name: str, + slug: str, + description: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new circuit group.""" + data = {'name': name, 'slug': slug, **kwargs} + if description: + data['description'] = description + return self.client.create(f'{self.base_endpoint}/circuit-groups', data) + + async def update_circuit_group(self, id: int, **kwargs) -> Dict: + """Update a circuit group.""" + return self.client.patch(f'{self.base_endpoint}/circuit-groups', id, kwargs) + + async def delete_circuit_group(self, id: int) -> None: + """Delete a circuit group.""" + self.client.delete(f'{self.base_endpoint}/circuit-groups', id) + + # ==================== Circuit Group Assignments ==================== + + async def list_circuit_group_assignments( + self, + group_id: Optional[int] = None, + circuit_id: Optional[int] = None, + **kwargs + ) -> List[Dict]: + """List all circuit group assignments.""" + params = {k: v for k, v in { + 'group_id': group_id, 'circuit_id': circuit_id, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/circuit-group-assignments', params=params) + + async def get_circuit_group_assignment(self, id: int) -> Dict: + """Get a specific circuit group assignment by ID.""" + return self.client.get(f'{self.base_endpoint}/circuit-group-assignments', id) + + async def create_circuit_group_assignment( + self, + group: int, + circuit: int, + priority: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new circuit group assignment.""" + data = {'group': group, 'circuit': circuit, **kwargs} + if priority: + data['priority'] = priority + return self.client.create(f'{self.base_endpoint}/circuit-group-assignments', data) + + async def update_circuit_group_assignment(self, id: int, **kwargs) -> Dict: + """Update a circuit group assignment.""" + return self.client.patch(f'{self.base_endpoint}/circuit-group-assignments', id, kwargs) + + async def delete_circuit_group_assignment(self, id: int) -> None: + """Delete a circuit group assignment.""" + self.client.delete(f'{self.base_endpoint}/circuit-group-assignments', id) + + # ==================== Circuits ==================== + + async def list_circuits( + self, + cid: Optional[str] = None, + provider_id: Optional[int] = None, + provider_account_id: Optional[int] = None, + type_id: Optional[int] = None, + status: Optional[str] = None, + tenant_id: Optional[int] = None, + site_id: Optional[int] = None, + **kwargs + ) -> List[Dict]: + """List all circuits with optional filtering.""" + params = {k: v for k, v in { + 'cid': cid, 'provider_id': provider_id, 'provider_account_id': provider_account_id, + 'type_id': type_id, 'status': status, 'tenant_id': tenant_id, 'site_id': site_id, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/circuits', params=params) + + async def get_circuit(self, id: int) -> Dict: + """Get a specific circuit by ID.""" + return self.client.get(f'{self.base_endpoint}/circuits', id) + + async def create_circuit( + self, + cid: str, + provider: int, + type: int, + status: str = 'active', + provider_account: Optional[int] = None, + tenant: Optional[int] = None, + install_date: Optional[str] = None, + termination_date: Optional[str] = None, + commit_rate: Optional[int] = None, + description: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new circuit.""" + data = {'cid': cid, 'provider': provider, 'type': type, 'status': status, **kwargs} + for key, val in [ + ('provider_account', provider_account), ('tenant', tenant), + ('install_date', install_date), ('termination_date', termination_date), + ('commit_rate', commit_rate), ('description', description) + ]: + if val is not None: + data[key] = val + return self.client.create(f'{self.base_endpoint}/circuits', data) + + async def update_circuit(self, id: int, **kwargs) -> Dict: + """Update a circuit.""" + return self.client.patch(f'{self.base_endpoint}/circuits', id, kwargs) + + async def delete_circuit(self, id: int) -> None: + """Delete a circuit.""" + self.client.delete(f'{self.base_endpoint}/circuits', id) + + # ==================== Circuit Terminations ==================== + + async def list_circuit_terminations( + self, + circuit_id: Optional[int] = None, + site_id: Optional[int] = None, + provider_network_id: Optional[int] = None, + term_side: Optional[str] = None, + **kwargs + ) -> List[Dict]: + """List all circuit terminations.""" + params = {k: v for k, v in { + 'circuit_id': circuit_id, 'site_id': site_id, + 'provider_network_id': provider_network_id, 'term_side': term_side, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/circuit-terminations', params=params) + + async def get_circuit_termination(self, id: int) -> Dict: + """Get a specific circuit termination by ID.""" + return self.client.get(f'{self.base_endpoint}/circuit-terminations', id) + + async def create_circuit_termination( + self, + circuit: int, + term_side: str, + site: Optional[int] = None, + provider_network: Optional[int] = None, + port_speed: Optional[int] = None, + upstream_speed: Optional[int] = None, + xconnect_id: Optional[str] = None, + pp_info: Optional[str] = None, + description: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new circuit termination.""" + data = {'circuit': circuit, 'term_side': term_side, **kwargs} + for key, val in [ + ('site', site), ('provider_network', provider_network), + ('port_speed', port_speed), ('upstream_speed', upstream_speed), + ('xconnect_id', xconnect_id), ('pp_info', pp_info), ('description', description) + ]: + if val is not None: + data[key] = val + return self.client.create(f'{self.base_endpoint}/circuit-terminations', data) + + async def update_circuit_termination(self, id: int, **kwargs) -> Dict: + """Update a circuit termination.""" + return self.client.patch(f'{self.base_endpoint}/circuit-terminations', id, kwargs) + + async def delete_circuit_termination(self, id: int) -> None: + """Delete a circuit termination.""" + self.client.delete(f'{self.base_endpoint}/circuit-terminations', id) + + async def get_circuit_termination_paths(self, id: int) -> Dict: + """Get cable paths for a circuit termination.""" + return self.client.get(f'{self.base_endpoint}/circuit-terminations', f'{id}/paths') diff --git a/mcp-servers/netbox/mcp_server/tools/dcim.py b/mcp-servers/netbox/mcp_server/tools/dcim.py new file mode 100644 index 0000000..400baba --- /dev/null +++ b/mcp-servers/netbox/mcp_server/tools/dcim.py @@ -0,0 +1,935 @@ +""" +DCIM (Data Center Infrastructure Management) tools for NetBox MCP Server. + +Covers: Sites, Locations, Racks, Devices, Cables, Interfaces, and related models. +""" +import logging +from typing import List, Dict, Optional, Any +from ..netbox_client import NetBoxClient + +logger = logging.getLogger(__name__) + + +class DCIMTools: + """Tools for DCIM operations in NetBox""" + + def __init__(self, client: NetBoxClient): + self.client = client + self.base_endpoint = 'dcim' + + # ==================== Regions ==================== + + async def list_regions( + self, + name: Optional[str] = None, + slug: Optional[str] = None, + parent_id: Optional[int] = None, + **kwargs + ) -> List[Dict]: + """List all regions with optional filtering.""" + params = {k: v for k, v in { + 'name': name, 'slug': slug, 'parent_id': parent_id, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/regions', params=params) + + async def get_region(self, id: int) -> Dict: + """Get a specific region by ID.""" + return self.client.get(f'{self.base_endpoint}/regions', id) + + async def create_region(self, name: str, slug: str, parent: Optional[int] = None, **kwargs) -> Dict: + """Create a new region.""" + data = {'name': name, 'slug': slug, **kwargs} + if parent: + data['parent'] = parent + return self.client.create(f'{self.base_endpoint}/regions', data) + + async def update_region(self, id: int, **kwargs) -> Dict: + """Update a region.""" + return self.client.patch(f'{self.base_endpoint}/regions', id, kwargs) + + async def delete_region(self, id: int) -> None: + """Delete a region.""" + self.client.delete(f'{self.base_endpoint}/regions', id) + + # ==================== Site Groups ==================== + + async def list_site_groups( + self, + name: Optional[str] = None, + slug: Optional[str] = None, + parent_id: Optional[int] = None, + **kwargs + ) -> List[Dict]: + """List all site groups with optional filtering.""" + params = {k: v for k, v in { + 'name': name, 'slug': slug, 'parent_id': parent_id, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/site-groups', params=params) + + async def get_site_group(self, id: int) -> Dict: + """Get a specific site group by ID.""" + return self.client.get(f'{self.base_endpoint}/site-groups', id) + + async def create_site_group(self, name: str, slug: str, parent: Optional[int] = None, **kwargs) -> Dict: + """Create a new site group.""" + data = {'name': name, 'slug': slug, **kwargs} + if parent: + data['parent'] = parent + return self.client.create(f'{self.base_endpoint}/site-groups', data) + + async def update_site_group(self, id: int, **kwargs) -> Dict: + """Update a site group.""" + return self.client.patch(f'{self.base_endpoint}/site-groups', id, kwargs) + + async def delete_site_group(self, id: int) -> None: + """Delete a site group.""" + self.client.delete(f'{self.base_endpoint}/site-groups', id) + + # ==================== Sites ==================== + + async def list_sites( + self, + name: Optional[str] = None, + slug: Optional[str] = None, + status: Optional[str] = None, + region_id: Optional[int] = None, + group_id: Optional[int] = None, + tenant_id: Optional[int] = None, + **kwargs + ) -> List[Dict]: + """List all sites with optional filtering.""" + params = {k: v for k, v in { + 'name': name, 'slug': slug, 'status': status, + 'region_id': region_id, 'group_id': group_id, 'tenant_id': tenant_id, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/sites', params=params) + + async def get_site(self, id: int) -> Dict: + """Get a specific site by ID.""" + return self.client.get(f'{self.base_endpoint}/sites', id) + + async def create_site( + self, + name: str, + slug: str, + status: str = 'active', + region: Optional[int] = None, + group: Optional[int] = None, + tenant: Optional[int] = None, + facility: Optional[str] = None, + time_zone: Optional[str] = None, + description: Optional[str] = None, + physical_address: Optional[str] = None, + shipping_address: Optional[str] = None, + latitude: Optional[float] = None, + longitude: Optional[float] = None, + **kwargs + ) -> Dict: + """Create a new site.""" + data = {'name': name, 'slug': slug, 'status': status, **kwargs} + for key, val in [ + ('region', region), ('group', group), ('tenant', tenant), + ('facility', facility), ('time_zone', time_zone), + ('description', description), ('physical_address', physical_address), + ('shipping_address', shipping_address), ('latitude', latitude), + ('longitude', longitude) + ]: + if val is not None: + data[key] = val + return self.client.create(f'{self.base_endpoint}/sites', data) + + async def update_site(self, id: int, **kwargs) -> Dict: + """Update a site.""" + return self.client.patch(f'{self.base_endpoint}/sites', id, kwargs) + + async def delete_site(self, id: int) -> None: + """Delete a site.""" + self.client.delete(f'{self.base_endpoint}/sites', id) + + # ==================== Locations ==================== + + async def list_locations( + self, + name: Optional[str] = None, + slug: Optional[str] = None, + site_id: Optional[int] = None, + parent_id: Optional[int] = None, + **kwargs + ) -> List[Dict]: + """List all locations with optional filtering.""" + params = {k: v for k, v in { + 'name': name, 'slug': slug, 'site_id': site_id, 'parent_id': parent_id, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/locations', params=params) + + async def get_location(self, id: int) -> Dict: + """Get a specific location by ID.""" + return self.client.get(f'{self.base_endpoint}/locations', id) + + async def create_location( + self, + name: str, + slug: str, + site: int, + parent: Optional[int] = None, + **kwargs + ) -> Dict: + """Create a new location.""" + data = {'name': name, 'slug': slug, 'site': site, **kwargs} + if parent: + data['parent'] = parent + return self.client.create(f'{self.base_endpoint}/locations', data) + + async def update_location(self, id: int, **kwargs) -> Dict: + """Update a location.""" + return self.client.patch(f'{self.base_endpoint}/locations', id, kwargs) + + async def delete_location(self, id: int) -> None: + """Delete a location.""" + self.client.delete(f'{self.base_endpoint}/locations', id) + + # ==================== Rack Roles ==================== + + async def list_rack_roles(self, name: Optional[str] = None, **kwargs) -> List[Dict]: + """List all rack roles.""" + params = {k: v for k, v in {'name': name, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/rack-roles', params=params) + + async def get_rack_role(self, id: int) -> Dict: + """Get a specific rack role by ID.""" + return self.client.get(f'{self.base_endpoint}/rack-roles', id) + + async def create_rack_role(self, name: str, slug: str, color: str = '9e9e9e', **kwargs) -> Dict: + """Create a new rack role.""" + data = {'name': name, 'slug': slug, 'color': color, **kwargs} + return self.client.create(f'{self.base_endpoint}/rack-roles', data) + + async def update_rack_role(self, id: int, **kwargs) -> Dict: + """Update a rack role.""" + return self.client.patch(f'{self.base_endpoint}/rack-roles', id, kwargs) + + async def delete_rack_role(self, id: int) -> None: + """Delete a rack role.""" + self.client.delete(f'{self.base_endpoint}/rack-roles', id) + + # ==================== Rack Types ==================== + + async def list_rack_types(self, manufacturer_id: Optional[int] = None, **kwargs) -> List[Dict]: + """List all rack types.""" + params = {k: v for k, v in {'manufacturer_id': manufacturer_id, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/rack-types', params=params) + + async def get_rack_type(self, id: int) -> Dict: + """Get a specific rack type by ID.""" + return self.client.get(f'{self.base_endpoint}/rack-types', id) + + async def create_rack_type( + self, + manufacturer: int, + model: str, + slug: str, + form_factor: str = '4-post-frame', + width: int = 19, + u_height: int = 42, + **kwargs + ) -> Dict: + """Create a new rack type.""" + data = { + 'manufacturer': manufacturer, 'model': model, 'slug': slug, + 'form_factor': form_factor, 'width': width, 'u_height': u_height, **kwargs + } + return self.client.create(f'{self.base_endpoint}/rack-types', data) + + async def update_rack_type(self, id: int, **kwargs) -> Dict: + """Update a rack type.""" + return self.client.patch(f'{self.base_endpoint}/rack-types', id, kwargs) + + async def delete_rack_type(self, id: int) -> None: + """Delete a rack type.""" + self.client.delete(f'{self.base_endpoint}/rack-types', id) + + # ==================== Racks ==================== + + async def list_racks( + self, + name: Optional[str] = None, + site_id: Optional[int] = None, + location_id: Optional[int] = None, + status: Optional[str] = None, + role_id: Optional[int] = None, + tenant_id: Optional[int] = None, + **kwargs + ) -> List[Dict]: + """List all racks with optional filtering.""" + params = {k: v for k, v in { + 'name': name, 'site_id': site_id, 'location_id': location_id, + 'status': status, 'role_id': role_id, 'tenant_id': tenant_id, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/racks', params=params) + + async def get_rack(self, id: int) -> Dict: + """Get a specific rack by ID.""" + return self.client.get(f'{self.base_endpoint}/racks', id) + + async def create_rack( + self, + name: str, + site: int, + status: str = 'active', + location: Optional[int] = None, + role: Optional[int] = None, + tenant: Optional[int] = None, + rack_type: Optional[int] = None, + width: int = 19, + u_height: int = 42, + **kwargs + ) -> Dict: + """Create a new rack.""" + data = {'name': name, 'site': site, 'status': status, 'width': width, 'u_height': u_height, **kwargs} + for key, val in [('location', location), ('role', role), ('tenant', tenant), ('rack_type', rack_type)]: + if val is not None: + data[key] = val + return self.client.create(f'{self.base_endpoint}/racks', data) + + async def update_rack(self, id: int, **kwargs) -> Dict: + """Update a rack.""" + return self.client.patch(f'{self.base_endpoint}/racks', id, kwargs) + + async def delete_rack(self, id: int) -> None: + """Delete a rack.""" + self.client.delete(f'{self.base_endpoint}/racks', id) + + # ==================== Rack Reservations ==================== + + async def list_rack_reservations( + self, + rack_id: Optional[int] = None, + site_id: Optional[int] = None, + tenant_id: Optional[int] = None, + **kwargs + ) -> List[Dict]: + """List all rack reservations.""" + params = {k: v for k, v in { + 'rack_id': rack_id, 'site_id': site_id, 'tenant_id': tenant_id, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/rack-reservations', params=params) + + async def get_rack_reservation(self, id: int) -> Dict: + """Get a specific rack reservation by ID.""" + return self.client.get(f'{self.base_endpoint}/rack-reservations', id) + + async def create_rack_reservation( + self, + rack: int, + units: List[int], + user: int, + description: str, + tenant: Optional[int] = None, + **kwargs + ) -> Dict: + """Create a new rack reservation.""" + data = {'rack': rack, 'units': units, 'user': user, 'description': description, **kwargs} + if tenant: + data['tenant'] = tenant + return self.client.create(f'{self.base_endpoint}/rack-reservations', data) + + async def update_rack_reservation(self, id: int, **kwargs) -> Dict: + """Update a rack reservation.""" + return self.client.patch(f'{self.base_endpoint}/rack-reservations', id, kwargs) + + async def delete_rack_reservation(self, id: int) -> None: + """Delete a rack reservation.""" + self.client.delete(f'{self.base_endpoint}/rack-reservations', id) + + # ==================== Manufacturers ==================== + + async def list_manufacturers(self, name: Optional[str] = None, **kwargs) -> List[Dict]: + """List all manufacturers.""" + params = {k: v for k, v in {'name': name, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/manufacturers', params=params) + + async def get_manufacturer(self, id: int) -> Dict: + """Get a specific manufacturer by ID.""" + return self.client.get(f'{self.base_endpoint}/manufacturers', id) + + async def create_manufacturer(self, name: str, slug: str, **kwargs) -> Dict: + """Create a new manufacturer.""" + data = {'name': name, 'slug': slug, **kwargs} + return self.client.create(f'{self.base_endpoint}/manufacturers', data) + + async def update_manufacturer(self, id: int, **kwargs) -> Dict: + """Update a manufacturer.""" + return self.client.patch(f'{self.base_endpoint}/manufacturers', id, kwargs) + + async def delete_manufacturer(self, id: int) -> None: + """Delete a manufacturer.""" + self.client.delete(f'{self.base_endpoint}/manufacturers', id) + + # ==================== Device Types ==================== + + async def list_device_types( + self, + manufacturer_id: Optional[int] = None, + model: Optional[str] = None, + slug: Optional[str] = None, + **kwargs + ) -> List[Dict]: + """List all device types.""" + params = {k: v for k, v in { + 'manufacturer_id': manufacturer_id, 'model': model, 'slug': slug, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/device-types', params=params) + + async def get_device_type(self, id: int) -> Dict: + """Get a specific device type by ID.""" + return self.client.get(f'{self.base_endpoint}/device-types', id) + + async def create_device_type( + self, + manufacturer: int, + model: str, + slug: str, + u_height: float = 1.0, + is_full_depth: bool = True, + **kwargs + ) -> Dict: + """Create a new device type.""" + data = { + 'manufacturer': manufacturer, 'model': model, 'slug': slug, + 'u_height': u_height, 'is_full_depth': is_full_depth, **kwargs + } + return self.client.create(f'{self.base_endpoint}/device-types', data) + + async def update_device_type(self, id: int, **kwargs) -> Dict: + """Update a device type.""" + return self.client.patch(f'{self.base_endpoint}/device-types', id, kwargs) + + async def delete_device_type(self, id: int) -> None: + """Delete a device type.""" + self.client.delete(f'{self.base_endpoint}/device-types', id) + + # ==================== Module Types ==================== + + async def list_module_types(self, manufacturer_id: Optional[int] = None, **kwargs) -> List[Dict]: + """List all module types.""" + params = {k: v for k, v in {'manufacturer_id': manufacturer_id, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/module-types', params=params) + + async def get_module_type(self, id: int) -> Dict: + """Get a specific module type by ID.""" + return self.client.get(f'{self.base_endpoint}/module-types', id) + + async def create_module_type(self, manufacturer: int, model: str, **kwargs) -> Dict: + """Create a new module type.""" + data = {'manufacturer': manufacturer, 'model': model, **kwargs} + return self.client.create(f'{self.base_endpoint}/module-types', data) + + async def update_module_type(self, id: int, **kwargs) -> Dict: + """Update a module type.""" + return self.client.patch(f'{self.base_endpoint}/module-types', id, kwargs) + + async def delete_module_type(self, id: int) -> None: + """Delete a module type.""" + self.client.delete(f'{self.base_endpoint}/module-types', id) + + # ==================== Device Roles ==================== + + async def list_device_roles(self, name: Optional[str] = None, **kwargs) -> List[Dict]: + """List all device roles.""" + params = {k: v for k, v in {'name': name, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/device-roles', params=params) + + async def get_device_role(self, id: int) -> Dict: + """Get a specific device role by ID.""" + return self.client.get(f'{self.base_endpoint}/device-roles', id) + + async def create_device_role( + self, + name: str, + slug: str, + color: str = '9e9e9e', + vm_role: bool = False, + **kwargs + ) -> Dict: + """Create a new device role.""" + data = {'name': name, 'slug': slug, 'color': color, 'vm_role': vm_role, **kwargs} + return self.client.create(f'{self.base_endpoint}/device-roles', data) + + async def update_device_role(self, id: int, **kwargs) -> Dict: + """Update a device role.""" + return self.client.patch(f'{self.base_endpoint}/device-roles', id, kwargs) + + async def delete_device_role(self, id: int) -> None: + """Delete a device role.""" + self.client.delete(f'{self.base_endpoint}/device-roles', id) + + # ==================== Platforms ==================== + + async def list_platforms(self, name: Optional[str] = None, manufacturer_id: Optional[int] = None, **kwargs) -> List[Dict]: + """List all platforms.""" + params = {k: v for k, v in {'name': name, 'manufacturer_id': manufacturer_id, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/platforms', params=params) + + async def get_platform(self, id: int) -> Dict: + """Get a specific platform by ID.""" + return self.client.get(f'{self.base_endpoint}/platforms', id) + + async def create_platform( + self, + name: str, + slug: str, + manufacturer: Optional[int] = None, + **kwargs + ) -> Dict: + """Create a new platform.""" + data = {'name': name, 'slug': slug, **kwargs} + if manufacturer: + data['manufacturer'] = manufacturer + return self.client.create(f'{self.base_endpoint}/platforms', data) + + async def update_platform(self, id: int, **kwargs) -> Dict: + """Update a platform.""" + return self.client.patch(f'{self.base_endpoint}/platforms', id, kwargs) + + async def delete_platform(self, id: int) -> None: + """Delete a platform.""" + self.client.delete(f'{self.base_endpoint}/platforms', id) + + # ==================== Devices ==================== + + async def list_devices( + self, + name: Optional[str] = None, + site_id: Optional[int] = None, + location_id: Optional[int] = None, + rack_id: Optional[int] = None, + status: Optional[str] = None, + role_id: Optional[int] = None, + device_type_id: Optional[int] = None, + manufacturer_id: Optional[int] = None, + platform_id: Optional[int] = None, + tenant_id: Optional[int] = None, + serial: Optional[str] = None, + asset_tag: Optional[str] = None, + **kwargs + ) -> List[Dict]: + """List all devices with optional filtering.""" + params = {k: v for k, v in { + 'name': name, 'site_id': site_id, 'location_id': location_id, + 'rack_id': rack_id, 'status': status, 'role_id': role_id, + 'device_type_id': device_type_id, 'manufacturer_id': manufacturer_id, + 'platform_id': platform_id, 'tenant_id': tenant_id, + 'serial': serial, 'asset_tag': asset_tag, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/devices', params=params) + + async def get_device(self, id: int) -> Dict: + """Get a specific device by ID.""" + return self.client.get(f'{self.base_endpoint}/devices', id) + + async def create_device( + self, + name: str, + device_type: int, + role: int, + site: int, + status: str = 'active', + location: Optional[int] = None, + rack: Optional[int] = None, + position: Optional[float] = None, + face: Optional[str] = None, + platform: Optional[int] = None, + tenant: Optional[int] = None, + serial: Optional[str] = None, + asset_tag: Optional[str] = None, + primary_ip4: Optional[int] = None, + primary_ip6: Optional[int] = None, + **kwargs + ) -> Dict: + """Create a new device.""" + data = { + 'name': name, 'device_type': device_type, 'role': role, + 'site': site, 'status': status, **kwargs + } + for key, val in [ + ('location', location), ('rack', rack), ('position', position), + ('face', face), ('platform', platform), ('tenant', tenant), + ('serial', serial), ('asset_tag', asset_tag), + ('primary_ip4', primary_ip4), ('primary_ip6', primary_ip6) + ]: + if val is not None: + data[key] = val + return self.client.create(f'{self.base_endpoint}/devices', data) + + async def update_device(self, id: int, **kwargs) -> Dict: + """Update a device.""" + return self.client.patch(f'{self.base_endpoint}/devices', id, kwargs) + + async def delete_device(self, id: int) -> None: + """Delete a device.""" + self.client.delete(f'{self.base_endpoint}/devices', id) + + # ==================== Modules ==================== + + async def list_modules(self, device_id: Optional[int] = None, **kwargs) -> List[Dict]: + """List all modules.""" + params = {k: v for k, v in {'device_id': device_id, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/modules', params=params) + + async def get_module(self, id: int) -> Dict: + """Get a specific module by ID.""" + return self.client.get(f'{self.base_endpoint}/modules', id) + + async def create_module(self, device: int, module_bay: int, module_type: int, **kwargs) -> Dict: + """Create a new module.""" + data = {'device': device, 'module_bay': module_bay, 'module_type': module_type, **kwargs} + return self.client.create(f'{self.base_endpoint}/modules', data) + + async def update_module(self, id: int, **kwargs) -> Dict: + """Update a module.""" + return self.client.patch(f'{self.base_endpoint}/modules', id, kwargs) + + async def delete_module(self, id: int) -> None: + """Delete a module.""" + self.client.delete(f'{self.base_endpoint}/modules', id) + + # ==================== Interfaces ==================== + + async def list_interfaces( + self, + device_id: Optional[int] = None, + name: Optional[str] = None, + type: Optional[str] = None, + enabled: Optional[bool] = None, + **kwargs + ) -> List[Dict]: + """List all interfaces.""" + params = {k: v for k, v in { + 'device_id': device_id, 'name': name, 'type': type, 'enabled': enabled, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/interfaces', params=params) + + async def get_interface(self, id: int) -> Dict: + """Get a specific interface by ID.""" + return self.client.get(f'{self.base_endpoint}/interfaces', id) + + async def create_interface( + self, + device: int, + name: str, + type: str, + enabled: bool = True, + mtu: Optional[int] = None, + mac_address: Optional[str] = None, + description: Optional[str] = None, + mode: Optional[str] = None, + untagged_vlan: Optional[int] = None, + tagged_vlans: Optional[List[int]] = None, + **kwargs + ) -> Dict: + """Create a new interface.""" + data = {'device': device, 'name': name, 'type': type, 'enabled': enabled, **kwargs} + for key, val in [ + ('mtu', mtu), ('mac_address', mac_address), ('description', description), + ('mode', mode), ('untagged_vlan', untagged_vlan), ('tagged_vlans', tagged_vlans) + ]: + if val is not None: + data[key] = val + return self.client.create(f'{self.base_endpoint}/interfaces', data) + + async def update_interface(self, id: int, **kwargs) -> Dict: + """Update an interface.""" + return self.client.patch(f'{self.base_endpoint}/interfaces', id, kwargs) + + async def delete_interface(self, id: int) -> None: + """Delete an interface.""" + self.client.delete(f'{self.base_endpoint}/interfaces', id) + + # ==================== Console Ports ==================== + + async def list_console_ports(self, device_id: Optional[int] = None, **kwargs) -> List[Dict]: + """List all console ports.""" + params = {k: v for k, v in {'device_id': device_id, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/console-ports', params=params) + + async def get_console_port(self, id: int) -> Dict: + """Get a specific console port by ID.""" + return self.client.get(f'{self.base_endpoint}/console-ports', id) + + async def create_console_port(self, device: int, name: str, **kwargs) -> Dict: + """Create a new console port.""" + data = {'device': device, 'name': name, **kwargs} + return self.client.create(f'{self.base_endpoint}/console-ports', data) + + async def update_console_port(self, id: int, **kwargs) -> Dict: + """Update a console port.""" + return self.client.patch(f'{self.base_endpoint}/console-ports', id, kwargs) + + async def delete_console_port(self, id: int) -> None: + """Delete a console port.""" + self.client.delete(f'{self.base_endpoint}/console-ports', id) + + # ==================== Console Server Ports ==================== + + async def list_console_server_ports(self, device_id: Optional[int] = None, **kwargs) -> List[Dict]: + """List all console server ports.""" + params = {k: v for k, v in {'device_id': device_id, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/console-server-ports', params=params) + + async def get_console_server_port(self, id: int) -> Dict: + """Get a specific console server port by ID.""" + return self.client.get(f'{self.base_endpoint}/console-server-ports', id) + + async def create_console_server_port(self, device: int, name: str, **kwargs) -> Dict: + """Create a new console server port.""" + data = {'device': device, 'name': name, **kwargs} + return self.client.create(f'{self.base_endpoint}/console-server-ports', data) + + async def update_console_server_port(self, id: int, **kwargs) -> Dict: + """Update a console server port.""" + return self.client.patch(f'{self.base_endpoint}/console-server-ports', id, kwargs) + + async def delete_console_server_port(self, id: int) -> None: + """Delete a console server port.""" + self.client.delete(f'{self.base_endpoint}/console-server-ports', id) + + # ==================== Power Ports ==================== + + async def list_power_ports(self, device_id: Optional[int] = None, **kwargs) -> List[Dict]: + """List all power ports.""" + params = {k: v for k, v in {'device_id': device_id, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/power-ports', params=params) + + async def get_power_port(self, id: int) -> Dict: + """Get a specific power port by ID.""" + return self.client.get(f'{self.base_endpoint}/power-ports', id) + + async def create_power_port(self, device: int, name: str, **kwargs) -> Dict: + """Create a new power port.""" + data = {'device': device, 'name': name, **kwargs} + return self.client.create(f'{self.base_endpoint}/power-ports', data) + + async def update_power_port(self, id: int, **kwargs) -> Dict: + """Update a power port.""" + return self.client.patch(f'{self.base_endpoint}/power-ports', id, kwargs) + + async def delete_power_port(self, id: int) -> None: + """Delete a power port.""" + self.client.delete(f'{self.base_endpoint}/power-ports', id) + + # ==================== Power Outlets ==================== + + async def list_power_outlets(self, device_id: Optional[int] = None, **kwargs) -> List[Dict]: + """List all power outlets.""" + params = {k: v for k, v in {'device_id': device_id, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/power-outlets', params=params) + + async def get_power_outlet(self, id: int) -> Dict: + """Get a specific power outlet by ID.""" + return self.client.get(f'{self.base_endpoint}/power-outlets', id) + + async def create_power_outlet(self, device: int, name: str, **kwargs) -> Dict: + """Create a new power outlet.""" + data = {'device': device, 'name': name, **kwargs} + return self.client.create(f'{self.base_endpoint}/power-outlets', data) + + async def update_power_outlet(self, id: int, **kwargs) -> Dict: + """Update a power outlet.""" + return self.client.patch(f'{self.base_endpoint}/power-outlets', id, kwargs) + + async def delete_power_outlet(self, id: int) -> None: + """Delete a power outlet.""" + self.client.delete(f'{self.base_endpoint}/power-outlets', id) + + # ==================== Power Panels ==================== + + async def list_power_panels(self, site_id: Optional[int] = None, **kwargs) -> List[Dict]: + """List all power panels.""" + params = {k: v for k, v in {'site_id': site_id, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/power-panels', params=params) + + async def get_power_panel(self, id: int) -> Dict: + """Get a specific power panel by ID.""" + return self.client.get(f'{self.base_endpoint}/power-panels', id) + + async def create_power_panel(self, site: int, name: str, location: Optional[int] = None, **kwargs) -> Dict: + """Create a new power panel.""" + data = {'site': site, 'name': name, **kwargs} + if location: + data['location'] = location + return self.client.create(f'{self.base_endpoint}/power-panels', data) + + async def update_power_panel(self, id: int, **kwargs) -> Dict: + """Update a power panel.""" + return self.client.patch(f'{self.base_endpoint}/power-panels', id, kwargs) + + async def delete_power_panel(self, id: int) -> None: + """Delete a power panel.""" + self.client.delete(f'{self.base_endpoint}/power-panels', id) + + # ==================== Power Feeds ==================== + + async def list_power_feeds(self, power_panel_id: Optional[int] = None, **kwargs) -> List[Dict]: + """List all power feeds.""" + params = {k: v for k, v in {'power_panel_id': power_panel_id, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/power-feeds', params=params) + + async def get_power_feed(self, id: int) -> Dict: + """Get a specific power feed by ID.""" + return self.client.get(f'{self.base_endpoint}/power-feeds', id) + + async def create_power_feed( + self, + power_panel: int, + name: str, + status: str = 'active', + type: str = 'primary', + supply: str = 'ac', + phase: str = 'single-phase', + voltage: int = 120, + amperage: int = 20, + **kwargs + ) -> Dict: + """Create a new power feed.""" + data = { + 'power_panel': power_panel, 'name': name, 'status': status, + 'type': type, 'supply': supply, 'phase': phase, + 'voltage': voltage, 'amperage': amperage, **kwargs + } + return self.client.create(f'{self.base_endpoint}/power-feeds', data) + + async def update_power_feed(self, id: int, **kwargs) -> Dict: + """Update a power feed.""" + return self.client.patch(f'{self.base_endpoint}/power-feeds', id, kwargs) + + async def delete_power_feed(self, id: int) -> None: + """Delete a power feed.""" + self.client.delete(f'{self.base_endpoint}/power-feeds', id) + + # ==================== Cables ==================== + + async def list_cables( + self, + site_id: Optional[int] = None, + device_id: Optional[int] = None, + rack_id: Optional[int] = None, + type: Optional[str] = None, + status: Optional[str] = None, + **kwargs + ) -> List[Dict]: + """List all cables.""" + params = {k: v for k, v in { + 'site_id': site_id, 'device_id': device_id, 'rack_id': rack_id, + 'type': type, 'status': status, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/cables', params=params) + + async def get_cable(self, id: int) -> Dict: + """Get a specific cable by ID.""" + return self.client.get(f'{self.base_endpoint}/cables', id) + + async def create_cable( + self, + a_terminations: List[Dict], + b_terminations: List[Dict], + type: Optional[str] = None, + status: str = 'connected', + label: Optional[str] = None, + color: Optional[str] = None, + length: Optional[float] = None, + length_unit: Optional[str] = None, + **kwargs + ) -> Dict: + """ + Create a new cable. + + a_terminations and b_terminations are lists of dicts with: + - object_type: e.g., 'dcim.interface' + - object_id: ID of the object + """ + data = { + 'a_terminations': a_terminations, + 'b_terminations': b_terminations, + 'status': status, + **kwargs + } + for key, val in [ + ('type', type), ('label', label), ('color', color), + ('length', length), ('length_unit', length_unit) + ]: + if val is not None: + data[key] = val + return self.client.create(f'{self.base_endpoint}/cables', data) + + async def update_cable(self, id: int, **kwargs) -> Dict: + """Update a cable.""" + return self.client.patch(f'{self.base_endpoint}/cables', id, kwargs) + + async def delete_cable(self, id: int) -> None: + """Delete a cable.""" + self.client.delete(f'{self.base_endpoint}/cables', id) + + # ==================== Virtual Chassis ==================== + + async def list_virtual_chassis(self, **kwargs) -> List[Dict]: + """List all virtual chassis.""" + return self.client.list(f'{self.base_endpoint}/virtual-chassis', params=kwargs) + + async def get_virtual_chassis(self, id: int) -> Dict: + """Get a specific virtual chassis by ID.""" + return self.client.get(f'{self.base_endpoint}/virtual-chassis', id) + + async def create_virtual_chassis(self, name: str, domain: Optional[str] = None, **kwargs) -> Dict: + """Create a new virtual chassis.""" + data = {'name': name, **kwargs} + if domain: + data['domain'] = domain + return self.client.create(f'{self.base_endpoint}/virtual-chassis', data) + + async def update_virtual_chassis(self, id: int, **kwargs) -> Dict: + """Update a virtual chassis.""" + return self.client.patch(f'{self.base_endpoint}/virtual-chassis', id, kwargs) + + async def delete_virtual_chassis(self, id: int) -> None: + """Delete a virtual chassis.""" + self.client.delete(f'{self.base_endpoint}/virtual-chassis', id) + + # ==================== Inventory Items ==================== + + async def list_inventory_items(self, device_id: Optional[int] = None, **kwargs) -> List[Dict]: + """List all inventory items.""" + params = {k: v for k, v in {'device_id': device_id, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/inventory-items', params=params) + + async def get_inventory_item(self, id: int) -> Dict: + """Get a specific inventory item by ID.""" + return self.client.get(f'{self.base_endpoint}/inventory-items', id) + + async def create_inventory_item( + self, + device: int, + name: str, + parent: Optional[int] = None, + manufacturer: Optional[int] = None, + part_id: Optional[str] = None, + serial: Optional[str] = None, + asset_tag: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new inventory item.""" + data = {'device': device, 'name': name, **kwargs} + for key, val in [ + ('parent', parent), ('manufacturer', manufacturer), + ('part_id', part_id), ('serial', serial), ('asset_tag', asset_tag) + ]: + if val is not None: + data[key] = val + return self.client.create(f'{self.base_endpoint}/inventory-items', data) + + async def update_inventory_item(self, id: int, **kwargs) -> Dict: + """Update an inventory item.""" + return self.client.patch(f'{self.base_endpoint}/inventory-items', id, kwargs) + + async def delete_inventory_item(self, id: int) -> None: + """Delete an inventory item.""" + self.client.delete(f'{self.base_endpoint}/inventory-items', id) diff --git a/mcp-servers/netbox/mcp_server/tools/extras.py b/mcp-servers/netbox/mcp_server/tools/extras.py new file mode 100644 index 0000000..6c17766 --- /dev/null +++ b/mcp-servers/netbox/mcp_server/tools/extras.py @@ -0,0 +1,560 @@ +""" +Extras tools for NetBox MCP Server. + +Covers: Tags, Custom Fields, Custom Links, Webhooks, Journal Entries, and more. +""" +import logging +from typing import List, Dict, Optional, Any +from ..netbox_client import NetBoxClient + +logger = logging.getLogger(__name__) + + +class ExtrasTools: + """Tools for Extras operations in NetBox""" + + def __init__(self, client: NetBoxClient): + self.client = client + self.base_endpoint = 'extras' + + # ==================== Tags ==================== + + async def list_tags( + self, + name: Optional[str] = None, + slug: Optional[str] = None, + color: Optional[str] = None, + **kwargs + ) -> List[Dict]: + """List all tags with optional filtering.""" + params = {k: v for k, v in { + 'name': name, 'slug': slug, 'color': color, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/tags', params=params) + + async def get_tag(self, id: int) -> Dict: + """Get a specific tag by ID.""" + return self.client.get(f'{self.base_endpoint}/tags', id) + + async def create_tag( + self, + name: str, + slug: str, + color: str = '9e9e9e', + description: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new tag.""" + data = {'name': name, 'slug': slug, 'color': color, **kwargs} + if description: + data['description'] = description + return self.client.create(f'{self.base_endpoint}/tags', data) + + async def update_tag(self, id: int, **kwargs) -> Dict: + """Update a tag.""" + return self.client.patch(f'{self.base_endpoint}/tags', id, kwargs) + + async def delete_tag(self, id: int) -> None: + """Delete a tag.""" + self.client.delete(f'{self.base_endpoint}/tags', id) + + # ==================== Custom Fields ==================== + + async def list_custom_fields( + self, + name: Optional[str] = None, + type: Optional[str] = None, + content_types: Optional[str] = None, + **kwargs + ) -> List[Dict]: + """List all custom fields.""" + params = {k: v for k, v in { + 'name': name, 'type': type, 'content_types': content_types, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/custom-fields', params=params) + + async def get_custom_field(self, id: int) -> Dict: + """Get a specific custom field by ID.""" + return self.client.get(f'{self.base_endpoint}/custom-fields', id) + + async def create_custom_field( + self, + name: str, + content_types: List[str], + type: str = 'text', + label: Optional[str] = None, + description: Optional[str] = None, + required: bool = False, + filter_logic: str = 'loose', + default: Optional[Any] = None, + weight: int = 100, + validation_minimum: Optional[int] = None, + validation_maximum: Optional[int] = None, + validation_regex: Optional[str] = None, + choice_set: Optional[int] = None, + **kwargs + ) -> Dict: + """Create a new custom field.""" + data = { + 'name': name, 'content_types': content_types, 'type': type, + 'required': required, 'filter_logic': filter_logic, 'weight': weight, **kwargs + } + for key, val in [ + ('label', label), ('description', description), ('default', default), + ('validation_minimum', validation_minimum), ('validation_maximum', validation_maximum), + ('validation_regex', validation_regex), ('choice_set', choice_set) + ]: + if val is not None: + data[key] = val + return self.client.create(f'{self.base_endpoint}/custom-fields', data) + + async def update_custom_field(self, id: int, **kwargs) -> Dict: + """Update a custom field.""" + return self.client.patch(f'{self.base_endpoint}/custom-fields', id, kwargs) + + async def delete_custom_field(self, id: int) -> None: + """Delete a custom field.""" + self.client.delete(f'{self.base_endpoint}/custom-fields', id) + + # ==================== Custom Field Choice Sets ==================== + + async def list_custom_field_choice_sets(self, name: Optional[str] = None, **kwargs) -> List[Dict]: + """List all custom field choice sets.""" + params = {k: v for k, v in {'name': name, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/custom-field-choice-sets', params=params) + + async def get_custom_field_choice_set(self, id: int) -> Dict: + """Get a specific custom field choice set by ID.""" + return self.client.get(f'{self.base_endpoint}/custom-field-choice-sets', id) + + async def create_custom_field_choice_set( + self, + name: str, + extra_choices: List[List[str]], + description: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new custom field choice set.""" + data = {'name': name, 'extra_choices': extra_choices, **kwargs} + if description: + data['description'] = description + return self.client.create(f'{self.base_endpoint}/custom-field-choice-sets', data) + + async def update_custom_field_choice_set(self, id: int, **kwargs) -> Dict: + """Update a custom field choice set.""" + return self.client.patch(f'{self.base_endpoint}/custom-field-choice-sets', id, kwargs) + + async def delete_custom_field_choice_set(self, id: int) -> None: + """Delete a custom field choice set.""" + self.client.delete(f'{self.base_endpoint}/custom-field-choice-sets', id) + + # ==================== Custom Links ==================== + + async def list_custom_links( + self, + name: Optional[str] = None, + content_types: Optional[str] = None, + **kwargs + ) -> List[Dict]: + """List all custom links.""" + params = {k: v for k, v in { + 'name': name, 'content_types': content_types, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/custom-links', params=params) + + async def get_custom_link(self, id: int) -> Dict: + """Get a specific custom link by ID.""" + return self.client.get(f'{self.base_endpoint}/custom-links', id) + + async def create_custom_link( + self, + name: str, + content_types: List[str], + link_text: str, + link_url: str, + enabled: bool = True, + new_window: bool = False, + weight: int = 100, + group_name: Optional[str] = None, + button_class: str = 'outline-dark', + **kwargs + ) -> Dict: + """Create a new custom link.""" + data = { + 'name': name, 'content_types': content_types, + 'link_text': link_text, 'link_url': link_url, + 'enabled': enabled, 'new_window': new_window, + 'weight': weight, 'button_class': button_class, **kwargs + } + if group_name: + data['group_name'] = group_name + return self.client.create(f'{self.base_endpoint}/custom-links', data) + + async def update_custom_link(self, id: int, **kwargs) -> Dict: + """Update a custom link.""" + return self.client.patch(f'{self.base_endpoint}/custom-links', id, kwargs) + + async def delete_custom_link(self, id: int) -> None: + """Delete a custom link.""" + self.client.delete(f'{self.base_endpoint}/custom-links', id) + + # ==================== Webhooks ==================== + + async def list_webhooks(self, name: Optional[str] = None, **kwargs) -> List[Dict]: + """List all webhooks.""" + params = {k: v for k, v in {'name': name, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/webhooks', params=params) + + async def get_webhook(self, id: int) -> Dict: + """Get a specific webhook by ID.""" + return self.client.get(f'{self.base_endpoint}/webhooks', id) + + async def create_webhook( + self, + name: str, + payload_url: str, + content_types: List[str], + type_create: bool = True, + type_update: bool = True, + type_delete: bool = True, + type_job_start: bool = False, + type_job_end: bool = False, + enabled: bool = True, + http_method: str = 'POST', + http_content_type: str = 'application/json', + additional_headers: Optional[str] = None, + body_template: Optional[str] = None, + secret: Optional[str] = None, + ssl_verification: bool = True, + ca_file_path: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new webhook.""" + data = { + 'name': name, 'payload_url': payload_url, 'content_types': content_types, + 'type_create': type_create, 'type_update': type_update, 'type_delete': type_delete, + 'type_job_start': type_job_start, 'type_job_end': type_job_end, + 'enabled': enabled, 'http_method': http_method, + 'http_content_type': http_content_type, 'ssl_verification': ssl_verification, **kwargs + } + for key, val in [ + ('additional_headers', additional_headers), ('body_template', body_template), + ('secret', secret), ('ca_file_path', ca_file_path) + ]: + if val is not None: + data[key] = val + return self.client.create(f'{self.base_endpoint}/webhooks', data) + + async def update_webhook(self, id: int, **kwargs) -> Dict: + """Update a webhook.""" + return self.client.patch(f'{self.base_endpoint}/webhooks', id, kwargs) + + async def delete_webhook(self, id: int) -> None: + """Delete a webhook.""" + self.client.delete(f'{self.base_endpoint}/webhooks', id) + + # ==================== Journal Entries ==================== + + async def list_journal_entries( + self, + assigned_object_type: Optional[str] = None, + assigned_object_id: Optional[int] = None, + kind: Optional[str] = None, + **kwargs + ) -> List[Dict]: + """List all journal entries.""" + params = {k: v for k, v in { + 'assigned_object_type': assigned_object_type, + 'assigned_object_id': assigned_object_id, 'kind': kind, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/journal-entries', params=params) + + async def get_journal_entry(self, id: int) -> Dict: + """Get a specific journal entry by ID.""" + return self.client.get(f'{self.base_endpoint}/journal-entries', id) + + async def create_journal_entry( + self, + assigned_object_type: str, + assigned_object_id: int, + comments: str, + kind: str = 'info', + **kwargs + ) -> Dict: + """Create a new journal entry.""" + data = { + 'assigned_object_type': assigned_object_type, + 'assigned_object_id': assigned_object_id, + 'comments': comments, 'kind': kind, **kwargs + } + return self.client.create(f'{self.base_endpoint}/journal-entries', data) + + async def update_journal_entry(self, id: int, **kwargs) -> Dict: + """Update a journal entry.""" + return self.client.patch(f'{self.base_endpoint}/journal-entries', id, kwargs) + + async def delete_journal_entry(self, id: int) -> None: + """Delete a journal entry.""" + self.client.delete(f'{self.base_endpoint}/journal-entries', id) + + # ==================== Config Contexts ==================== + + async def list_config_contexts(self, name: Optional[str] = None, **kwargs) -> List[Dict]: + """List all config contexts.""" + params = {k: v for k, v in {'name': name, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/config-contexts', params=params) + + async def get_config_context(self, id: int) -> Dict: + """Get a specific config context by ID.""" + return self.client.get(f'{self.base_endpoint}/config-contexts', id) + + async def create_config_context( + self, + name: str, + data: Dict[str, Any], + weight: int = 1000, + description: Optional[str] = None, + is_active: bool = True, + regions: Optional[List[int]] = None, + site_groups: Optional[List[int]] = None, + sites: Optional[List[int]] = None, + locations: Optional[List[int]] = None, + device_types: Optional[List[int]] = None, + roles: Optional[List[int]] = None, + platforms: Optional[List[int]] = None, + cluster_types: Optional[List[int]] = None, + cluster_groups: Optional[List[int]] = None, + clusters: Optional[List[int]] = None, + tenant_groups: Optional[List[int]] = None, + tenants: Optional[List[int]] = None, + tags: Optional[List[str]] = None, + **kwargs + ) -> Dict: + """Create a new config context.""" + context_data = { + 'name': name, 'data': data, 'weight': weight, 'is_active': is_active, **kwargs + } + for key, val in [ + ('description', description), ('regions', regions), + ('site_groups', site_groups), ('sites', sites), + ('locations', locations), ('device_types', device_types), + ('roles', roles), ('platforms', platforms), + ('cluster_types', cluster_types), ('cluster_groups', cluster_groups), + ('clusters', clusters), ('tenant_groups', tenant_groups), + ('tenants', tenants), ('tags', tags) + ]: + if val is not None: + context_data[key] = val + return self.client.create(f'{self.base_endpoint}/config-contexts', context_data) + + async def update_config_context(self, id: int, **kwargs) -> Dict: + """Update a config context.""" + return self.client.patch(f'{self.base_endpoint}/config-contexts', id, kwargs) + + async def delete_config_context(self, id: int) -> None: + """Delete a config context.""" + self.client.delete(f'{self.base_endpoint}/config-contexts', id) + + # ==================== Config Templates ==================== + + async def list_config_templates(self, name: Optional[str] = None, **kwargs) -> List[Dict]: + """List all config templates.""" + params = {k: v for k, v in {'name': name, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/config-templates', params=params) + + async def get_config_template(self, id: int) -> Dict: + """Get a specific config template by ID.""" + return self.client.get(f'{self.base_endpoint}/config-templates', id) + + async def create_config_template( + self, + name: str, + template_code: str, + description: Optional[str] = None, + environment_params: Optional[Dict[str, Any]] = None, + **kwargs + ) -> Dict: + """Create a new config template.""" + data = {'name': name, 'template_code': template_code, **kwargs} + if description: + data['description'] = description + if environment_params: + data['environment_params'] = environment_params + return self.client.create(f'{self.base_endpoint}/config-templates', data) + + async def update_config_template(self, id: int, **kwargs) -> Dict: + """Update a config template.""" + return self.client.patch(f'{self.base_endpoint}/config-templates', id, kwargs) + + async def delete_config_template(self, id: int) -> None: + """Delete a config template.""" + self.client.delete(f'{self.base_endpoint}/config-templates', id) + + # ==================== Export Templates ==================== + + async def list_export_templates( + self, + name: Optional[str] = None, + content_types: Optional[str] = None, + **kwargs + ) -> List[Dict]: + """List all export templates.""" + params = {k: v for k, v in { + 'name': name, 'content_types': content_types, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/export-templates', params=params) + + async def get_export_template(self, id: int) -> Dict: + """Get a specific export template by ID.""" + return self.client.get(f'{self.base_endpoint}/export-templates', id) + + async def create_export_template( + self, + name: str, + content_types: List[str], + template_code: str, + description: Optional[str] = None, + mime_type: str = 'text/plain', + file_extension: Optional[str] = None, + as_attachment: bool = True, + **kwargs + ) -> Dict: + """Create a new export template.""" + data = { + 'name': name, 'content_types': content_types, + 'template_code': template_code, 'mime_type': mime_type, + 'as_attachment': as_attachment, **kwargs + } + if description: + data['description'] = description + if file_extension: + data['file_extension'] = file_extension + return self.client.create(f'{self.base_endpoint}/export-templates', data) + + async def update_export_template(self, id: int, **kwargs) -> Dict: + """Update an export template.""" + return self.client.patch(f'{self.base_endpoint}/export-templates', id, kwargs) + + async def delete_export_template(self, id: int) -> None: + """Delete an export template.""" + self.client.delete(f'{self.base_endpoint}/export-templates', id) + + # ==================== Saved Filters ==================== + + async def list_saved_filters( + self, + name: Optional[str] = None, + content_types: Optional[str] = None, + **kwargs + ) -> List[Dict]: + """List all saved filters.""" + params = {k: v for k, v in { + 'name': name, 'content_types': content_types, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/saved-filters', params=params) + + async def get_saved_filter(self, id: int) -> Dict: + """Get a specific saved filter by ID.""" + return self.client.get(f'{self.base_endpoint}/saved-filters', id) + + async def create_saved_filter( + self, + name: str, + slug: str, + content_types: List[str], + parameters: Dict[str, Any], + description: Optional[str] = None, + weight: int = 100, + enabled: bool = True, + shared: bool = True, + **kwargs + ) -> Dict: + """Create a new saved filter.""" + data = { + 'name': name, 'slug': slug, 'content_types': content_types, + 'parameters': parameters, 'weight': weight, + 'enabled': enabled, 'shared': shared, **kwargs + } + if description: + data['description'] = description + return self.client.create(f'{self.base_endpoint}/saved-filters', data) + + async def update_saved_filter(self, id: int, **kwargs) -> Dict: + """Update a saved filter.""" + return self.client.patch(f'{self.base_endpoint}/saved-filters', id, kwargs) + + async def delete_saved_filter(self, id: int) -> None: + """Delete a saved filter.""" + self.client.delete(f'{self.base_endpoint}/saved-filters', id) + + # ==================== Image Attachments ==================== + + async def list_image_attachments( + self, + object_type: Optional[str] = None, + object_id: Optional[int] = None, + **kwargs + ) -> List[Dict]: + """List all image attachments.""" + params = {k: v for k, v in { + 'object_type': object_type, 'object_id': object_id, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/image-attachments', params=params) + + async def get_image_attachment(self, id: int) -> Dict: + """Get a specific image attachment by ID.""" + return self.client.get(f'{self.base_endpoint}/image-attachments', id) + + async def delete_image_attachment(self, id: int) -> None: + """Delete an image attachment.""" + self.client.delete(f'{self.base_endpoint}/image-attachments', id) + + # ==================== Object Changes (Audit Log) ==================== + + async def list_object_changes( + self, + user_id: Optional[int] = None, + changed_object_type: Optional[str] = None, + changed_object_id: Optional[int] = None, + action: Optional[str] = None, + **kwargs + ) -> List[Dict]: + """List all object changes (audit log).""" + params = {k: v for k, v in { + 'user_id': user_id, 'changed_object_type': changed_object_type, + 'changed_object_id': changed_object_id, 'action': action, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/object-changes', params=params) + + async def get_object_change(self, id: int) -> Dict: + """Get a specific object change by ID.""" + return self.client.get(f'{self.base_endpoint}/object-changes', id) + + # ==================== Scripts ==================== + + async def list_scripts(self, **kwargs) -> List[Dict]: + """List all available scripts.""" + return self.client.list(f'{self.base_endpoint}/scripts', params=kwargs) + + async def get_script(self, id: str) -> Dict: + """Get a specific script by ID.""" + return self.client.get(f'{self.base_endpoint}/scripts', id) + + async def run_script(self, id: str, data: Dict[str, Any], commit: bool = True) -> Dict: + """Run a script with the provided data.""" + payload = {'data': data, 'commit': commit} + return self.client.create(f'{self.base_endpoint}/scripts/{id}', payload) + + # ==================== Reports ==================== + + async def list_reports(self, **kwargs) -> List[Dict]: + """List all available reports.""" + return self.client.list(f'{self.base_endpoint}/reports', params=kwargs) + + async def get_report(self, id: str) -> Dict: + """Get a specific report by ID.""" + return self.client.get(f'{self.base_endpoint}/reports', id) + + async def run_report(self, id: str) -> Dict: + """Run a report.""" + return self.client.create(f'{self.base_endpoint}/reports/{id}', {}) diff --git a/mcp-servers/netbox/mcp_server/tools/ipam.py b/mcp-servers/netbox/mcp_server/tools/ipam.py new file mode 100644 index 0000000..336b3d2 --- /dev/null +++ b/mcp-servers/netbox/mcp_server/tools/ipam.py @@ -0,0 +1,718 @@ +""" +IPAM (IP Address Management) tools for NetBox MCP Server. + +Covers: IP Addresses, Prefixes, VLANs, VRFs, ASNs, and related models. +""" +import logging +from typing import List, Dict, Optional, Any +from ..netbox_client import NetBoxClient + +logger = logging.getLogger(__name__) + + +class IPAMTools: + """Tools for IPAM operations in NetBox""" + + def __init__(self, client: NetBoxClient): + self.client = client + self.base_endpoint = 'ipam' + + # ==================== ASN Ranges ==================== + + async def list_asn_ranges(self, name: Optional[str] = None, **kwargs) -> List[Dict]: + """List all ASN ranges.""" + params = {k: v for k, v in {'name': name, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/asn-ranges', params=params) + + async def get_asn_range(self, id: int) -> Dict: + """Get a specific ASN range by ID.""" + return self.client.get(f'{self.base_endpoint}/asn-ranges', id) + + async def create_asn_range(self, name: str, slug: str, rir: int, start: int, end: int, **kwargs) -> Dict: + """Create a new ASN range.""" + data = {'name': name, 'slug': slug, 'rir': rir, 'start': start, 'end': end, **kwargs} + return self.client.create(f'{self.base_endpoint}/asn-ranges', data) + + async def update_asn_range(self, id: int, **kwargs) -> Dict: + """Update an ASN range.""" + return self.client.patch(f'{self.base_endpoint}/asn-ranges', id, kwargs) + + async def delete_asn_range(self, id: int) -> None: + """Delete an ASN range.""" + self.client.delete(f'{self.base_endpoint}/asn-ranges', id) + + # ==================== ASNs ==================== + + async def list_asns( + self, + asn: Optional[int] = None, + rir_id: Optional[int] = None, + tenant_id: Optional[int] = None, + **kwargs + ) -> List[Dict]: + """List all ASNs.""" + params = {k: v for k, v in { + 'asn': asn, 'rir_id': rir_id, 'tenant_id': tenant_id, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/asns', params=params) + + async def get_asn(self, id: int) -> Dict: + """Get a specific ASN by ID.""" + return self.client.get(f'{self.base_endpoint}/asns', id) + + async def create_asn( + self, + asn: int, + rir: int, + tenant: Optional[int] = None, + description: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new ASN.""" + data = {'asn': asn, 'rir': rir, **kwargs} + if tenant: + data['tenant'] = tenant + if description: + data['description'] = description + return self.client.create(f'{self.base_endpoint}/asns', data) + + async def update_asn(self, id: int, **kwargs) -> Dict: + """Update an ASN.""" + return self.client.patch(f'{self.base_endpoint}/asns', id, kwargs) + + async def delete_asn(self, id: int) -> None: + """Delete an ASN.""" + self.client.delete(f'{self.base_endpoint}/asns', id) + + # ==================== RIRs ==================== + + async def list_rirs(self, name: Optional[str] = None, **kwargs) -> List[Dict]: + """List all RIRs (Regional Internet Registries).""" + params = {k: v for k, v in {'name': name, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/rirs', params=params) + + async def get_rir(self, id: int) -> Dict: + """Get a specific RIR by ID.""" + return self.client.get(f'{self.base_endpoint}/rirs', id) + + async def create_rir(self, name: str, slug: str, is_private: bool = False, **kwargs) -> Dict: + """Create a new RIR.""" + data = {'name': name, 'slug': slug, 'is_private': is_private, **kwargs} + return self.client.create(f'{self.base_endpoint}/rirs', data) + + async def update_rir(self, id: int, **kwargs) -> Dict: + """Update a RIR.""" + return self.client.patch(f'{self.base_endpoint}/rirs', id, kwargs) + + async def delete_rir(self, id: int) -> None: + """Delete a RIR.""" + self.client.delete(f'{self.base_endpoint}/rirs', id) + + # ==================== Aggregates ==================== + + async def list_aggregates( + self, + prefix: Optional[str] = None, + rir_id: Optional[int] = None, + tenant_id: Optional[int] = None, + **kwargs + ) -> List[Dict]: + """List all aggregates.""" + params = {k: v for k, v in { + 'prefix': prefix, 'rir_id': rir_id, 'tenant_id': tenant_id, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/aggregates', params=params) + + async def get_aggregate(self, id: int) -> Dict: + """Get a specific aggregate by ID.""" + return self.client.get(f'{self.base_endpoint}/aggregates', id) + + async def create_aggregate( + self, + prefix: str, + rir: int, + tenant: Optional[int] = None, + description: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new aggregate.""" + data = {'prefix': prefix, 'rir': rir, **kwargs} + if tenant: + data['tenant'] = tenant + if description: + data['description'] = description + return self.client.create(f'{self.base_endpoint}/aggregates', data) + + async def update_aggregate(self, id: int, **kwargs) -> Dict: + """Update an aggregate.""" + return self.client.patch(f'{self.base_endpoint}/aggregates', id, kwargs) + + async def delete_aggregate(self, id: int) -> None: + """Delete an aggregate.""" + self.client.delete(f'{self.base_endpoint}/aggregates', id) + + # ==================== Roles ==================== + + async def list_roles(self, name: Optional[str] = None, **kwargs) -> List[Dict]: + """List all IPAM roles.""" + params = {k: v for k, v in {'name': name, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/roles', params=params) + + async def get_role(self, id: int) -> Dict: + """Get a specific role by ID.""" + return self.client.get(f'{self.base_endpoint}/roles', id) + + async def create_role(self, name: str, slug: str, weight: int = 1000, **kwargs) -> Dict: + """Create a new IPAM role.""" + data = {'name': name, 'slug': slug, 'weight': weight, **kwargs} + return self.client.create(f'{self.base_endpoint}/roles', data) + + async def update_role(self, id: int, **kwargs) -> Dict: + """Update a role.""" + return self.client.patch(f'{self.base_endpoint}/roles', id, kwargs) + + async def delete_role(self, id: int) -> None: + """Delete a role.""" + self.client.delete(f'{self.base_endpoint}/roles', id) + + # ==================== Prefixes ==================== + + async def list_prefixes( + self, + prefix: Optional[str] = None, + site_id: Optional[int] = None, + vrf_id: Optional[int] = None, + vlan_id: Optional[int] = None, + role_id: Optional[int] = None, + tenant_id: Optional[int] = None, + status: Optional[str] = None, + family: Optional[int] = None, + is_pool: Optional[bool] = None, + within: Optional[str] = None, + within_include: Optional[str] = None, + contains: Optional[str] = None, + **kwargs + ) -> List[Dict]: + """List all prefixes with optional filtering.""" + params = {k: v for k, v in { + 'prefix': prefix, 'site_id': site_id, 'vrf_id': vrf_id, + 'vlan_id': vlan_id, 'role_id': role_id, 'tenant_id': tenant_id, + 'status': status, 'family': family, 'is_pool': is_pool, + 'within': within, 'within_include': within_include, 'contains': contains, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/prefixes', params=params) + + async def get_prefix(self, id: int) -> Dict: + """Get a specific prefix by ID.""" + return self.client.get(f'{self.base_endpoint}/prefixes', id) + + async def create_prefix( + self, + prefix: str, + status: str = 'active', + site: Optional[int] = None, + vrf: Optional[int] = None, + vlan: Optional[int] = None, + role: Optional[int] = None, + tenant: Optional[int] = None, + is_pool: bool = False, + mark_utilized: bool = False, + description: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new prefix.""" + data = {'prefix': prefix, 'status': status, 'is_pool': is_pool, 'mark_utilized': mark_utilized, **kwargs} + for key, val in [ + ('site', site), ('vrf', vrf), ('vlan', vlan), + ('role', role), ('tenant', tenant), ('description', description) + ]: + if val is not None: + data[key] = val + return self.client.create(f'{self.base_endpoint}/prefixes', data) + + async def update_prefix(self, id: int, **kwargs) -> Dict: + """Update a prefix.""" + return self.client.patch(f'{self.base_endpoint}/prefixes', id, kwargs) + + async def delete_prefix(self, id: int) -> None: + """Delete a prefix.""" + self.client.delete(f'{self.base_endpoint}/prefixes', id) + + async def list_available_prefixes(self, id: int) -> List[Dict]: + """List available child prefixes within a prefix.""" + return self.client.list(f'{self.base_endpoint}/prefixes/{id}/available-prefixes', paginate=False) + + async def create_available_prefix(self, id: int, prefix_length: int, **kwargs) -> Dict: + """Create a new prefix from available space.""" + data = {'prefix_length': prefix_length, **kwargs} + return self.client.create(f'{self.base_endpoint}/prefixes/{id}/available-prefixes', data) + + async def list_available_ips(self, id: int) -> List[Dict]: + """List available IP addresses within a prefix.""" + return self.client.list(f'{self.base_endpoint}/prefixes/{id}/available-ips', paginate=False) + + async def create_available_ip(self, id: int, **kwargs) -> Dict: + """Create a new IP address from available space in prefix.""" + return self.client.create(f'{self.base_endpoint}/prefixes/{id}/available-ips', kwargs) + + # ==================== IP Ranges ==================== + + async def list_ip_ranges( + self, + start_address: Optional[str] = None, + end_address: Optional[str] = None, + vrf_id: Optional[int] = None, + tenant_id: Optional[int] = None, + status: Optional[str] = None, + **kwargs + ) -> List[Dict]: + """List all IP ranges.""" + params = {k: v for k, v in { + 'start_address': start_address, 'end_address': end_address, + 'vrf_id': vrf_id, 'tenant_id': tenant_id, 'status': status, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/ip-ranges', params=params) + + async def get_ip_range(self, id: int) -> Dict: + """Get a specific IP range by ID.""" + return self.client.get(f'{self.base_endpoint}/ip-ranges', id) + + async def create_ip_range( + self, + start_address: str, + end_address: str, + status: str = 'active', + vrf: Optional[int] = None, + tenant: Optional[int] = None, + role: Optional[int] = None, + description: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new IP range.""" + data = {'start_address': start_address, 'end_address': end_address, 'status': status, **kwargs} + for key, val in [('vrf', vrf), ('tenant', tenant), ('role', role), ('description', description)]: + if val is not None: + data[key] = val + return self.client.create(f'{self.base_endpoint}/ip-ranges', data) + + async def update_ip_range(self, id: int, **kwargs) -> Dict: + """Update an IP range.""" + return self.client.patch(f'{self.base_endpoint}/ip-ranges', id, kwargs) + + async def delete_ip_range(self, id: int) -> None: + """Delete an IP range.""" + self.client.delete(f'{self.base_endpoint}/ip-ranges', id) + + async def list_available_ips_in_range(self, id: int) -> List[Dict]: + """List available IP addresses within an IP range.""" + return self.client.list(f'{self.base_endpoint}/ip-ranges/{id}/available-ips', paginate=False) + + # ==================== IP Addresses ==================== + + async def list_ip_addresses( + self, + address: Optional[str] = None, + vrf_id: Optional[int] = None, + tenant_id: Optional[int] = None, + status: Optional[str] = None, + role: Optional[str] = None, + interface_id: Optional[int] = None, + device_id: Optional[int] = None, + virtual_machine_id: Optional[int] = None, + family: Optional[int] = None, + parent: Optional[str] = None, + dns_name: Optional[str] = None, + **kwargs + ) -> List[Dict]: + """List all IP addresses with optional filtering.""" + params = {k: v for k, v in { + 'address': address, 'vrf_id': vrf_id, 'tenant_id': tenant_id, + 'status': status, 'role': role, 'interface_id': interface_id, + 'device_id': device_id, 'virtual_machine_id': virtual_machine_id, + 'family': family, 'parent': parent, 'dns_name': dns_name, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/ip-addresses', params=params) + + async def get_ip_address(self, id: int) -> Dict: + """Get a specific IP address by ID.""" + return self.client.get(f'{self.base_endpoint}/ip-addresses', id) + + async def create_ip_address( + self, + address: str, + status: str = 'active', + vrf: Optional[int] = None, + tenant: Optional[int] = None, + role: Optional[str] = None, + assigned_object_type: Optional[str] = None, + assigned_object_id: Optional[int] = None, + nat_inside: Optional[int] = None, + dns_name: Optional[str] = None, + description: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new IP address.""" + data = {'address': address, 'status': status, **kwargs} + for key, val in [ + ('vrf', vrf), ('tenant', tenant), ('role', role), + ('assigned_object_type', assigned_object_type), + ('assigned_object_id', assigned_object_id), + ('nat_inside', nat_inside), ('dns_name', dns_name), + ('description', description) + ]: + if val is not None: + data[key] = val + return self.client.create(f'{self.base_endpoint}/ip-addresses', data) + + async def update_ip_address(self, id: int, **kwargs) -> Dict: + """Update an IP address.""" + return self.client.patch(f'{self.base_endpoint}/ip-addresses', id, kwargs) + + async def delete_ip_address(self, id: int) -> None: + """Delete an IP address.""" + self.client.delete(f'{self.base_endpoint}/ip-addresses', id) + + # ==================== FHRP Groups ==================== + + async def list_fhrp_groups( + self, + protocol: Optional[str] = None, + group_id: Optional[int] = None, + **kwargs + ) -> List[Dict]: + """List all FHRP groups.""" + params = {k: v for k, v in {'protocol': protocol, 'group_id': group_id, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/fhrp-groups', params=params) + + async def get_fhrp_group(self, id: int) -> Dict: + """Get a specific FHRP group by ID.""" + return self.client.get(f'{self.base_endpoint}/fhrp-groups', id) + + async def create_fhrp_group( + self, + protocol: str, + group_id: int, + auth_type: Optional[str] = None, + auth_key: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new FHRP group.""" + data = {'protocol': protocol, 'group_id': group_id, **kwargs} + if auth_type: + data['auth_type'] = auth_type + if auth_key: + data['auth_key'] = auth_key + return self.client.create(f'{self.base_endpoint}/fhrp-groups', data) + + async def update_fhrp_group(self, id: int, **kwargs) -> Dict: + """Update an FHRP group.""" + return self.client.patch(f'{self.base_endpoint}/fhrp-groups', id, kwargs) + + async def delete_fhrp_group(self, id: int) -> None: + """Delete an FHRP group.""" + self.client.delete(f'{self.base_endpoint}/fhrp-groups', id) + + # ==================== FHRP Group Assignments ==================== + + async def list_fhrp_group_assignments(self, group_id: Optional[int] = None, **kwargs) -> List[Dict]: + """List all FHRP group assignments.""" + params = {k: v for k, v in {'group_id': group_id, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/fhrp-group-assignments', params=params) + + async def get_fhrp_group_assignment(self, id: int) -> Dict: + """Get a specific FHRP group assignment by ID.""" + return self.client.get(f'{self.base_endpoint}/fhrp-group-assignments', id) + + async def create_fhrp_group_assignment( + self, + group: int, + interface_type: str, + interface_id: int, + priority: int = 100, + **kwargs + ) -> Dict: + """Create a new FHRP group assignment.""" + data = { + 'group': group, 'interface_type': interface_type, + 'interface_id': interface_id, 'priority': priority, **kwargs + } + return self.client.create(f'{self.base_endpoint}/fhrp-group-assignments', data) + + async def update_fhrp_group_assignment(self, id: int, **kwargs) -> Dict: + """Update an FHRP group assignment.""" + return self.client.patch(f'{self.base_endpoint}/fhrp-group-assignments', id, kwargs) + + async def delete_fhrp_group_assignment(self, id: int) -> None: + """Delete an FHRP group assignment.""" + self.client.delete(f'{self.base_endpoint}/fhrp-group-assignments', id) + + # ==================== VLAN Groups ==================== + + async def list_vlan_groups( + self, + name: Optional[str] = None, + site_id: Optional[int] = None, + **kwargs + ) -> List[Dict]: + """List all VLAN groups.""" + params = {k: v for k, v in {'name': name, 'site_id': site_id, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/vlan-groups', params=params) + + async def get_vlan_group(self, id: int) -> Dict: + """Get a specific VLAN group by ID.""" + return self.client.get(f'{self.base_endpoint}/vlan-groups', id) + + async def create_vlan_group( + self, + name: str, + slug: str, + scope_type: Optional[str] = None, + scope_id: Optional[int] = None, + min_vid: int = 1, + max_vid: int = 4094, + **kwargs + ) -> Dict: + """Create a new VLAN group.""" + data = {'name': name, 'slug': slug, 'min_vid': min_vid, 'max_vid': max_vid, **kwargs} + if scope_type: + data['scope_type'] = scope_type + if scope_id: + data['scope_id'] = scope_id + return self.client.create(f'{self.base_endpoint}/vlan-groups', data) + + async def update_vlan_group(self, id: int, **kwargs) -> Dict: + """Update a VLAN group.""" + return self.client.patch(f'{self.base_endpoint}/vlan-groups', id, kwargs) + + async def delete_vlan_group(self, id: int) -> None: + """Delete a VLAN group.""" + self.client.delete(f'{self.base_endpoint}/vlan-groups', id) + + async def list_available_vlans(self, id: int) -> List[Dict]: + """List available VLANs in a VLAN group.""" + return self.client.list(f'{self.base_endpoint}/vlan-groups/{id}/available-vlans', paginate=False) + + # ==================== VLANs ==================== + + async def list_vlans( + self, + vid: Optional[int] = None, + name: Optional[str] = None, + site_id: Optional[int] = None, + group_id: Optional[int] = None, + role_id: Optional[int] = None, + tenant_id: Optional[int] = None, + status: Optional[str] = None, + **kwargs + ) -> List[Dict]: + """List all VLANs with optional filtering.""" + params = {k: v for k, v in { + 'vid': vid, 'name': name, 'site_id': site_id, 'group_id': group_id, + 'role_id': role_id, 'tenant_id': tenant_id, 'status': status, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/vlans', params=params) + + async def get_vlan(self, id: int) -> Dict: + """Get a specific VLAN by ID.""" + return self.client.get(f'{self.base_endpoint}/vlans', id) + + async def create_vlan( + self, + vid: int, + name: str, + status: str = 'active', + site: Optional[int] = None, + group: Optional[int] = None, + role: Optional[int] = None, + tenant: Optional[int] = None, + description: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new VLAN.""" + data = {'vid': vid, 'name': name, 'status': status, **kwargs} + for key, val in [ + ('site', site), ('group', group), ('role', role), + ('tenant', tenant), ('description', description) + ]: + if val is not None: + data[key] = val + return self.client.create(f'{self.base_endpoint}/vlans', data) + + async def update_vlan(self, id: int, **kwargs) -> Dict: + """Update a VLAN.""" + return self.client.patch(f'{self.base_endpoint}/vlans', id, kwargs) + + async def delete_vlan(self, id: int) -> None: + """Delete a VLAN.""" + self.client.delete(f'{self.base_endpoint}/vlans', id) + + # ==================== VRFs ==================== + + async def list_vrfs( + self, + name: Optional[str] = None, + rd: Optional[str] = None, + tenant_id: Optional[int] = None, + **kwargs + ) -> List[Dict]: + """List all VRFs with optional filtering.""" + params = {k: v for k, v in { + 'name': name, 'rd': rd, 'tenant_id': tenant_id, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/vrfs', params=params) + + async def get_vrf(self, id: int) -> Dict: + """Get a specific VRF by ID.""" + return self.client.get(f'{self.base_endpoint}/vrfs', id) + + async def create_vrf( + self, + name: str, + rd: Optional[str] = None, + tenant: Optional[int] = None, + enforce_unique: bool = True, + description: Optional[str] = None, + import_targets: Optional[List[int]] = None, + export_targets: Optional[List[int]] = None, + **kwargs + ) -> Dict: + """Create a new VRF.""" + data = {'name': name, 'enforce_unique': enforce_unique, **kwargs} + for key, val in [ + ('rd', rd), ('tenant', tenant), ('description', description), + ('import_targets', import_targets), ('export_targets', export_targets) + ]: + if val is not None: + data[key] = val + return self.client.create(f'{self.base_endpoint}/vrfs', data) + + async def update_vrf(self, id: int, **kwargs) -> Dict: + """Update a VRF.""" + return self.client.patch(f'{self.base_endpoint}/vrfs', id, kwargs) + + async def delete_vrf(self, id: int) -> None: + """Delete a VRF.""" + self.client.delete(f'{self.base_endpoint}/vrfs', id) + + # ==================== Route Targets ==================== + + async def list_route_targets( + self, + name: Optional[str] = None, + tenant_id: Optional[int] = None, + **kwargs + ) -> List[Dict]: + """List all route targets.""" + params = {k: v for k, v in {'name': name, 'tenant_id': tenant_id, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/route-targets', params=params) + + async def get_route_target(self, id: int) -> Dict: + """Get a specific route target by ID.""" + return self.client.get(f'{self.base_endpoint}/route-targets', id) + + async def create_route_target( + self, + name: str, + tenant: Optional[int] = None, + description: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new route target.""" + data = {'name': name, **kwargs} + if tenant: + data['tenant'] = tenant + if description: + data['description'] = description + return self.client.create(f'{self.base_endpoint}/route-targets', data) + + async def update_route_target(self, id: int, **kwargs) -> Dict: + """Update a route target.""" + return self.client.patch(f'{self.base_endpoint}/route-targets', id, kwargs) + + async def delete_route_target(self, id: int) -> None: + """Delete a route target.""" + self.client.delete(f'{self.base_endpoint}/route-targets', id) + + # ==================== Services ==================== + + async def list_services( + self, + device_id: Optional[int] = None, + virtual_machine_id: Optional[int] = None, + name: Optional[str] = None, + protocol: Optional[str] = None, + port: Optional[int] = None, + **kwargs + ) -> List[Dict]: + """List all services.""" + params = {k: v for k, v in { + 'device_id': device_id, 'virtual_machine_id': virtual_machine_id, + 'name': name, 'protocol': protocol, 'port': port, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/services', params=params) + + async def get_service(self, id: int) -> Dict: + """Get a specific service by ID.""" + return self.client.get(f'{self.base_endpoint}/services', id) + + async def create_service( + self, + name: str, + ports: List[int], + protocol: str, + device: Optional[int] = None, + virtual_machine: Optional[int] = None, + ipaddresses: Optional[List[int]] = None, + description: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new service.""" + data = {'name': name, 'ports': ports, 'protocol': protocol, **kwargs} + for key, val in [ + ('device', device), ('virtual_machine', virtual_machine), + ('ipaddresses', ipaddresses), ('description', description) + ]: + if val is not None: + data[key] = val + return self.client.create(f'{self.base_endpoint}/services', data) + + async def update_service(self, id: int, **kwargs) -> Dict: + """Update a service.""" + return self.client.patch(f'{self.base_endpoint}/services', id, kwargs) + + async def delete_service(self, id: int) -> None: + """Delete a service.""" + self.client.delete(f'{self.base_endpoint}/services', id) + + # ==================== Service Templates ==================== + + async def list_service_templates(self, name: Optional[str] = None, **kwargs) -> List[Dict]: + """List all service templates.""" + params = {k: v for k, v in {'name': name, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/service-templates', params=params) + + async def get_service_template(self, id: int) -> Dict: + """Get a specific service template by ID.""" + return self.client.get(f'{self.base_endpoint}/service-templates', id) + + async def create_service_template( + self, + name: str, + ports: List[int], + protocol: str, + description: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new service template.""" + data = {'name': name, 'ports': ports, 'protocol': protocol, **kwargs} + if description: + data['description'] = description + return self.client.create(f'{self.base_endpoint}/service-templates', data) + + async def update_service_template(self, id: int, **kwargs) -> Dict: + """Update a service template.""" + return self.client.patch(f'{self.base_endpoint}/service-templates', id, kwargs) + + async def delete_service_template(self, id: int) -> None: + """Delete a service template.""" + self.client.delete(f'{self.base_endpoint}/service-templates', id) diff --git a/mcp-servers/netbox/mcp_server/tools/tenancy.py b/mcp-servers/netbox/mcp_server/tools/tenancy.py new file mode 100644 index 0000000..bb1e3b7 --- /dev/null +++ b/mcp-servers/netbox/mcp_server/tools/tenancy.py @@ -0,0 +1,281 @@ +""" +Tenancy tools for NetBox MCP Server. + +Covers: Tenants, Tenant Groups, Contacts, Contact Groups, and Contact Roles. +""" +import logging +from typing import List, Dict, Optional, Any +from ..netbox_client import NetBoxClient + +logger = logging.getLogger(__name__) + + +class TenancyTools: + """Tools for Tenancy operations in NetBox""" + + def __init__(self, client: NetBoxClient): + self.client = client + self.base_endpoint = 'tenancy' + + # ==================== Tenant Groups ==================== + + async def list_tenant_groups( + self, + name: Optional[str] = None, + slug: Optional[str] = None, + parent_id: Optional[int] = None, + **kwargs + ) -> List[Dict]: + """List all tenant groups.""" + params = {k: v for k, v in { + 'name': name, 'slug': slug, 'parent_id': parent_id, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/tenant-groups', params=params) + + async def get_tenant_group(self, id: int) -> Dict: + """Get a specific tenant group by ID.""" + return self.client.get(f'{self.base_endpoint}/tenant-groups', id) + + async def create_tenant_group( + self, + name: str, + slug: str, + parent: Optional[int] = None, + description: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new tenant group.""" + data = {'name': name, 'slug': slug, **kwargs} + if parent: + data['parent'] = parent + if description: + data['description'] = description + return self.client.create(f'{self.base_endpoint}/tenant-groups', data) + + async def update_tenant_group(self, id: int, **kwargs) -> Dict: + """Update a tenant group.""" + return self.client.patch(f'{self.base_endpoint}/tenant-groups', id, kwargs) + + async def delete_tenant_group(self, id: int) -> None: + """Delete a tenant group.""" + self.client.delete(f'{self.base_endpoint}/tenant-groups', id) + + # ==================== Tenants ==================== + + async def list_tenants( + self, + name: Optional[str] = None, + slug: Optional[str] = None, + group_id: Optional[int] = None, + **kwargs + ) -> List[Dict]: + """List all tenants with optional filtering.""" + params = {k: v for k, v in { + 'name': name, 'slug': slug, 'group_id': group_id, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/tenants', params=params) + + async def get_tenant(self, id: int) -> Dict: + """Get a specific tenant by ID.""" + return self.client.get(f'{self.base_endpoint}/tenants', id) + + async def create_tenant( + self, + name: str, + slug: str, + group: Optional[int] = None, + description: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new tenant.""" + data = {'name': name, 'slug': slug, **kwargs} + if group: + data['group'] = group + if description: + data['description'] = description + return self.client.create(f'{self.base_endpoint}/tenants', data) + + async def update_tenant(self, id: int, **kwargs) -> Dict: + """Update a tenant.""" + return self.client.patch(f'{self.base_endpoint}/tenants', id, kwargs) + + async def delete_tenant(self, id: int) -> None: + """Delete a tenant.""" + self.client.delete(f'{self.base_endpoint}/tenants', id) + + # ==================== Contact Groups ==================== + + async def list_contact_groups( + self, + name: Optional[str] = None, + slug: Optional[str] = None, + parent_id: Optional[int] = None, + **kwargs + ) -> List[Dict]: + """List all contact groups.""" + params = {k: v for k, v in { + 'name': name, 'slug': slug, 'parent_id': parent_id, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/contact-groups', params=params) + + async def get_contact_group(self, id: int) -> Dict: + """Get a specific contact group by ID.""" + return self.client.get(f'{self.base_endpoint}/contact-groups', id) + + async def create_contact_group( + self, + name: str, + slug: str, + parent: Optional[int] = None, + description: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new contact group.""" + data = {'name': name, 'slug': slug, **kwargs} + if parent: + data['parent'] = parent + if description: + data['description'] = description + return self.client.create(f'{self.base_endpoint}/contact-groups', data) + + async def update_contact_group(self, id: int, **kwargs) -> Dict: + """Update a contact group.""" + return self.client.patch(f'{self.base_endpoint}/contact-groups', id, kwargs) + + async def delete_contact_group(self, id: int) -> None: + """Delete a contact group.""" + self.client.delete(f'{self.base_endpoint}/contact-groups', id) + + # ==================== Contact Roles ==================== + + async def list_contact_roles( + self, + name: Optional[str] = None, + slug: Optional[str] = None, + **kwargs + ) -> List[Dict]: + """List all contact roles.""" + params = {k: v for k, v in {'name': name, 'slug': slug, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/contact-roles', params=params) + + async def get_contact_role(self, id: int) -> Dict: + """Get a specific contact role by ID.""" + return self.client.get(f'{self.base_endpoint}/contact-roles', id) + + async def create_contact_role( + self, + name: str, + slug: str, + description: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new contact role.""" + data = {'name': name, 'slug': slug, **kwargs} + if description: + data['description'] = description + return self.client.create(f'{self.base_endpoint}/contact-roles', data) + + async def update_contact_role(self, id: int, **kwargs) -> Dict: + """Update a contact role.""" + return self.client.patch(f'{self.base_endpoint}/contact-roles', id, kwargs) + + async def delete_contact_role(self, id: int) -> None: + """Delete a contact role.""" + self.client.delete(f'{self.base_endpoint}/contact-roles', id) + + # ==================== Contacts ==================== + + async def list_contacts( + self, + name: Optional[str] = None, + group_id: Optional[int] = None, + email: Optional[str] = None, + **kwargs + ) -> List[Dict]: + """List all contacts with optional filtering.""" + params = {k: v for k, v in { + 'name': name, 'group_id': group_id, 'email': email, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/contacts', params=params) + + async def get_contact(self, id: int) -> Dict: + """Get a specific contact by ID.""" + return self.client.get(f'{self.base_endpoint}/contacts', id) + + async def create_contact( + self, + name: str, + group: Optional[int] = None, + title: Optional[str] = None, + phone: Optional[str] = None, + email: Optional[str] = None, + address: Optional[str] = None, + link: Optional[str] = None, + description: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new contact.""" + data = {'name': name, **kwargs} + for key, val in [ + ('group', group), ('title', title), ('phone', phone), + ('email', email), ('address', address), ('link', link), + ('description', description) + ]: + if val is not None: + data[key] = val + return self.client.create(f'{self.base_endpoint}/contacts', data) + + async def update_contact(self, id: int, **kwargs) -> Dict: + """Update a contact.""" + return self.client.patch(f'{self.base_endpoint}/contacts', id, kwargs) + + async def delete_contact(self, id: int) -> None: + """Delete a contact.""" + self.client.delete(f'{self.base_endpoint}/contacts', id) + + # ==================== Contact Assignments ==================== + + async def list_contact_assignments( + self, + contact_id: Optional[int] = None, + role_id: Optional[int] = None, + object_type: Optional[str] = None, + object_id: Optional[int] = None, + **kwargs + ) -> List[Dict]: + """List all contact assignments.""" + params = {k: v for k, v in { + 'contact_id': contact_id, 'role_id': role_id, + 'object_type': object_type, 'object_id': object_id, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/contact-assignments', params=params) + + async def get_contact_assignment(self, id: int) -> Dict: + """Get a specific contact assignment by ID.""" + return self.client.get(f'{self.base_endpoint}/contact-assignments', id) + + async def create_contact_assignment( + self, + contact: int, + role: int, + object_type: str, + object_id: int, + priority: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new contact assignment.""" + data = { + 'contact': contact, 'role': role, + 'object_type': object_type, 'object_id': object_id, **kwargs + } + if priority: + data['priority'] = priority + return self.client.create(f'{self.base_endpoint}/contact-assignments', data) + + async def update_contact_assignment(self, id: int, **kwargs) -> Dict: + """Update a contact assignment.""" + return self.client.patch(f'{self.base_endpoint}/contact-assignments', id, kwargs) + + async def delete_contact_assignment(self, id: int) -> None: + """Delete a contact assignment.""" + self.client.delete(f'{self.base_endpoint}/contact-assignments', id) diff --git a/mcp-servers/netbox/mcp_server/tools/virtualization.py b/mcp-servers/netbox/mcp_server/tools/virtualization.py new file mode 100644 index 0000000..9bf3e84 --- /dev/null +++ b/mcp-servers/netbox/mcp_server/tools/virtualization.py @@ -0,0 +1,296 @@ +""" +Virtualization tools for NetBox MCP Server. + +Covers: Clusters, Virtual Machines, VM Interfaces, and related models. +""" +import logging +from typing import List, Dict, Optional, Any +from ..netbox_client import NetBoxClient + +logger = logging.getLogger(__name__) + + +class VirtualizationTools: + """Tools for Virtualization operations in NetBox""" + + def __init__(self, client: NetBoxClient): + self.client = client + self.base_endpoint = 'virtualization' + + # ==================== Cluster Types ==================== + + async def list_cluster_types( + self, + name: Optional[str] = None, + slug: Optional[str] = None, + **kwargs + ) -> List[Dict]: + """List all cluster types.""" + params = {k: v for k, v in {'name': name, 'slug': slug, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/cluster-types', params=params) + + async def get_cluster_type(self, id: int) -> Dict: + """Get a specific cluster type by ID.""" + return self.client.get(f'{self.base_endpoint}/cluster-types', id) + + async def create_cluster_type( + self, + name: str, + slug: str, + description: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new cluster type.""" + data = {'name': name, 'slug': slug, **kwargs} + if description: + data['description'] = description + return self.client.create(f'{self.base_endpoint}/cluster-types', data) + + async def update_cluster_type(self, id: int, **kwargs) -> Dict: + """Update a cluster type.""" + return self.client.patch(f'{self.base_endpoint}/cluster-types', id, kwargs) + + async def delete_cluster_type(self, id: int) -> None: + """Delete a cluster type.""" + self.client.delete(f'{self.base_endpoint}/cluster-types', id) + + # ==================== Cluster Groups ==================== + + async def list_cluster_groups( + self, + name: Optional[str] = None, + slug: Optional[str] = None, + **kwargs + ) -> List[Dict]: + """List all cluster groups.""" + params = {k: v for k, v in {'name': name, 'slug': slug, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/cluster-groups', params=params) + + async def get_cluster_group(self, id: int) -> Dict: + """Get a specific cluster group by ID.""" + return self.client.get(f'{self.base_endpoint}/cluster-groups', id) + + async def create_cluster_group( + self, + name: str, + slug: str, + description: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new cluster group.""" + data = {'name': name, 'slug': slug, **kwargs} + if description: + data['description'] = description + return self.client.create(f'{self.base_endpoint}/cluster-groups', data) + + async def update_cluster_group(self, id: int, **kwargs) -> Dict: + """Update a cluster group.""" + return self.client.patch(f'{self.base_endpoint}/cluster-groups', id, kwargs) + + async def delete_cluster_group(self, id: int) -> None: + """Delete a cluster group.""" + self.client.delete(f'{self.base_endpoint}/cluster-groups', id) + + # ==================== Clusters ==================== + + async def list_clusters( + self, + name: Optional[str] = None, + type_id: Optional[int] = None, + group_id: Optional[int] = None, + site_id: Optional[int] = None, + tenant_id: Optional[int] = None, + status: Optional[str] = None, + **kwargs + ) -> List[Dict]: + """List all clusters with optional filtering.""" + params = {k: v for k, v in { + 'name': name, 'type_id': type_id, 'group_id': group_id, + 'site_id': site_id, 'tenant_id': tenant_id, 'status': status, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/clusters', params=params) + + async def get_cluster(self, id: int) -> Dict: + """Get a specific cluster by ID.""" + return self.client.get(f'{self.base_endpoint}/clusters', id) + + async def create_cluster( + self, + name: str, + type: int, + status: str = 'active', + group: Optional[int] = None, + site: Optional[int] = None, + tenant: Optional[int] = None, + description: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new cluster.""" + data = {'name': name, 'type': type, 'status': status, **kwargs} + for key, val in [ + ('group', group), ('site', site), ('tenant', tenant), ('description', description) + ]: + if val is not None: + data[key] = val + return self.client.create(f'{self.base_endpoint}/clusters', data) + + async def update_cluster(self, id: int, **kwargs) -> Dict: + """Update a cluster.""" + return self.client.patch(f'{self.base_endpoint}/clusters', id, kwargs) + + async def delete_cluster(self, id: int) -> None: + """Delete a cluster.""" + self.client.delete(f'{self.base_endpoint}/clusters', id) + + # ==================== Virtual Machines ==================== + + async def list_virtual_machines( + self, + name: Optional[str] = None, + cluster_id: Optional[int] = None, + site_id: Optional[int] = None, + role_id: Optional[int] = None, + tenant_id: Optional[int] = None, + platform_id: Optional[int] = None, + status: Optional[str] = None, + **kwargs + ) -> List[Dict]: + """List all virtual machines with optional filtering.""" + params = {k: v for k, v in { + 'name': name, 'cluster_id': cluster_id, 'site_id': site_id, + 'role_id': role_id, 'tenant_id': tenant_id, 'platform_id': platform_id, + 'status': status, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/virtual-machines', params=params) + + async def get_virtual_machine(self, id: int) -> Dict: + """Get a specific virtual machine by ID.""" + return self.client.get(f'{self.base_endpoint}/virtual-machines', id) + + async def create_virtual_machine( + self, + name: str, + status: str = 'active', + cluster: Optional[int] = None, + site: Optional[int] = None, + role: Optional[int] = None, + tenant: Optional[int] = None, + platform: Optional[int] = None, + primary_ip4: Optional[int] = None, + primary_ip6: Optional[int] = None, + vcpus: Optional[float] = None, + memory: Optional[int] = None, + disk: Optional[int] = None, + description: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new virtual machine.""" + data = {'name': name, 'status': status, **kwargs} + for key, val in [ + ('cluster', cluster), ('site', site), ('role', role), + ('tenant', tenant), ('platform', platform), + ('primary_ip4', primary_ip4), ('primary_ip6', primary_ip6), + ('vcpus', vcpus), ('memory', memory), ('disk', disk), + ('description', description) + ]: + if val is not None: + data[key] = val + return self.client.create(f'{self.base_endpoint}/virtual-machines', data) + + async def update_virtual_machine(self, id: int, **kwargs) -> Dict: + """Update a virtual machine.""" + return self.client.patch(f'{self.base_endpoint}/virtual-machines', id, kwargs) + + async def delete_virtual_machine(self, id: int) -> None: + """Delete a virtual machine.""" + self.client.delete(f'{self.base_endpoint}/virtual-machines', id) + + # ==================== VM Interfaces ==================== + + async def list_vm_interfaces( + self, + virtual_machine_id: Optional[int] = None, + name: Optional[str] = None, + enabled: Optional[bool] = None, + **kwargs + ) -> List[Dict]: + """List all VM interfaces.""" + params = {k: v for k, v in { + 'virtual_machine_id': virtual_machine_id, 'name': name, 'enabled': enabled, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/interfaces', params=params) + + async def get_vm_interface(self, id: int) -> Dict: + """Get a specific VM interface by ID.""" + return self.client.get(f'{self.base_endpoint}/interfaces', id) + + async def create_vm_interface( + self, + virtual_machine: int, + name: str, + enabled: bool = True, + mtu: Optional[int] = None, + mac_address: Optional[str] = None, + description: Optional[str] = None, + mode: Optional[str] = None, + untagged_vlan: Optional[int] = None, + tagged_vlans: Optional[List[int]] = None, + **kwargs + ) -> Dict: + """Create a new VM interface.""" + data = {'virtual_machine': virtual_machine, 'name': name, 'enabled': enabled, **kwargs} + for key, val in [ + ('mtu', mtu), ('mac_address', mac_address), ('description', description), + ('mode', mode), ('untagged_vlan', untagged_vlan), ('tagged_vlans', tagged_vlans) + ]: + if val is not None: + data[key] = val + return self.client.create(f'{self.base_endpoint}/interfaces', data) + + async def update_vm_interface(self, id: int, **kwargs) -> Dict: + """Update a VM interface.""" + return self.client.patch(f'{self.base_endpoint}/interfaces', id, kwargs) + + async def delete_vm_interface(self, id: int) -> None: + """Delete a VM interface.""" + self.client.delete(f'{self.base_endpoint}/interfaces', id) + + # ==================== Virtual Disks ==================== + + async def list_virtual_disks( + self, + virtual_machine_id: Optional[int] = None, + name: Optional[str] = None, + **kwargs + ) -> List[Dict]: + """List all virtual disks.""" + params = {k: v for k, v in { + 'virtual_machine_id': virtual_machine_id, 'name': name, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/virtual-disks', params=params) + + async def get_virtual_disk(self, id: int) -> Dict: + """Get a specific virtual disk by ID.""" + return self.client.get(f'{self.base_endpoint}/virtual-disks', id) + + async def create_virtual_disk( + self, + virtual_machine: int, + name: str, + size: int, + description: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new virtual disk.""" + data = {'virtual_machine': virtual_machine, 'name': name, 'size': size, **kwargs} + if description: + data['description'] = description + return self.client.create(f'{self.base_endpoint}/virtual-disks', data) + + async def update_virtual_disk(self, id: int, **kwargs) -> Dict: + """Update a virtual disk.""" + return self.client.patch(f'{self.base_endpoint}/virtual-disks', id, kwargs) + + async def delete_virtual_disk(self, id: int) -> None: + """Delete a virtual disk.""" + self.client.delete(f'{self.base_endpoint}/virtual-disks', id) diff --git a/mcp-servers/netbox/mcp_server/tools/vpn.py b/mcp-servers/netbox/mcp_server/tools/vpn.py new file mode 100644 index 0000000..e936e74 --- /dev/null +++ b/mcp-servers/netbox/mcp_server/tools/vpn.py @@ -0,0 +1,428 @@ +""" +VPN tools for NetBox MCP Server. + +Covers: Tunnels, Tunnel Groups, Tunnel Terminations, IKE/IPSec Policies, and L2VPN. +""" +import logging +from typing import List, Dict, Optional, Any +from ..netbox_client import NetBoxClient + +logger = logging.getLogger(__name__) + + +class VPNTools: + """Tools for VPN operations in NetBox""" + + def __init__(self, client: NetBoxClient): + self.client = client + self.base_endpoint = 'vpn' + + # ==================== Tunnel Groups ==================== + + async def list_tunnel_groups( + self, + name: Optional[str] = None, + slug: Optional[str] = None, + **kwargs + ) -> List[Dict]: + """List all tunnel groups.""" + params = {k: v for k, v in {'name': name, 'slug': slug, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/tunnel-groups', params=params) + + async def get_tunnel_group(self, id: int) -> Dict: + """Get a specific tunnel group by ID.""" + return self.client.get(f'{self.base_endpoint}/tunnel-groups', id) + + async def create_tunnel_group( + self, + name: str, + slug: str, + description: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new tunnel group.""" + data = {'name': name, 'slug': slug, **kwargs} + if description: + data['description'] = description + return self.client.create(f'{self.base_endpoint}/tunnel-groups', data) + + async def update_tunnel_group(self, id: int, **kwargs) -> Dict: + """Update a tunnel group.""" + return self.client.patch(f'{self.base_endpoint}/tunnel-groups', id, kwargs) + + async def delete_tunnel_group(self, id: int) -> None: + """Delete a tunnel group.""" + self.client.delete(f'{self.base_endpoint}/tunnel-groups', id) + + # ==================== Tunnels ==================== + + async def list_tunnels( + self, + name: Optional[str] = None, + status: Optional[str] = None, + group_id: Optional[int] = None, + encapsulation: Optional[str] = None, + tenant_id: Optional[int] = None, + **kwargs + ) -> List[Dict]: + """List all tunnels with optional filtering.""" + params = {k: v for k, v in { + 'name': name, 'status': status, 'group_id': group_id, + 'encapsulation': encapsulation, 'tenant_id': tenant_id, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/tunnels', params=params) + + async def get_tunnel(self, id: int) -> Dict: + """Get a specific tunnel by ID.""" + return self.client.get(f'{self.base_endpoint}/tunnels', id) + + async def create_tunnel( + self, + name: str, + status: str = 'active', + encapsulation: str = 'ipsec-tunnel', + group: Optional[int] = None, + ipsec_profile: Optional[int] = None, + tenant: Optional[int] = None, + tunnel_id: Optional[int] = None, + description: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new tunnel.""" + data = {'name': name, 'status': status, 'encapsulation': encapsulation, **kwargs} + for key, val in [ + ('group', group), ('ipsec_profile', ipsec_profile), + ('tenant', tenant), ('tunnel_id', tunnel_id), ('description', description) + ]: + if val is not None: + data[key] = val + return self.client.create(f'{self.base_endpoint}/tunnels', data) + + async def update_tunnel(self, id: int, **kwargs) -> Dict: + """Update a tunnel.""" + return self.client.patch(f'{self.base_endpoint}/tunnels', id, kwargs) + + async def delete_tunnel(self, id: int) -> None: + """Delete a tunnel.""" + self.client.delete(f'{self.base_endpoint}/tunnels', id) + + # ==================== Tunnel Terminations ==================== + + async def list_tunnel_terminations( + self, + tunnel_id: Optional[int] = None, + role: Optional[str] = None, + **kwargs + ) -> List[Dict]: + """List all tunnel terminations.""" + params = {k: v for k, v in { + 'tunnel_id': tunnel_id, 'role': role, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/tunnel-terminations', params=params) + + async def get_tunnel_termination(self, id: int) -> Dict: + """Get a specific tunnel termination by ID.""" + return self.client.get(f'{self.base_endpoint}/tunnel-terminations', id) + + async def create_tunnel_termination( + self, + tunnel: int, + role: str, + termination_type: str, + termination_id: int, + outside_ip: Optional[int] = None, + **kwargs + ) -> Dict: + """Create a new tunnel termination.""" + data = { + 'tunnel': tunnel, 'role': role, + 'termination_type': termination_type, 'termination_id': termination_id, **kwargs + } + if outside_ip: + data['outside_ip'] = outside_ip + return self.client.create(f'{self.base_endpoint}/tunnel-terminations', data) + + async def update_tunnel_termination(self, id: int, **kwargs) -> Dict: + """Update a tunnel termination.""" + return self.client.patch(f'{self.base_endpoint}/tunnel-terminations', id, kwargs) + + async def delete_tunnel_termination(self, id: int) -> None: + """Delete a tunnel termination.""" + self.client.delete(f'{self.base_endpoint}/tunnel-terminations', id) + + # ==================== IKE Proposals ==================== + + async def list_ike_proposals(self, name: Optional[str] = None, **kwargs) -> List[Dict]: + """List all IKE proposals.""" + params = {k: v for k, v in {'name': name, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/ike-proposals', params=params) + + async def get_ike_proposal(self, id: int) -> Dict: + """Get a specific IKE proposal by ID.""" + return self.client.get(f'{self.base_endpoint}/ike-proposals', id) + + async def create_ike_proposal( + self, + name: str, + authentication_method: str, + encryption_algorithm: str, + authentication_algorithm: str, + group: int, + sa_lifetime: Optional[int] = None, + description: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new IKE proposal.""" + data = { + 'name': name, 'authentication_method': authentication_method, + 'encryption_algorithm': encryption_algorithm, + 'authentication_algorithm': authentication_algorithm, 'group': group, **kwargs + } + if sa_lifetime: + data['sa_lifetime'] = sa_lifetime + if description: + data['description'] = description + return self.client.create(f'{self.base_endpoint}/ike-proposals', data) + + async def update_ike_proposal(self, id: int, **kwargs) -> Dict: + """Update an IKE proposal.""" + return self.client.patch(f'{self.base_endpoint}/ike-proposals', id, kwargs) + + async def delete_ike_proposal(self, id: int) -> None: + """Delete an IKE proposal.""" + self.client.delete(f'{self.base_endpoint}/ike-proposals', id) + + # ==================== IKE Policies ==================== + + async def list_ike_policies(self, name: Optional[str] = None, **kwargs) -> List[Dict]: + """List all IKE policies.""" + params = {k: v for k, v in {'name': name, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/ike-policies', params=params) + + async def get_ike_policy(self, id: int) -> Dict: + """Get a specific IKE policy by ID.""" + return self.client.get(f'{self.base_endpoint}/ike-policies', id) + + async def create_ike_policy( + self, + name: str, + version: int, + mode: str, + proposals: List[int], + preshared_key: Optional[str] = None, + description: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new IKE policy.""" + data = {'name': name, 'version': version, 'mode': mode, 'proposals': proposals, **kwargs} + if preshared_key: + data['preshared_key'] = preshared_key + if description: + data['description'] = description + return self.client.create(f'{self.base_endpoint}/ike-policies', data) + + async def update_ike_policy(self, id: int, **kwargs) -> Dict: + """Update an IKE policy.""" + return self.client.patch(f'{self.base_endpoint}/ike-policies', id, kwargs) + + async def delete_ike_policy(self, id: int) -> None: + """Delete an IKE policy.""" + self.client.delete(f'{self.base_endpoint}/ike-policies', id) + + # ==================== IPSec Proposals ==================== + + async def list_ipsec_proposals(self, name: Optional[str] = None, **kwargs) -> List[Dict]: + """List all IPSec proposals.""" + params = {k: v for k, v in {'name': name, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/ipsec-proposals', params=params) + + async def get_ipsec_proposal(self, id: int) -> Dict: + """Get a specific IPSec proposal by ID.""" + return self.client.get(f'{self.base_endpoint}/ipsec-proposals', id) + + async def create_ipsec_proposal( + self, + name: str, + encryption_algorithm: str, + authentication_algorithm: str, + sa_lifetime_seconds: Optional[int] = None, + sa_lifetime_data: Optional[int] = None, + description: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new IPSec proposal.""" + data = { + 'name': name, 'encryption_algorithm': encryption_algorithm, + 'authentication_algorithm': authentication_algorithm, **kwargs + } + for key, val in [ + ('sa_lifetime_seconds', sa_lifetime_seconds), + ('sa_lifetime_data', sa_lifetime_data), ('description', description) + ]: + if val is not None: + data[key] = val + return self.client.create(f'{self.base_endpoint}/ipsec-proposals', data) + + async def update_ipsec_proposal(self, id: int, **kwargs) -> Dict: + """Update an IPSec proposal.""" + return self.client.patch(f'{self.base_endpoint}/ipsec-proposals', id, kwargs) + + async def delete_ipsec_proposal(self, id: int) -> None: + """Delete an IPSec proposal.""" + self.client.delete(f'{self.base_endpoint}/ipsec-proposals', id) + + # ==================== IPSec Policies ==================== + + async def list_ipsec_policies(self, name: Optional[str] = None, **kwargs) -> List[Dict]: + """List all IPSec policies.""" + params = {k: v for k, v in {'name': name, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/ipsec-policies', params=params) + + async def get_ipsec_policy(self, id: int) -> Dict: + """Get a specific IPSec policy by ID.""" + return self.client.get(f'{self.base_endpoint}/ipsec-policies', id) + + async def create_ipsec_policy( + self, + name: str, + proposals: List[int], + pfs_group: Optional[int] = None, + description: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new IPSec policy.""" + data = {'name': name, 'proposals': proposals, **kwargs} + if pfs_group: + data['pfs_group'] = pfs_group + if description: + data['description'] = description + return self.client.create(f'{self.base_endpoint}/ipsec-policies', data) + + async def update_ipsec_policy(self, id: int, **kwargs) -> Dict: + """Update an IPSec policy.""" + return self.client.patch(f'{self.base_endpoint}/ipsec-policies', id, kwargs) + + async def delete_ipsec_policy(self, id: int) -> None: + """Delete an IPSec policy.""" + self.client.delete(f'{self.base_endpoint}/ipsec-policies', id) + + # ==================== IPSec Profiles ==================== + + async def list_ipsec_profiles(self, name: Optional[str] = None, **kwargs) -> List[Dict]: + """List all IPSec profiles.""" + params = {k: v for k, v in {'name': name, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/ipsec-profiles', params=params) + + async def get_ipsec_profile(self, id: int) -> Dict: + """Get a specific IPSec profile by ID.""" + return self.client.get(f'{self.base_endpoint}/ipsec-profiles', id) + + async def create_ipsec_profile( + self, + name: str, + mode: str, + ike_policy: int, + ipsec_policy: int, + description: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new IPSec profile.""" + data = {'name': name, 'mode': mode, 'ike_policy': ike_policy, 'ipsec_policy': ipsec_policy, **kwargs} + if description: + data['description'] = description + return self.client.create(f'{self.base_endpoint}/ipsec-profiles', data) + + async def update_ipsec_profile(self, id: int, **kwargs) -> Dict: + """Update an IPSec profile.""" + return self.client.patch(f'{self.base_endpoint}/ipsec-profiles', id, kwargs) + + async def delete_ipsec_profile(self, id: int) -> None: + """Delete an IPSec profile.""" + self.client.delete(f'{self.base_endpoint}/ipsec-profiles', id) + + # ==================== L2VPN ==================== + + async def list_l2vpns( + self, + name: Optional[str] = None, + slug: Optional[str] = None, + type: Optional[str] = None, + tenant_id: Optional[int] = None, + **kwargs + ) -> List[Dict]: + """List all L2VPNs with optional filtering.""" + params = {k: v for k, v in { + 'name': name, 'slug': slug, 'type': type, 'tenant_id': tenant_id, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/l2vpns', params=params) + + async def get_l2vpn(self, id: int) -> Dict: + """Get a specific L2VPN by ID.""" + return self.client.get(f'{self.base_endpoint}/l2vpns', id) + + async def create_l2vpn( + self, + name: str, + slug: str, + type: str, + identifier: Optional[int] = None, + tenant: Optional[int] = None, + description: Optional[str] = None, + import_targets: Optional[List[int]] = None, + export_targets: Optional[List[int]] = None, + **kwargs + ) -> Dict: + """Create a new L2VPN.""" + data = {'name': name, 'slug': slug, 'type': type, **kwargs} + for key, val in [ + ('identifier', identifier), ('tenant', tenant), ('description', description), + ('import_targets', import_targets), ('export_targets', export_targets) + ]: + if val is not None: + data[key] = val + return self.client.create(f'{self.base_endpoint}/l2vpns', data) + + async def update_l2vpn(self, id: int, **kwargs) -> Dict: + """Update an L2VPN.""" + return self.client.patch(f'{self.base_endpoint}/l2vpns', id, kwargs) + + async def delete_l2vpn(self, id: int) -> None: + """Delete an L2VPN.""" + self.client.delete(f'{self.base_endpoint}/l2vpns', id) + + # ==================== L2VPN Terminations ==================== + + async def list_l2vpn_terminations( + self, + l2vpn_id: Optional[int] = None, + **kwargs + ) -> List[Dict]: + """List all L2VPN terminations.""" + params = {k: v for k, v in {'l2vpn_id': l2vpn_id, **kwargs}.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/l2vpn-terminations', params=params) + + async def get_l2vpn_termination(self, id: int) -> Dict: + """Get a specific L2VPN termination by ID.""" + return self.client.get(f'{self.base_endpoint}/l2vpn-terminations', id) + + async def create_l2vpn_termination( + self, + l2vpn: int, + assigned_object_type: str, + assigned_object_id: int, + **kwargs + ) -> Dict: + """Create a new L2VPN termination.""" + data = { + 'l2vpn': l2vpn, 'assigned_object_type': assigned_object_type, + 'assigned_object_id': assigned_object_id, **kwargs + } + return self.client.create(f'{self.base_endpoint}/l2vpn-terminations', data) + + async def update_l2vpn_termination(self, id: int, **kwargs) -> Dict: + """Update an L2VPN termination.""" + return self.client.patch(f'{self.base_endpoint}/l2vpn-terminations', id, kwargs) + + async def delete_l2vpn_termination(self, id: int) -> None: + """Delete an L2VPN termination.""" + self.client.delete(f'{self.base_endpoint}/l2vpn-terminations', id) diff --git a/mcp-servers/netbox/mcp_server/tools/wireless.py b/mcp-servers/netbox/mcp_server/tools/wireless.py new file mode 100644 index 0000000..f88626b --- /dev/null +++ b/mcp-servers/netbox/mcp_server/tools/wireless.py @@ -0,0 +1,166 @@ +""" +Wireless tools for NetBox MCP Server. + +Covers: Wireless LANs, Wireless LAN Groups, and Wireless Links. +""" +import logging +from typing import List, Dict, Optional, Any +from ..netbox_client import NetBoxClient + +logger = logging.getLogger(__name__) + + +class WirelessTools: + """Tools for Wireless operations in NetBox""" + + def __init__(self, client: NetBoxClient): + self.client = client + self.base_endpoint = 'wireless' + + # ==================== Wireless LAN Groups ==================== + + async def list_wireless_lan_groups( + self, + name: Optional[str] = None, + slug: Optional[str] = None, + parent_id: Optional[int] = None, + **kwargs + ) -> List[Dict]: + """List all wireless LAN groups.""" + params = {k: v for k, v in { + 'name': name, 'slug': slug, 'parent_id': parent_id, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/wireless-lan-groups', params=params) + + async def get_wireless_lan_group(self, id: int) -> Dict: + """Get a specific wireless LAN group by ID.""" + return self.client.get(f'{self.base_endpoint}/wireless-lan-groups', id) + + async def create_wireless_lan_group( + self, + name: str, + slug: str, + parent: Optional[int] = None, + description: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new wireless LAN group.""" + data = {'name': name, 'slug': slug, **kwargs} + if parent: + data['parent'] = parent + if description: + data['description'] = description + return self.client.create(f'{self.base_endpoint}/wireless-lan-groups', data) + + async def update_wireless_lan_group(self, id: int, **kwargs) -> Dict: + """Update a wireless LAN group.""" + return self.client.patch(f'{self.base_endpoint}/wireless-lan-groups', id, kwargs) + + async def delete_wireless_lan_group(self, id: int) -> None: + """Delete a wireless LAN group.""" + self.client.delete(f'{self.base_endpoint}/wireless-lan-groups', id) + + # ==================== Wireless LANs ==================== + + async def list_wireless_lans( + self, + ssid: Optional[str] = None, + group_id: Optional[int] = None, + vlan_id: Optional[int] = None, + tenant_id: Optional[int] = None, + status: Optional[str] = None, + auth_type: Optional[str] = None, + **kwargs + ) -> List[Dict]: + """List all wireless LANs with optional filtering.""" + params = {k: v for k, v in { + 'ssid': ssid, 'group_id': group_id, 'vlan_id': vlan_id, + 'tenant_id': tenant_id, 'status': status, 'auth_type': auth_type, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/wireless-lans', params=params) + + async def get_wireless_lan(self, id: int) -> Dict: + """Get a specific wireless LAN by ID.""" + return self.client.get(f'{self.base_endpoint}/wireless-lans', id) + + async def create_wireless_lan( + self, + ssid: str, + status: str = 'active', + group: Optional[int] = None, + vlan: Optional[int] = None, + tenant: Optional[int] = None, + auth_type: Optional[str] = None, + auth_cipher: Optional[str] = None, + auth_psk: Optional[str] = None, + description: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new wireless LAN.""" + data = {'ssid': ssid, 'status': status, **kwargs} + for key, val in [ + ('group', group), ('vlan', vlan), ('tenant', tenant), + ('auth_type', auth_type), ('auth_cipher', auth_cipher), + ('auth_psk', auth_psk), ('description', description) + ]: + if val is not None: + data[key] = val + return self.client.create(f'{self.base_endpoint}/wireless-lans', data) + + async def update_wireless_lan(self, id: int, **kwargs) -> Dict: + """Update a wireless LAN.""" + return self.client.patch(f'{self.base_endpoint}/wireless-lans', id, kwargs) + + async def delete_wireless_lan(self, id: int) -> None: + """Delete a wireless LAN.""" + self.client.delete(f'{self.base_endpoint}/wireless-lans', id) + + # ==================== Wireless Links ==================== + + async def list_wireless_links( + self, + ssid: Optional[str] = None, + status: Optional[str] = None, + tenant_id: Optional[int] = None, + **kwargs + ) -> List[Dict]: + """List all wireless links with optional filtering.""" + params = {k: v for k, v in { + 'ssid': ssid, 'status': status, 'tenant_id': tenant_id, **kwargs + }.items() if v is not None} + return self.client.list(f'{self.base_endpoint}/wireless-links', params=params) + + async def get_wireless_link(self, id: int) -> Dict: + """Get a specific wireless link by ID.""" + return self.client.get(f'{self.base_endpoint}/wireless-links', id) + + async def create_wireless_link( + self, + interface_a: int, + interface_b: int, + ssid: Optional[str] = None, + status: str = 'connected', + tenant: Optional[int] = None, + auth_type: Optional[str] = None, + auth_cipher: Optional[str] = None, + auth_psk: Optional[str] = None, + description: Optional[str] = None, + **kwargs + ) -> Dict: + """Create a new wireless link.""" + data = {'interface_a': interface_a, 'interface_b': interface_b, 'status': status, **kwargs} + for key, val in [ + ('ssid', ssid), ('tenant', tenant), ('auth_type', auth_type), + ('auth_cipher', auth_cipher), ('auth_psk', auth_psk), ('description', description) + ]: + if val is not None: + data[key] = val + return self.client.create(f'{self.base_endpoint}/wireless-links', data) + + async def update_wireless_link(self, id: int, **kwargs) -> Dict: + """Update a wireless link.""" + return self.client.patch(f'{self.base_endpoint}/wireless-links', id, kwargs) + + async def delete_wireless_link(self, id: int) -> None: + """Delete a wireless link.""" + self.client.delete(f'{self.base_endpoint}/wireless-links', id) diff --git a/mcp-servers/netbox/requirements.txt b/mcp-servers/netbox/requirements.txt new file mode 100644 index 0000000..7c8c08a --- /dev/null +++ b/mcp-servers/netbox/requirements.txt @@ -0,0 +1,6 @@ +mcp>=0.9.0 # MCP SDK from Anthropic +python-dotenv>=1.0.0 # Environment variable loading +requests>=2.31.0 # HTTP client for NetBox API +pydantic>=2.5.0 # Data validation +pytest>=7.4.3 # Testing framework +pytest-asyncio>=0.23.0 # Async testing support diff --git a/mcp-servers/netbox/tests/__init__.py b/mcp-servers/netbox/tests/__init__.py new file mode 100644 index 0000000..5755d9b --- /dev/null +++ b/mcp-servers/netbox/tests/__init__.py @@ -0,0 +1 @@ +"""NetBox MCP Server tests.""" -- 2.49.1 From ba599e342ed77ed880caaabcd466a0b2c810d625 Mon Sep 17 00:00:00 2001 From: lmiranda Date: Tue, 9 Dec 2025 11:51:13 -0500 Subject: [PATCH 2/2] refactor: update repository URL and rebrand to Bandit Labs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update git remote to new Tailscale hostname - Replace old organization name (hhl-infra) with bandit - Replace old repository name (claude-code-hhl-toolkit) with support-claude-mktplace - Update all documentation references to use generic gitea.example.com - Rebrand from HyperHive Labs to Bandit Labs across all files - Rename workspace file to match new repository name πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../projman-test-marketplace/marketplace.json | 2 +- .../references/manifest-schema.md | 2 +- .../references/marketplace-guide.md | 6 +- CLAUDE.md | 2 +- cmdb-assistant/.claude-plugin/plugin.json | 65 +++++++ cmdb-assistant/.mcp.json | 9 + cmdb-assistant/README.md | 170 ++++++++++++++++++ cmdb-assistant/agents/cmdb-assistant.md | 78 ++++++++ cmdb-assistant/commands/cmdb-device.md | 52 ++++++ cmdb-assistant/commands/cmdb-ip.md | 53 ++++++ cmdb-assistant/commands/cmdb-search.md | 34 ++++ cmdb-assistant/commands/cmdb-site.md | 56 ++++++ create_labels.py | 10 +- docs/CREATE_LABELS_GUIDE.md | 20 +-- docs/LABEL_CREATION_COMPLETE.md | 20 +-- docs/LIVE_API_TEST_RESULTS.md | 32 ++-- docs/PROJMAN_TESTING_COMPLETE.md | 10 +- docs/STATUS_UPDATE_2025-11-21.md | 10 +- docs/TEST_01_PROJMAN.md | 12 +- docs/TEST_EXECUTION_REPORT.md | 4 +- docs/references/MCP-GITEA.md | 10 +- docs/references/MCP-WIKIJS.md | 10 +- docs/references/PLUGIN-PMO.md | 8 +- docs/references/PLUGIN-PROJMAN.md | 16 +- docs/references/PROJECT-SUMMARY.md | 10 +- mcp-servers/gitea/README.md | 16 +- mcp-servers/gitea/TESTING.md | 22 +-- mcp-servers/netbox/README.md | 2 +- mcp-servers/wikijs/README.md | 13 +- projman/.claude-plugin/plugin.json | 6 +- projman/CONFIGURATION.md | 18 +- projman/README.md | 12 +- projman/commands/labels-sync.md | 2 +- .../skills/label-taxonomy/labels-reference.md | 6 +- ... => support-claude-mktplace.code-workspace | 2 +- 35 files changed, 658 insertions(+), 142 deletions(-) create mode 100644 cmdb-assistant/.claude-plugin/plugin.json create mode 100644 cmdb-assistant/.mcp.json create mode 100644 cmdb-assistant/README.md create mode 100644 cmdb-assistant/agents/cmdb-assistant.md create mode 100644 cmdb-assistant/commands/cmdb-device.md create mode 100644 cmdb-assistant/commands/cmdb-ip.md create mode 100644 cmdb-assistant/commands/cmdb-search.md create mode 100644 cmdb-assistant/commands/cmdb-site.md rename claude-code-hhl-toolkit.code-workspace => support-claude-mktplace.code-workspace (98%) diff --git a/.claude-plugins/projman-test-marketplace/marketplace.json b/.claude-plugins/projman-test-marketplace/marketplace.json index 32bd619..4d8fffc 100644 --- a/.claude-plugins/projman-test-marketplace/marketplace.json +++ b/.claude-plugins/projman-test-marketplace/marketplace.json @@ -3,7 +3,7 @@ "version": "1.0.0", "displayName": "Projman Test Marketplace", "description": "Local marketplace for testing the Projman plugin", - "author": "Hyper Hive Labs", + "author": "Bandit Labs", "plugins": [ { "name": "projman", diff --git a/.claude/skills/claude-plugin-developer/references/manifest-schema.md b/.claude/skills/claude-plugin-developer/references/manifest-schema.md index 34dfb80..3d48783 100644 --- a/.claude/skills/claude-plugin-developer/references/manifest-schema.md +++ b/.claude/skills/claude-plugin-developer/references/manifest-schema.md @@ -98,7 +98,7 @@ Complete JSON schema reference for `.claude-plugin/plugin.json` files. "version": "2.1.0", "description": "Automated deployment tools for cloud platforms", "author": { - "name": "Hyper Hive Labs", + "name": "Bandit Labs", "email": "plugins@hyperhivelabs.com", "url": "https://hyperhivelabs.com" }, diff --git a/.claude/skills/claude-plugin-developer/references/marketplace-guide.md b/.claude/skills/claude-plugin-developer/references/marketplace-guide.md index 6b7e372..8ceebbc 100644 --- a/.claude/skills/claude-plugin-developer/references/marketplace-guide.md +++ b/.claude/skills/claude-plugin-developer/references/marketplace-guide.md @@ -59,7 +59,7 @@ claude plugin marketplace add https://plugins.example.com ### marketplace.json Structure ```json { - "name": "Hyper Hive Labs Plugins", + "name": "Bandit Labs Plugins", "description": "Restaurant automation and AI tools", "version": "1.0.0", "plugins": [ @@ -67,7 +67,7 @@ claude plugin marketplace add https://plugins.example.com "name": "restaurant-analytics", "version": "2.1.0", "description": "Analytics dashboard for restaurant data", - "author": "Hyper Hive Labs", + "author": "Bandit Labs", "path": "plugins/restaurant-analytics", "tags": ["analytics", "restaurant", "reporting"], "requirements": { @@ -79,7 +79,7 @@ claude plugin marketplace add https://plugins.example.com "name": "order-automation", "version": "1.5.2", "description": "Automated order processing system", - "author": "Hyper Hive Labs", + "author": "Bandit Labs", "path": "plugins/order-automation", "featured": true, "beta": false diff --git a/CLAUDE.md b/CLAUDE.md index bc946fa..a10bab0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -163,7 +163,7 @@ See [docs/reference-material/projman-implementation-plan.md](docs/reference-mate ⚠️ **See `docs/CORRECT-ARCHITECTURE.md` for the authoritative structure reference** ``` -hhl-infra/claude-code-hhl-toolkit/ +bandit/support-claude-mktplace/ β”œβ”€β”€ .claude-plugin/ β”‚ └── marketplace.json β”œβ”€β”€ mcp-servers/ # ← SHARED BY BOTH PLUGINS diff --git a/cmdb-assistant/.claude-plugin/plugin.json b/cmdb-assistant/.claude-plugin/plugin.json new file mode 100644 index 0000000..865f796 --- /dev/null +++ b/cmdb-assistant/.claude-plugin/plugin.json @@ -0,0 +1,65 @@ +{ + "name": "cmdb-assistant", + "version": "1.0.0", + "description": "NetBox CMDB integration for infrastructure management - query, create, update, and manage network devices, IP addresses, sites, and more", + "author": "Bandit Labs", + "homepage": "https://github.com/bandit-labs/cmdb-assistant", + "license": "MIT", + "keywords": [ + "netbox", + "cmdb", + "infrastructure", + "network", + "ipam", + "dcim" + ], + "commands": { + "cmdb-search": { + "description": "Search NetBox for devices, IPs, sites, or any CMDB object", + "file": "commands/cmdb-search.md" + }, + "cmdb-device": { + "description": "Manage network devices (create, view, update, delete)", + "file": "commands/cmdb-device.md" + }, + "cmdb-ip": { + "description": "Manage IP addresses and prefixes", + "file": "commands/cmdb-ip.md" + }, + "cmdb-site": { + "description": "Manage sites and locations", + "file": "commands/cmdb-site.md" + } + }, + "agents": { + "cmdb-assistant": { + "description": "Infrastructure management assistant for NetBox CMDB operations", + "file": "agents/cmdb-assistant.md" + } + }, + "mcpServers": { + "netbox": { + "description": "NetBox API integration via MCP", + "configFile": ".mcp.json" + } + }, + "configuration": { + "required": [ + { + "name": "NETBOX_URL", + "description": "NetBox instance URL (e.g., https://netbox.example.com)" + }, + { + "name": "NETBOX_TOKEN", + "description": "NetBox API token for authentication" + } + ], + "optional": [ + { + "name": "NETBOX_VERIFY_SSL", + "description": "Verify SSL certificates (default: true)", + "default": "true" + } + ] + } +} diff --git a/cmdb-assistant/.mcp.json b/cmdb-assistant/.mcp.json new file mode 100644 index 0000000..41c6e68 --- /dev/null +++ b/cmdb-assistant/.mcp.json @@ -0,0 +1,9 @@ +{ + "mcpServers": { + "netbox": { + "command": "${CLAUDE_PLUGIN_ROOT}/../mcp-servers/netbox/.venv/bin/python", + "args": ["-m", "mcp_server.server"], + "cwd": "${CLAUDE_PLUGIN_ROOT}/../mcp-servers/netbox" + } + } +} diff --git a/cmdb-assistant/README.md b/cmdb-assistant/README.md new file mode 100644 index 0000000..40dc7d8 --- /dev/null +++ b/cmdb-assistant/README.md @@ -0,0 +1,170 @@ +# CMDB Assistant + +A Claude Code plugin for NetBox CMDB integration - query, create, update, and manage your network infrastructure directly from Claude Code. + +## Features + +- **Full CRUD Operations**: Create, read, update, and delete across all NetBox modules +- **Smart Search**: Find devices, IPs, sites, and more with natural language queries +- **IP Management**: Allocate IPs, manage prefixes, track VLANs +- **Infrastructure Documentation**: Document servers, network devices, and connections +- **Audit Trail**: Review changes and maintain infrastructure history + +## Installation + +### Prerequisites + +1. A running NetBox instance (v4.x recommended) +2. NetBox API token with appropriate permissions +3. The NetBox MCP server configured (see below) + +### Configure NetBox Credentials + +Create the configuration file: + +```bash +mkdir -p ~/.config/claude +cat > ~/.config/claude/netbox.env << 'EOF' +NETBOX_API_URL=https://your-netbox-instance/api +NETBOX_API_TOKEN=your-api-token-here +NETBOX_VERIFY_SSL=true +NETBOX_TIMEOUT=30 +EOF +``` + +### Install the Plugin + +Add to your Claude Code plugins or marketplace configuration. + +## Commands + +| Command | Description | +|---------|-------------| +| `/cmdb-search ` | Search for devices, IPs, sites, or any CMDB object | +| `/cmdb-device ` | Manage network devices (list, create, update, delete) | +| `/cmdb-ip ` | Manage IP addresses and prefixes | +| `/cmdb-site ` | Manage sites and locations | + +## Agent + +The **cmdb-assistant** agent provides conversational infrastructure management: + +``` +@cmdb-assistant Show me all devices at the headquarters site +@cmdb-assistant Allocate the next available IP from 10.0.1.0/24 for the new web server +@cmdb-assistant What changes were made to the network today? +``` + +## Usage Examples + +### Search for Infrastructure + +``` +/cmdb-search router +/cmdb-search 10.0.1.0/24 +/cmdb-search datacenter +``` + +### Device Management + +``` +/cmdb-device list +/cmdb-device show core-router-01 +/cmdb-device create web-server-03 +/cmdb-device at headquarters +``` + +### IP Address Management + +``` +/cmdb-ip prefixes +/cmdb-ip available in 10.0.1.0/24 +/cmdb-ip allocate from 10.0.1.0/24 +``` + +### Site Management + +``` +/cmdb-site list +/cmdb-site show headquarters +/cmdb-site racks at datacenter-east +``` + +## NetBox Coverage + +This plugin provides access to the full NetBox API: + +- **DCIM**: Sites, Locations, Racks, Devices, Interfaces, Cables, Power +- **IPAM**: IP Addresses, Prefixes, VLANs, VRFs, ASNs, Services +- **Circuits**: Providers, Circuits, Terminations +- **Virtualization**: Clusters, Virtual Machines, VM Interfaces +- **Tenancy**: Tenants, Contacts +- **VPN**: Tunnels, L2VPNs, IKE/IPSec Policies +- **Wireless**: WLANs, Wireless Links +- **Extras**: Tags, Custom Fields, Journal Entries, Audit Log + +## Architecture + +``` +cmdb-assistant/ +β”œβ”€β”€ .claude-plugin/ +β”‚ └── plugin.json # Plugin manifest +β”œβ”€β”€ .mcp.json # MCP server configuration +β”œβ”€β”€ commands/ +β”‚ β”œβ”€β”€ cmdb-search.md # Search command +β”‚ β”œβ”€β”€ cmdb-device.md # Device management +β”‚ β”œβ”€β”€ cmdb-ip.md # IP management +β”‚ └── cmdb-site.md # Site management +β”œβ”€β”€ agents/ +β”‚ └── cmdb-assistant.md # Main assistant agent +└── README.md +``` + +The plugin uses the shared NetBox MCP server at `../mcp-servers/netbox/`. + +## Configuration + +### Required Environment Variables + +| Variable | Description | +|----------|-------------| +| `NETBOX_API_URL` | Full URL to NetBox API (e.g., `https://netbox.example.com/api`) | +| `NETBOX_API_TOKEN` | API authentication token | + +### Optional Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `NETBOX_VERIFY_SSL` | `true` | Verify SSL certificates | +| `NETBOX_TIMEOUT` | `30` | Request timeout in seconds | + +## Getting a NetBox API Token + +1. Log into your NetBox instance +2. Navigate to your profile (top-right menu) +3. Go to "API Tokens" +4. Click "Add a token" +5. Set appropriate permissions (read-only or read-write) +6. Copy the generated token + +## Troubleshooting + +### Connection Issues + +- Verify `NETBOX_API_URL` is correct and accessible +- Check firewall rules allow access to NetBox +- For self-signed certificates, set `NETBOX_VERIFY_SSL=false` + +### Authentication Errors + +- Ensure API token is valid and not expired +- Check token has required permissions for the operation + +### Timeout Errors + +- Increase `NETBOX_TIMEOUT` for slow connections +- Check network latency to NetBox instance + +## License + +MIT License - Part of the Bandit Labs plugin collection. diff --git a/cmdb-assistant/agents/cmdb-assistant.md b/cmdb-assistant/agents/cmdb-assistant.md new file mode 100644 index 0000000..b216e15 --- /dev/null +++ b/cmdb-assistant/agents/cmdb-assistant.md @@ -0,0 +1,78 @@ +# CMDB Assistant Agent + +You are an infrastructure management assistant specialized in NetBox CMDB operations. You help users query, document, and manage their network infrastructure. + +## Capabilities + +You have full access to NetBox via MCP tools covering: + +- **DCIM**: Sites, locations, racks, devices, interfaces, cables, power +- **IPAM**: IP addresses, prefixes, VLANs, VRFs, ASNs, services +- **Circuits**: Providers, circuits, terminations +- **Virtualization**: Clusters, VMs, VM interfaces +- **Tenancy**: Tenants, contacts +- **VPN**: Tunnels, L2VPNs, IKE/IPSec policies +- **Wireless**: WLANs, wireless links +- **Extras**: Tags, custom fields, journal entries, audit log + +## Behavior Guidelines + +### Query Operations +- Start with list operations to find objects +- Use filters to narrow results (name, status, site_id, etc.) +- Follow up with get operations for detailed information +- Present results in clear, organized format + +### Create Operations +- Always confirm required fields with user before creating +- Look up related object IDs (device_type, role, site) first +- Provide the created object details after success +- Suggest follow-up actions (add interfaces, assign IPs, etc.) + +### Update Operations +- Show current values before updating +- Confirm changes with user +- Report what was changed after success + +### Delete Operations +- ALWAYS ask for explicit confirmation before deleting +- Show what will be deleted +- Warn about dependent objects that may be affected + +## Common Workflows + +### Document a New Server +1. Create device with `dcim_create_device` +2. Add interfaces with `dcim_create_interface` +3. Assign IPs with `ipam_create_ip_address` +4. Add journal entry with `extras_create_journal_entry` + +### Allocate IP Space +1. Find available prefixes with `ipam_list_available_prefixes` +2. Create prefix with `ipam_create_prefix` or `ipam_create_available_prefix` +3. Allocate IPs with `ipam_create_available_ip` + +### Audit Infrastructure +1. List recent changes with `extras_list_object_changes` +2. Review devices by site with `dcim_list_devices` +3. Check IP utilization with prefix operations + +### Cable Management +1. List interfaces with `dcim_list_interfaces` +2. Create cable with `dcim_create_cable` +3. Verify connectivity + +## Response Format + +When presenting data: +- Use tables for lists +- Highlight key fields (name, status, IPs) +- Include IDs for reference in follow-up operations +- Suggest next steps when appropriate + +## Error Handling + +- If an operation fails, explain why clearly +- Suggest corrective actions +- For permission errors, note what access is needed +- For validation errors, explain required fields/formats diff --git a/cmdb-assistant/commands/cmdb-device.md b/cmdb-assistant/commands/cmdb-device.md new file mode 100644 index 0000000..13e6a2d --- /dev/null +++ b/cmdb-assistant/commands/cmdb-device.md @@ -0,0 +1,52 @@ +# CMDB Device Management + +Manage network devices in NetBox - create, view, update, or delete. + +## Usage + +``` +/cmdb-device [options] +``` + +## Instructions + +You are a device management assistant with full CRUD access to NetBox devices. + +### Actions + +**List/View:** +- `list` or `show all` - List all devices using `dcim_list_devices` +- `show ` - Get device details using `dcim_list_devices` with name filter, then `dcim_get_device` +- `at ` - List devices at a specific site + +**Create:** +- `create ` - Create a new device +- Required: name, device_type, role, site +- Use `dcim_list_device_types`, `dcim_list_device_roles`, `dcim_list_sites` to help user find IDs +- Then use `dcim_create_device` + +**Update:** +- `update ` - Update device properties +- First get the device ID, then use `dcim_update_device` + +**Delete:** +- `delete ` - Delete a device (ask for confirmation first) +- Use `dcim_delete_device` + +### Related Operations + +After creating a device, offer to: +- Add interfaces with `dcim_create_interface` +- Assign IP addresses with `ipam_create_ip_address` +- Add to a rack with `dcim_update_device` + +## Examples + +- `/cmdb-device list` - Show all devices +- `/cmdb-device show core-router-01` - Get details for specific device +- `/cmdb-device create web-server-03` - Create a new device +- `/cmdb-device at headquarters` - List devices at headquarters site + +## User Request + +$ARGUMENTS diff --git a/cmdb-assistant/commands/cmdb-ip.md b/cmdb-assistant/commands/cmdb-ip.md new file mode 100644 index 0000000..fc01a1e --- /dev/null +++ b/cmdb-assistant/commands/cmdb-ip.md @@ -0,0 +1,53 @@ +# CMDB IP Management + +Manage IP addresses and prefixes in NetBox. + +## Usage + +``` +/cmdb-ip [options] +``` + +## Instructions + +You are an IP address management (IPAM) assistant with access to NetBox. + +### Actions + +**Prefixes:** +- `prefixes` - List all prefixes using `ipam_list_prefixes` +- `prefix ` - Get prefix details or find prefix containing address +- `available in ` - Show available IPs in a prefix using `ipam_list_available_ips` +- `create prefix ` - Create new prefix using `ipam_create_prefix` + +**IP Addresses:** +- `list` - List all IP addresses using `ipam_list_ip_addresses` +- `show
` - Get IP details +- `allocate from ` - Auto-allocate next available IP using `ipam_create_available_ip` +- `create
` - Create specific IP using `ipam_create_ip_address` +- `assign to ` - Assign IP to device interface + +**VLANs:** +- `vlans` - List VLANs using `ipam_list_vlans` +- `vlan ` - Get VLAN details + +**VRFs:** +- `vrfs` - List VRFs using `ipam_list_vrfs` + +### Workflow Examples + +**Allocate IP to new server:** +1. Find available IPs in target prefix +2. Create the IP address +3. Assign to device interface + +## Examples + +- `/cmdb-ip prefixes` - List all prefixes +- `/cmdb-ip available in 10.0.1.0/24` - Show available IPs +- `/cmdb-ip allocate from 10.0.1.0/24` - Get next available IP +- `/cmdb-ip assign 10.0.1.50/24 to web-server-01 eth0` - Assign IP to interface + +## User Request + +$ARGUMENTS diff --git a/cmdb-assistant/commands/cmdb-search.md b/cmdb-assistant/commands/cmdb-search.md new file mode 100644 index 0000000..7c4a5f4 --- /dev/null +++ b/cmdb-assistant/commands/cmdb-search.md @@ -0,0 +1,34 @@ +# CMDB Search + +Search NetBox for devices, IPs, sites, or any CMDB object. + +## Usage + +``` +/cmdb-search +``` + +## Instructions + +You are a CMDB search assistant with access to NetBox via MCP tools. + +When the user provides a search query, determine the best approach: + +1. **Device search**: Use `dcim_list_devices` with name filter +2. **IP search**: Use `ipam_list_ip_addresses` with address filter +3. **Site search**: Use `dcim_list_sites` with name filter +4. **Prefix search**: Use `ipam_list_prefixes` with prefix or within filter +5. **VLAN search**: Use `ipam_list_vlans` with vid or name filter +6. **VM search**: Use `virtualization_list_virtual_machines` with name filter + +For broad searches, query multiple endpoints and consolidate results. + +## Examples + +- `/cmdb-search router` - Find all devices with "router" in the name +- `/cmdb-search 10.0.1.0/24` - Find prefix and IPs within it +- `/cmdb-search datacenter` - Find sites matching "datacenter" + +## User Query + +$ARGUMENTS diff --git a/cmdb-assistant/commands/cmdb-site.md b/cmdb-assistant/commands/cmdb-site.md new file mode 100644 index 0000000..d8421de --- /dev/null +++ b/cmdb-assistant/commands/cmdb-site.md @@ -0,0 +1,56 @@ +# CMDB Site Management + +Manage sites and locations in NetBox. + +## Usage + +``` +/cmdb-site [options] +``` + +## Instructions + +You are a site/location management assistant with access to NetBox. + +### Actions + +**Sites:** +- `list` - List all sites using `dcim_list_sites` +- `show ` - Get site details using `dcim_get_site` +- `create ` - Create new site using `dcim_create_site` +- `update ` - Update site using `dcim_update_site` +- `delete ` - Delete site (with confirmation) + +**Locations (within sites):** +- `locations at ` - List locations using `dcim_list_locations` +- `create location at ` - Create location using `dcim_create_location` + +**Racks:** +- `racks at ` - List racks using `dcim_list_racks` +- `create rack at ` - Create rack using `dcim_create_rack` + +**Regions:** +- `regions` - List regions using `dcim_list_regions` +- `create region ` - Create region using `dcim_create_region` + +### Site Properties + +When creating/updating sites: +- name (required) +- slug (required, auto-generated if not provided) +- status: active, planned, staging, decommissioning, retired +- region: parent region ID +- facility: datacenter/building name +- physical_address, shipping_address +- time_zone + +## Examples + +- `/cmdb-site list` - Show all sites +- `/cmdb-site show headquarters` - Get HQ site details +- `/cmdb-site create branch-office-nyc` - Create new site +- `/cmdb-site racks at headquarters` - List racks at HQ + +## User Request + +$ARGUMENTS diff --git a/create_labels.py b/create_labels.py index f21eae2..be0f035 100644 --- a/create_labels.py +++ b/create_labels.py @@ -1,15 +1,15 @@ #!/usr/bin/env python3 """ -Batch create Gitea labels via API for hhl-infra organization +Batch create Gitea labels via API for bandit organization Creates 28 organization labels + 16 repository labels = 44 total """ import requests import sys -GITEA_URL = "https://gitea.hotserv.cloud" +GITEA_URL = "https://gitea.example.com" TOKEN = "ae72c63cd7de02e40bd16f66d1e98059c187759b" -ORG = "hhl-infra" -REPO = "claude-code-hhl-toolkit" +ORG = "bandit" +REPO = "support-claude-mktplace" headers = {"Authorization": f"token {TOKEN}", "Content-Type": "application/json"} @@ -196,7 +196,7 @@ def verify_labels(): def main(): print(f"\n{'#'*60}") print("# Gitea Label Creation Script") - print("# Creating 44-label taxonomy for hhl-infra organization") + print("# Creating 44-label taxonomy for bandit organization") print(f"{'#'*60}") # Create organization labels diff --git a/docs/CREATE_LABELS_GUIDE.md b/docs/CREATE_LABELS_GUIDE.md index 0aba22b..0b0d7c8 100644 --- a/docs/CREATE_LABELS_GUIDE.md +++ b/docs/CREATE_LABELS_GUIDE.md @@ -1,7 +1,7 @@ # Quick Guide: Creating Label Taxonomy in Gitea **Estimated Time:** 15-20 minutes -**Required:** Admin access to hhl-infra organization in Gitea +**Required:** Admin access to bandit organization in Gitea ## Why This Is Needed @@ -16,9 +16,9 @@ The Projman plugin depends on a 44-label taxonomy system for: ## Step 1: Create Organization Labels (28 labels) -**Navigate to:** https://gitea.hotserv.cloud/org/hhl-infra/settings/labels +**Navigate to:** https://gitea.example.com/org/bandit/settings/labels -These labels will be available to ALL repositories in hhl-infra organization. +These labels will be available to ALL repositories in bandit organization. ### Agent (2 labels) | Name | Color | Description | @@ -79,9 +79,9 @@ These labels will be available to ALL repositories in hhl-infra organization. ## Step 2: Create Repository Labels (16 labels) -**Navigate to:** https://gitea.hotserv.cloud/hhl-infra/claude-code-hhl-toolkit/labels +**Navigate to:** https://gitea.example.com/bandit/support-claude-mktplace/labels -These labels are specific to the claude-code-hhl-toolkit repository. +These labels are specific to the support-claude-mktplace repository. ### Component (9 labels) | Name | Color | Description | @@ -115,11 +115,11 @@ After creating all labels, verify: ```bash # Count organization labels -curl -s "https://gitea.hotserv.cloud/api/v1/orgs/hhl-infra/labels" \ +curl -s "https://gitea.example.com/api/v1/orgs/bandit/labels" \ -H "Authorization: token YOUR_TOKEN" | python3 -c "import sys, json; print(len(json.load(sys.stdin)), 'org labels')" # Count repository labels -curl -s "https://gitea.hotserv.cloud/api/v1/repos/hhl-infra/claude-code-hhl-toolkit/labels" \ +curl -s "https://gitea.example.com/api/v1/repos/bandit/support-claude-mktplace/labels" \ -H "Authorization: token YOUR_TOKEN" | python3 -c "import sys, json; print(len(json.load(sys.stdin)), 'repo labels')" ``` @@ -163,10 +163,10 @@ Batch create Gitea labels via API """ import requests -GITEA_URL = "https://gitea.hotserv.cloud" +GITEA_URL = "https://gitea.example.com" TOKEN = "ae72c63cd7de02e40bd16f66d1e98059c187759b" -ORG = "hhl-infra" -REPO = "claude-code-hhl-toolkit" +ORG = "bandit" +REPO = "support-claude-mktplace" headers = {"Authorization": f"token {TOKEN}"} diff --git a/docs/LABEL_CREATION_COMPLETE.md b/docs/LABEL_CREATION_COMPLETE.md index 9328849..f90fe2a 100644 --- a/docs/LABEL_CREATION_COMPLETE.md +++ b/docs/LABEL_CREATION_COMPLETE.md @@ -5,10 +5,10 @@ ## Summary -Successfully created **43 labels** in the hhl-infra organization and claude-code-hhl-toolkit repository: +Successfully created **43 labels** in the bandit organization and support-claude-mktplace repository: -- βœ… **27 Organization Labels** (available to all hhl-infra repositories) -- βœ… **16 Repository Labels** (specific to claude-code-hhl-toolkit) +- βœ… **27 Organization Labels** (available to all bandit repositories) +- βœ… **16 Repository Labels** (specific to support-claude-mktplace) - βœ… **Total: 43 Labels** (100% complete) ## Label Breakdown @@ -82,12 +82,12 @@ Successfully created **43 labels** in the hhl-infra organization and claude-code ```bash # Organization labels -$ curl -s "https://gitea.hotserv.cloud/api/v1/orgs/hhl-infra/labels" \ +$ curl -s "https://hotserv.tailc9b278.ts.net/api/v1/orgs/bandit/labels" \ -H "Authorization: token ***" | jq 'length' 27 # Repository labels (shows repo-specific only) -$ curl -s "https://gitea.hotserv.cloud/api/v1/repos/hhl-infra/claude-code-hhl-toolkit/labels" \ +$ curl -s "https://hotserv.tailc9b278.ts.net/api/v1/repos/bandit/support-claude-mktplace/labels" \ -H "Authorization: token ***" | jq 'length' 16 ``` @@ -98,8 +98,8 @@ $ curl -s "https://gitea.hotserv.cloud/api/v1/repos/hhl-infra/claude-code-hhl-to The Projman plugin's MCP server fetches labels from **both endpoints**: -1. **Organization Labels:** `GET /api/v1/orgs/hhl-infra/labels` β†’ 27 labels -2. **Repository Labels:** `GET /api/v1/repos/hhl-infra/claude-code-hhl-toolkit/labels` β†’ 16 labels +1. **Organization Labels:** `GET /api/v1/orgs/bandit/labels` β†’ 27 labels +2. **Repository Labels:** `GET /api/v1/repos/bandit/support-claude-mktplace/labels` β†’ 16 labels 3. **Total Available:** 43 labels for issue tagging See `mcp-servers/gitea/mcp_server/tools/labels.py:29` for implementation. @@ -133,9 +133,9 @@ Now that all labels are created: ## Gitea Configuration -**Organization:** hhl-infra -**Repository:** claude-code-hhl-toolkit -**API URL:** https://gitea.hotserv.cloud/api/v1 +**Organization:** bandit +**Repository:** support-claude-mktplace +**API URL:** https://hotserv.tailc9b278.ts.net/api/v1 **Auth:** Token-based (configured in ~/.config/claude/gitea.env) ## Success Metrics diff --git a/docs/LIVE_API_TEST_RESULTS.md b/docs/LIVE_API_TEST_RESULTS.md index 1456f48..4aa7f07 100644 --- a/docs/LIVE_API_TEST_RESULTS.md +++ b/docs/LIVE_API_TEST_RESULTS.md @@ -13,7 +13,7 @@ Successfully connected to both Gitea and Wiki.js instances running on hotport. A ⚠️ **CRITICAL FINDING: Repository has NO LABELS** -The `claude-code-hhl-toolkit` repository currently has **0 labels** defined. The plugin depends on a 44-label taxonomy system. Labels must be created before full plugin functionality can be tested. +The `support-claude-mktplace` repository currently has **0 labels** defined. The plugin depends on a 44-label taxonomy system. Labels must be created before full plugin functionality can be tested. ## Test Results @@ -21,10 +21,10 @@ The `claude-code-hhl-toolkit` repository currently has **0 labels** defined. The **Configuration:** ``` -URL: https://gitea.hotserv.cloud/api/v1 +URL: https://gitea.example.com/api/v1 Token: ae72c63cd7de02e40bd16f66d1e98059c187759b -Owner: hhl-infra (organization) -Repo: claude-code-hhl-toolkit +Owner: bandit (organization) +Repo: support-claude-mktplace ``` **Authentication Test:** @@ -37,8 +37,8 @@ Repo: claude-code-hhl-toolkit **Repository Access:** ``` -βœ… Found 4 repositories in hhl-infra organization: - - claude-code-hhl-toolkit ← Our test repo +βœ… Found 4 repositories in bandit organization: + - support-claude-mktplace ← Our test repo - serv-hotport-apps - serv-hhl-home-apps - serv-hhl @@ -46,7 +46,7 @@ Repo: claude-code-hhl-toolkit **Issue Fetching:** ``` -βœ… Successfully fetched 2 issues from claude-code-hhl-toolkit: +βœ… Successfully fetched 2 issues from support-claude-mktplace: - Open: 0 - Closed: 2 @@ -77,7 +77,7 @@ Label categories expected but missing: URL: http://localhost:7851/graphql Token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9... (JWT) Base Path: /hyper-hive-labs -Project: projects/claude-code-hhl-toolkit +Project: projects/support-claude-mktplace ``` **Connection Test:** @@ -141,16 +141,16 @@ Tech/Redis, Tech/Vue, Tech/FastAPI ``` **How to create:** -1. Navigate to: https://gitea.hotserv.cloud/org/hhl-infra/settings/labels +1. Navigate to: https://gitea.example.com/org/bandit/settings/labels 2. Create organization labels (available to all repos) -3. Navigate to: https://gitea.hotserv.cloud/hhl-infra/claude-code-hhl-toolkit/labels +3. Navigate to: https://gitea.example.com/bandit/support-claude-mktplace/labels 4. Create repository-specific labels **Option 2: Import from Existing Repo** If labels exist in another repository (e.g., CuisineFlow): 1. Export labels from existing repo -2. Import to claude-code-hhl-toolkit +2. Import to support-claude-mktplace 3. Run `/labels-sync` to update plugin **Option 3: Create Programmatically** @@ -173,8 +173,8 @@ GITEA_OWNER=claude # Wrong - user instead of org **After (Correct):** ```bash -GITEA_API_URL=https://gitea.hotserv.cloud/api/v1 # Public URL -GITEA_OWNER=hhl-infra # Correct organization +GITEA_API_URL=https://gitea.example.com/api/v1 # Public URL +GITEA_OWNER=bandit # Correct organization GITEA_API_TOKEN=ae72c63cd7de02e40bd16f66d1e98059c187759b # New token with access ``` @@ -188,8 +188,8 @@ WIKIJS_BASE_PATH=/hyper-hive-labs **File: `.env` (in project root)** ```bash -GITEA_REPO=claude-code-hhl-toolkit # βœ… Correct -WIKIJS_PROJECT=projects/claude-code-hhl-toolkit # βœ… Correct +GITEA_REPO=support-claude-mktplace # βœ… Correct +WIKIJS_PROJECT=projects/support-claude-mktplace # βœ… Correct ``` ## What Works Right Now @@ -239,7 +239,7 @@ WIKIJS_PROJECT=projects/claude-code-hhl-toolkit # βœ… Correct | Test Category | Status | Details | |---------------|--------|---------| | Gitea Authentication | βœ… PASS | Authenticated as lmiranda (admin) | -| Gitea Repository Access | βœ… PASS | Access to 4 repos in hhl-infra | +| Gitea Repository Access | βœ… PASS | Access to 4 repos in bandit | | Gitea Issue Fetching | βœ… PASS | Fetched 2 issues successfully | | Gitea Label Fetching | ⚠️ PASS | API works, but 0 labels found | | WikiJS Authentication | βœ… PASS | JWT token valid | diff --git a/docs/PROJMAN_TESTING_COMPLETE.md b/docs/PROJMAN_TESTING_COMPLETE.md index 4653c13..f4d25f3 100644 --- a/docs/PROJMAN_TESTING_COMPLETE.md +++ b/docs/PROJMAN_TESTING_COMPLETE.md @@ -22,12 +22,12 @@ Successfully completed comprehensive testing of the Projman plugin. All core fea - Network: Tailscale VPN (100.124.47.46) **Services:** -- Gitea: https://gitea.hotserv.cloud (online, responsive) +- Gitea: https://gitea.example.com (online, responsive) - Wiki.js: http://localhost:7851/graphql (online, responsive) **Repository:** -- Organization: hhl-infra -- Repository: claude-code-hhl-toolkit +- Organization: bandit +- Repository: support-claude-mktplace - Branch: feat/projman ## Tests Performed @@ -131,14 +131,14 @@ Last Synced: 2025-11-21 - Labels: 4 labels (Type/Feature, Priority/Medium, Component/Testing, Tech/Python) - Method: Direct curl with label IDs - Result: βœ… PASS -- URL: https://gitea.hotserv.cloud/hhl-infra/claude-code-hhl-toolkit/issues/4 +- URL: https://gitea.example.com/bandit/support-claude-mktplace/issues/4 **Issue #5:** Automated test via MCP server (with label resolution fix) - Title: "[TEST] Add Comprehensive Testing for Projman MCP Servers" - Labels: 11 labels (all automatically resolved from names to IDs) - Method: MCP server with automatic label nameβ†’ID resolution - Result: βœ… PASS -- URL: https://gitea.hotserv.cloud/hhl-infra/claude-code-hhl-toolkit/issues/5 +- URL: https://gitea.example.com/bandit/support-claude-mktplace/issues/5 **Conclusion:** Issue creation with automatic label resolution working flawlessly. diff --git a/docs/STATUS_UPDATE_2025-11-21.md b/docs/STATUS_UPDATE_2025-11-21.md index b66ad5e..dd32394 100644 --- a/docs/STATUS_UPDATE_2025-11-21.md +++ b/docs/STATUS_UPDATE_2025-11-21.md @@ -11,8 +11,8 @@ Successfully completed label creation for the Projman plugin! All 43 labels have ## What Was Accomplished ### 1. Label Creation βœ… -- **Created 27 organization labels** in hhl-infra organization -- **Created 16 repository labels** in claude-code-hhl-toolkit repository +- **Created 27 organization labels** in bandit organization +- **Created 16 repository labels** in support-claude-mktplace repository - **Total: 43 labels** (corrected from initial documentation of 44) - All labels created programmatically via Gitea API @@ -87,9 +87,9 @@ All suggestions are accurate and appropriate! πŸŽ‰ ## Configuration Details **Gitea Configuration:** -- API URL: `https://gitea.hotserv.cloud/api/v1` -- Organization: `hhl-infra` -- Repository: `claude-code-hhl-toolkit` +- API URL: `https://gitea.example.com/api/v1` +- Organization: `bandit` +- Repository: `support-claude-mktplace` - Token: Configured in `~/.config/claude/gitea.env` **MCP Server:** diff --git a/docs/TEST_01_PROJMAN.md b/docs/TEST_01_PROJMAN.md index 862fba7..2dc384c 100644 --- a/docs/TEST_01_PROJMAN.md +++ b/docs/TEST_01_PROJMAN.md @@ -46,8 +46,8 @@ This document outlines the testing strategy for the Projman plugin, which has co βœ… **Project Configuration:** - `.env` - Project-specific settings (NOT committed) ```bash - GITEA_REPO=claude-code-hhl-toolkit - WIKIJS_PROJECT=projects/claude-code-hhl-toolkit + GITEA_REPO=support-claude-mktplace + WIKIJS_PROJECT=projects/support-claude-mktplace ``` βœ… **Local Test Marketplace:** @@ -130,8 +130,8 @@ ls -la ~/.config/claude/*.env ```bash cat .env # Should show: -# GITEA_REPO=claude-code-hhl-toolkit -# WIKIJS_PROJECT=projects/claude-code-hhl-toolkit +# GITEA_REPO=support-claude-mktplace +# WIKIJS_PROJECT=projects/support-claude-mktplace ``` **Verify .env is ignored:** @@ -355,7 +355,7 @@ Implement the first task (e.g., add command examples to README). - Suggests appropriate tags 4. Saves to Wiki.js: - Uses `create_lesson` MCP tool - - Creates in `/projects/claude-code-hhl-toolkit/lessons-learned/sprints/` + - Creates in `/projects/support-claude-mktplace/lessons-learned/sprints/` 5. Offers git operations: - Commit changes - Merge branches @@ -571,7 +571,7 @@ These are expected at this stage and will be addressed in Phase 4 (Lessons Learn 3. **Prepare for Phase 5: Testing & Validation** - Write integration tests - - Test with real sprint on CuisineFlow + - Test with real sprint on a production project - Collect user feedback from team ### If Tests Fail ❌ diff --git a/docs/TEST_EXECUTION_REPORT.md b/docs/TEST_EXECUTION_REPORT.md index 007bc4e..4429851 100644 --- a/docs/TEST_EXECUTION_REPORT.md +++ b/docs/TEST_EXECUTION_REPORT.md @@ -43,7 +43,7 @@ Details: - System config loads correctly from ~/.config/claude/gitea.env - Project config loads correctly from .env - Mode detection works (project mode) - - Repository correctly identified: claude-code-hhl-toolkit + - Repository correctly identified: support-claude-mktplace - Owner correctly identified: claude ``` @@ -54,7 +54,7 @@ Details: - System config loads correctly from ~/.config/claude/wikijs.env - Project config loads correctly from .env - Mode detection works (project mode) - - Project correctly identified: projects/claude-code-hhl-toolkit + - Project correctly identified: projects/support-claude-mktplace - Base path correctly set: /hyper-hive-labs ``` diff --git a/docs/references/MCP-GITEA.md b/docs/references/MCP-GITEA.md index dbbb769..c63ff8a 100644 --- a/docs/references/MCP-GITEA.md +++ b/docs/references/MCP-GITEA.md @@ -63,14 +63,14 @@ def load(self): **File:** `~/.config/claude/gitea.env` ```bash -GITEA_API_URL=https://gitea.hotserv.cloud/api/v1 +GITEA_API_URL=https://gitea.example.com/api/v1 GITEA_API_TOKEN=your_gitea_token -GITEA_OWNER=hhl-infra +GITEA_OWNER=bandit ``` **Generating Gitea API Token:** -1. Log into Gitea: https://gitea.hotserv.cloud +1. Log into Gitea: https://gitea.example.com 2. Navigate to: **Settings** β†’ **Applications** β†’ **Manage Access Tokens** 3. Click **Generate New Token** 4. Token configuration: @@ -90,9 +90,9 @@ mkdir -p ~/.config/claude # Create gitea.env cat > ~/.config/claude/gitea.env << EOF -GITEA_API_URL=https://gitea.hotserv.cloud/api/v1 +GITEA_API_URL=https://gitea.example.com/api/v1 GITEA_API_TOKEN=your_token_here -GITEA_OWNER=hhl-infra +GITEA_OWNER=bandit EOF # Secure the file (important!) diff --git a/docs/references/MCP-WIKIJS.md b/docs/references/MCP-WIKIJS.md index 96c613f..a3b31fa 100644 --- a/docs/references/MCP-WIKIJS.md +++ b/docs/references/MCP-WIKIJS.md @@ -1134,7 +1134,7 @@ from mcp_server.wikijs_client import WikiJSClient async def initialize_wiki_structure(): - """Create base Wiki.js structure for Hyper Hive Labs""" + """Create base Wiki.js structure for Bandit Labs""" print("Initializing Wiki.js base structure...") print("=" * 60) @@ -1154,10 +1154,10 @@ async def initialize_wiki_structure(): base_pages = [ { 'path': 'hyper-hive-labs', - 'title': 'Hyper Hive Labs', - 'content': '''# Hyper Hive Labs Documentation + 'title': 'Bandit Labs', + 'content': '''# Bandit Labs Documentation -Welcome to the Hyper Hive Labs knowledge base. +Welcome to the Bandit Labs knowledge base. ## Organization @@ -1176,7 +1176,7 @@ This knowledge base captures: All content is searchable and tagged for easy discovery across projects. ''', 'tags': ['company', 'index'], - 'description': 'Hyper Hive Labs company knowledge base' + 'description': 'Bandit Labs company knowledge base' }, { 'path': 'hyper-hive-labs/projects', diff --git a/docs/references/PLUGIN-PMO.md b/docs/references/PLUGIN-PMO.md index 69e1e01..477aa99 100644 --- a/docs/references/PLUGIN-PMO.md +++ b/docs/references/PLUGIN-PMO.md @@ -48,11 +48,11 @@ projman-pmo/ "version": "0.1.0", "displayName": "Projman PMO - Multi-Project Coordination", "description": "PMO coordination with cross-project visibility, dependency tracking, and resource management", - "author": "Hyper Hive Labs", - "homepage": "https://gitea.hotserv.cloud/hhl-infra/claude-code-hhl-toolkit/projman-pmo", + "author": "Bandit Labs", + "homepage": "ssh://git@hotserv.tailc9b278.ts.net:2222/bandit/support-claude-mktplace", "repository": { "type": "git", - "url": "https://gitea.hotserv.cloud/hhl-infra/claude-code-hhl-toolkit.git" + "url": "ssh://git@hotserv.tailc9b278.ts.net:2222/bandit/support-claude-mktplace.git" }, "license": "MIT", "keywords": [ @@ -223,7 +223,7 @@ projman-pmo/ ### Agent Personality ```markdown -You are the PMO Coordinator for Hyper Hive Labs. +You are the PMO Coordinator for Bandit Labs. Your role: - Maintain strategic view across all projects diff --git a/docs/references/PLUGIN-PROJMAN.md b/docs/references/PLUGIN-PROJMAN.md index 580992b..b279124 100644 --- a/docs/references/PLUGIN-PROJMAN.md +++ b/docs/references/PLUGIN-PROJMAN.md @@ -54,11 +54,11 @@ projman/ "version": "0.1.0", "displayName": "Projman - Single-Repository Project Management", "description": "Sprint planning and project management with Gitea and Wiki.js integration", - "author": "Hyper Hive Labs", - "homepage": "https://gitea.hotserv.cloud/hhl-infra/claude-code-hhl-toolkit/projman", + "author": "Bandit Labs", + "homepage": "https://gitea.example.com/bandit/support-claude-mktplace/projman", "repository": { "type": "git", - "url": "https://gitea.hotserv.cloud/hhl-infra/claude-code-hhl-toolkit.git" + "url": "https://gitea.example.com/bandit/support-claude-mktplace.git" }, "license": "MIT", "keywords": [ @@ -269,7 +269,7 @@ Agents are also **markdown files** with specialized prompts. They can be invoked ```markdown # Sprint Planner Agent -You are the Sprint Planner for Hyper Hive Labs. +You are the Sprint Planner for Bandit Labs. ## Your Identity @@ -422,7 +422,7 @@ Planner: I'll create the issue... [Tool executes: create_issue(...)] Created issue #47: "Extract Intuit Engine Service" -View at: https://gitea.hotserv.cloud/org/repo/issues/47 +View at: https://gitea.example.com/org/repo/issues/47 Now let me generate a detailed sprint plan... ``` @@ -551,7 +551,7 @@ The projman plugin implements a three-agent architecture mirroring the proven wo **Example Prompt:** ```markdown -You are the Sprint Planner for Hyper Hive Labs. +You are the Sprint Planner for Bandit Labs. Your role: - Guide users through sprint planning @@ -609,7 +609,7 @@ Sprint Planning Flow: **Example Prompt:** ```markdown -You are the Sprint Orchestrator for Hyper Hive Labs. +You are the Sprint Orchestrator for Bandit Labs. Your role: - Monitor sprint progress @@ -668,7 +668,7 @@ Status Monitoring: **Example Prompt:** ```markdown -You are the Sprint Executor for Hyper Hive Labs. +You are the Sprint Executor for Bandit Labs. Your role: - Provide implementation guidance diff --git a/docs/references/PROJECT-SUMMARY.md b/docs/references/PROJECT-SUMMARY.md index 457b881..374c3c0 100644 --- a/docs/references/PROJECT-SUMMARY.md +++ b/docs/references/PROJECT-SUMMARY.md @@ -80,7 +80,7 @@ The MCP servers detect their operating mode based on environment variables: ## Repository Structure ``` -hhl-infra/claude-code-hhl-toolkit/ +bandit/support-claude-mktplace/ β”œβ”€β”€ mcp-servers/ # ← SHARED BY BOTH PLUGINS β”‚ β”œβ”€β”€ gitea/ # Gitea MCP Server β”‚ β”‚ β”œβ”€β”€ .venv/ @@ -150,9 +150,9 @@ The plugins use a hybrid configuration approach that balances security and flexi **System-Level:** ```bash # ~/.config/claude/gitea.env -GITEA_API_URL=https://gitea.hotserv.cloud/api/v1 +GITEA_API_URL=https://gitea.example.com/api/v1 GITEA_API_TOKEN=your_token -GITEA_OWNER=hhl-infra +GITEA_OWNER=bandit # ~/.config/claude/wikijs.env WIKIJS_API_URL=https://wiki.hyperhivelabs.com/graphql @@ -366,9 +366,9 @@ mkdir -p ~/.config/claude # Gitea config cat > ~/.config/claude/gitea.env << EOF -GITEA_API_URL=https://gitea.hotserv.cloud/api/v1 +GITEA_API_URL=https://gitea.example.com/api/v1 GITEA_API_TOKEN=your_gitea_token -GITEA_OWNER=hhl-infra +GITEA_OWNER=bandit EOF # Wiki.js config diff --git a/mcp-servers/gitea/README.md b/mcp-servers/gitea/README.md index 6547d6f..b0daa4b 100644 --- a/mcp-servers/gitea/README.md +++ b/mcp-servers/gitea/README.md @@ -109,9 +109,9 @@ Create `~/.config/claude/gitea.env`: mkdir -p ~/.config/claude cat > ~/.config/claude/gitea.env << EOF -GITEA_API_URL=https://gitea.hotserv.cloud/api/v1 +GITEA_API_URL=https://gitea.example.com/api/v1 GITEA_API_TOKEN=your_gitea_token_here -GITEA_OWNER=hhl-infra +GITEA_OWNER=bandit EOF chmod 600 ~/.config/claude/gitea.env @@ -135,9 +135,9 @@ For company/PMO mode, omit the `.env` file or don't set `GITEA_REPO`. **File**: `~/.config/claude/gitea.env` **Required Variables**: -- `GITEA_API_URL` - Gitea API endpoint (e.g., `https://gitea.hotserv.cloud/api/v1`) +- `GITEA_API_URL` - Gitea API endpoint (e.g., `https://gitea.example.com/api/v1`) - `GITEA_API_TOKEN` - Personal access token with repo permissions -- `GITEA_OWNER` - Organization or user name (e.g., `hhl-infra`) +- `GITEA_OWNER` - Organization or user name (e.g., `bandit`) ### Project-Level Configuration @@ -148,7 +148,7 @@ For company/PMO mode, omit the `.env` file or don't set `GITEA_REPO`. ### Generating Gitea API Token -1. Log into Gitea: https://gitea.hotserv.cloud +1. Log into Gitea: https://gitea.example.com 2. Navigate to: **Settings** β†’ **Applications** β†’ **Manage Access Tokens** 3. Click **Generate New Token** 4. Configure token: @@ -309,7 +309,7 @@ ls -la ~/.config/claude/gitea.env ```bash # Test token manually curl -H "Authorization: token YOUR_TOKEN" \ - https://gitea.hotserv.cloud/api/v1/user + https://gitea.example.com/api/v1/user ``` **Permission denied on branch**: @@ -389,7 +389,7 @@ def list_issues(self, state='open', labels=None, repo=None): ## License -Part of the HyperHive Labs Claude Code Plugins project. +Part of the Bandit Labs Claude Code Plugins project. ## Related Documentation @@ -407,7 +407,7 @@ For issues or questions: --- -**Built for**: HyperHive Labs Project Management Plugins +**Built for**: Bandit Labs Project Management Plugins **Phase**: 1 (Complete) **Status**: βœ… Production Ready **Last Updated**: 2025-01-06 diff --git a/mcp-servers/gitea/TESTING.md b/mcp-servers/gitea/TESTING.md index d79a0d9..c901b50 100644 --- a/mcp-servers/gitea/TESTING.md +++ b/mcp-servers/gitea/TESTING.md @@ -170,7 +170,7 @@ Test the MCP server with a real Gitea instance. ### Prerequisites -1. **Gitea Instance**: Access to https://gitea.hotserv.cloud (or your Gitea instance) +1. **Gitea Instance**: Access to https://gitea.example.com (or your Gitea instance) 2. **API Token**: Personal access token with required permissions 3. **Configuration**: Properly configured system and project configs @@ -182,9 +182,9 @@ Create system-level configuration: mkdir -p ~/.config/claude cat > ~/.config/claude/gitea.env << EOF -GITEA_API_URL=https://gitea.hotserv.cloud/api/v1 +GITEA_API_URL=https://gitea.example.com/api/v1 GITEA_API_TOKEN=your_gitea_token_here -GITEA_OWNER=hhl-infra +GITEA_OWNER=bandit EOF chmod 600 ~/.config/claude/gitea.env @@ -205,7 +205,7 @@ echo ".env" >> .gitignore ### Step 2: Generate Gitea API Token -1. Log into Gitea: https://gitea.hotserv.cloud +1. Log into Gitea: https://gitea.example.com 2. Navigate to: **Settings** β†’ **Applications** β†’ **Manage Access Tokens** 3. Click **Generate New Token** 4. Token configuration: @@ -238,8 +238,8 @@ print(f'Mode: {result[\"mode\"]}') Expected output: ``` -API URL: https://gitea.hotserv.cloud/api/v1 -Owner: hhl-infra +API URL: https://gitea.example.com/api/v1 +Owner: bandit Repo: test-repo (or None for company mode) Mode: project (or company) ``` @@ -375,9 +375,9 @@ print('\\nβœ… PMO mode tests passed!') **System-level** (`~/.config/claude/gitea.env`): ```bash -GITEA_API_URL=https://gitea.hotserv.cloud/api/v1 +GITEA_API_URL=https://gitea.example.com/api/v1 GITEA_API_TOKEN=your_token_here -GITEA_OWNER=hhl-infra +GITEA_OWNER=bandit ``` **Project-level** (`.env` in project root): @@ -443,9 +443,9 @@ FileNotFoundError: System config not found: /home/user/.config/claude/gitea.env # Create system config mkdir -p ~/.config/claude cat > ~/.config/claude/gitea.env << EOF -GITEA_API_URL=https://gitea.hotserv.cloud/api/v1 +GITEA_API_URL=https://gitea.example.com/api/v1 GITEA_API_TOKEN=your_token_here -GITEA_OWNER=hhl-infra +GITEA_OWNER=bandit EOF chmod 600 ~/.config/claude/gitea.env @@ -480,7 +480,7 @@ requests.exceptions.HTTPError: 401 Client Error: Unauthorized ```bash # Test token manually curl -H "Authorization: token YOUR_TOKEN" \ - https://gitea.hotserv.cloud/api/v1/user + https://gitea.example.com/api/v1/user # If fails, regenerate token in Gitea settings ``` diff --git a/mcp-servers/netbox/README.md b/mcp-servers/netbox/README.md index 588bbf8..f5e10e6 100644 --- a/mcp-servers/netbox/README.md +++ b/mcp-servers/netbox/README.md @@ -294,4 +294,4 @@ logging.basicConfig(level=logging.DEBUG) ## License -Part of the claude-code-hhl-toolkit project. +Part of the Bandit Labs Claude Code Plugins project (`support-claude-mktplace`). diff --git a/mcp-servers/wikijs/README.md b/mcp-servers/wikijs/README.md index bfabde8..99778aa 100644 --- a/mcp-servers/wikijs/README.md +++ b/mcp-servers/wikijs/README.md @@ -131,7 +131,7 @@ For project-scoped operations, create `.env` in project root: # In your project directory cat > .env << 'EOF' # Wiki.js project path -WIKIJS_PROJECT=projects/cuisineflow +WIKIJS_PROJECT=projects/your-project-name EOF # Add to .gitignore @@ -236,15 +236,14 @@ The MCP server is referenced in plugin `.mcp.json`: ``` /hyper-hive-labs/ # Base path β”œβ”€β”€ projects/ # Project-specific -β”‚ β”œβ”€β”€ cuisineflow/ +β”‚ β”œβ”€β”€ your-project/ β”‚ β”‚ β”œβ”€β”€ lessons-learned/ β”‚ β”‚ β”‚ β”œβ”€β”€ sprints/ β”‚ β”‚ β”‚ β”œβ”€β”€ patterns/ β”‚ β”‚ β”‚ └── INDEX.md β”‚ β”‚ └── documentation/ -β”‚ β”œβ”€β”€ cuisineflow-site/ -β”‚ β”œβ”€β”€ intuit-engine/ -β”‚ └── hhl-site/ +β”‚ β”œβ”€β”€ another-project/ +β”‚ └── shared-library/ β”œβ”€β”€ company/ # Company-wide β”‚ β”œβ”€β”€ processes/ β”‚ β”œβ”€β”€ standards/ @@ -409,6 +408,6 @@ MIT License - See repository root for details ## Support For issues and questions: -- **Repository**: https://gitea.hotserv.cloud/hhl-infra/claude-code-hhl-toolkit -- **Issues**: https://gitea.hotserv.cloud/hhl-infra/claude-code-hhl-toolkit/issues +- **Repository**: `ssh://git@hotserv.tailc9b278.ts.net:2222/bandit/support-claude-mktplace.git` +- **Issues**: Contact repository maintainer - **Documentation**: `/docs/references/MCP-WIKIJS.md` diff --git a/projman/.claude-plugin/plugin.json b/projman/.claude-plugin/plugin.json index b082da1..1bd1b5f 100644 --- a/projman/.claude-plugin/plugin.json +++ b/projman/.claude-plugin/plugin.json @@ -3,11 +3,11 @@ "version": "0.1.0", "displayName": "Projman - Project Management for Claude Code", "description": "Sprint planning and project management with Gitea and Wiki.js integration. Provides AI-guided sprint planning, issue creation with label taxonomy, and lessons learned capture.", - "author": "Hyper Hive Labs", - "homepage": "https://gitea.hotserv.cloud/hhl-infra/claude-code-hhl-toolkit", + "author": "Bandit Labs", + "homepage": "ssh://git@hotserv.tailc9b278.ts.net:2222/bandit/support-claude-mktplace", "repository": { "type": "git", - "url": "https://gitea.hotserv.cloud/hhl-infra/claude-code-hhl-toolkit.git" + "url": "ssh://git@hotserv.tailc9b278.ts.net:2222/bandit/support-claude-mktplace.git" }, "license": "MIT", "keywords": [ diff --git a/projman/CONFIGURATION.md b/projman/CONFIGURATION.md index acc7528..a0ee8e8 100644 --- a/projman/CONFIGURATION.md +++ b/projman/CONFIGURATION.md @@ -90,7 +90,7 @@ python -c "from mcp_server import server; print('Wiki.js MCP Server installed su ### 2.1 Generate Gitea API Token -1. Log into Gitea: https://gitea.hotserv.cloud +1. Log into Gitea: https://gitea.example.com 2. Navigate to: **User Icon** (top right) β†’ **Settings** 3. Click **Applications** tab 4. Scroll to **Manage Access Tokens** @@ -145,9 +145,9 @@ mkdir -p ~/.config/claude ```bash cat > ~/.config/claude/gitea.env << 'EOF' # Gitea API Configuration -GITEA_API_URL=https://gitea.hotserv.cloud/api/v1 +GITEA_API_URL=https://gitea.example.com/api/v1 GITEA_API_TOKEN=your_gitea_token_here -GITEA_OWNER=hhl-infra +GITEA_OWNER=bandit EOF # Secure the file (owner read/write only) @@ -159,7 +159,7 @@ chmod 600 ~/.config/claude/gitea.env **Configuration Variables:** - `GITEA_API_URL` - Gitea API endpoint (includes `/api/v1`) - `GITEA_API_TOKEN` - Personal access token from Step 2.1 -- `GITEA_OWNER` - Organization or user name (e.g., `hhl-infra`) +- `GITEA_OWNER` - Organization or user name (e.g., `bandit`) ### 3.3 Configure Wiki.js @@ -251,7 +251,7 @@ Test that everything is configured correctly: ```bash # Test with curl curl -H "Authorization: token YOUR_GITEA_TOKEN" \ - https://gitea.hotserv.cloud/api/v1/user + https://gitea.example.com/api/v1/user # Should return your user information in JSON format ``` @@ -313,9 +313,9 @@ This will: **`~/.config/claude/gitea.env`:** ```bash -GITEA_API_URL=https://gitea.hotserv.cloud/api/v1 +GITEA_API_URL=https://gitea.example.com/api/v1 GITEA_API_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxxx -GITEA_OWNER=hhl-infra +GITEA_OWNER=bandit ``` **`~/.config/claude/wikijs.env`:** @@ -417,7 +417,7 @@ ls -la ~/.config/claude/wikijs.env ```bash # Test Gitea token curl -H "Authorization: token YOUR_TOKEN" \ - https://gitea.hotserv.cloud/api/v1/user + https://gitea.example.com/api/v1/user # Test Wiki.js token curl -H "Authorization: Bearer YOUR_TOKEN" \ @@ -515,7 +515,7 @@ After configuration is complete: - Review MCP server documentation: - [Gitea MCP](../mcp-servers/gitea/README.md) - [Wiki.js MCP](../mcp-servers/wikijs/README.md) -- Open issue: https://gitea.hotserv.cloud/hhl-infra/claude-code-hhl-toolkit/issues +- Contact repository maintainer for support **Questions:** - Read command documentation: `commands/*.md` diff --git a/projman/README.md b/projman/README.md index 25cedf9..9c0dce1 100644 --- a/projman/README.md +++ b/projman/README.md @@ -52,9 +52,9 @@ mkdir -p ~/.config/claude # Gitea configuration cat > ~/.config/claude/gitea.env << EOF -GITEA_API_URL=https://gitea.hotserv.cloud/api/v1 +GITEA_API_URL=https://gitea.example.com/api/v1 GITEA_API_TOKEN=your_gitea_token_here -GITEA_OWNER=hhl-infra +GITEA_OWNER=bandit EOF # Wiki.js configuration @@ -322,7 +322,7 @@ See [CONFIGURATION.md](./CONFIGURATION.md) for detailed configuration instructio ### Cannot connect to Gitea - Verify `~/.config/claude/gitea.env` exists and has correct URL and token -- Test token: `curl -H "Authorization: token YOUR_TOKEN" https://gitea.hotserv.cloud/api/v1/user` +- Test token: `curl -H "Authorization: token YOUR_TOKEN" https://gitea.example.com/api/v1/user` - Check network connectivity ### Cannot connect to Wiki.js @@ -410,8 +410,8 @@ projman/ - [Wiki.js MCP Server](../mcp-servers/wikijs/README.md) - Wiki.js integration details **Issues:** -- Report bugs: https://gitea.hotserv.cloud/hhl-infra/claude-code-hhl-toolkit/issues -- Feature requests: Same issue tracker +- Report bugs: Contact repository maintainer +- Feature requests: Contact repository maintainer - Documentation improvements: Submit PR ## License @@ -434,6 +434,6 @@ MIT License - See repository root for details --- -**Built for:** HyperHive Labs +**Built for:** Bandit Labs **Status:** Phase 2 Complete - Commands ready for testing **Next:** Implement agent system (Phase 3) diff --git a/projman/commands/labels-sync.md b/projman/commands/labels-sync.md index 5189434..4cdcc5e 100644 --- a/projman/commands/labels-sync.md +++ b/projman/commands/labels-sync.md @@ -105,7 +105,7 @@ The command updates `skills/label-taxonomy/labels-reference.md` with: # Label Taxonomy Reference Last synced: 2025-01-18 14:30 UTC -Source: Gitea (hhl-infra/cuisineflow) +Source: Gitea (bandit/your-repo-name) ## Organization Labels (28) diff --git a/projman/skills/label-taxonomy/labels-reference.md b/projman/skills/label-taxonomy/labels-reference.md index 02c0d9b..cfa4cf0 100644 --- a/projman/skills/label-taxonomy/labels-reference.md +++ b/projman/skills/label-taxonomy/labels-reference.md @@ -7,7 +7,7 @@ description: Dynamic reference for Gitea label taxonomy (organization + reposito **Status:** βœ… Synced with Gitea **Last synced:** 2025-11-21 (via automated testing) -**Source:** Gitea (hhl-infra/claude-code-hhl-toolkit) +**Source:** Gitea (bandit/support-claude-mktplace) ## Overview @@ -17,7 +17,7 @@ This skill provides the current label taxonomy used for issue classification in ## Organization Labels (27) -Organization-level labels are shared across all repositories in the `hhl-infra` organization. +Organization-level labels are shared across all repositories in the `bandit` organization. ### Agent (2) - `Agent/Human` (#0052cc) - Work performed by human developers @@ -62,7 +62,7 @@ Organization-level labels are shared across all repositories in the `hhl-infra` ## Repository Labels (16) -Repository-level labels are specific to the claude-code-hhl-toolkit project. +Repository-level labels are specific to each project. ### Component (9) - `Component/Backend` (#5319e7) - Backend service code and business logic diff --git a/claude-code-hhl-toolkit.code-workspace b/support-claude-mktplace.code-workspace similarity index 98% rename from claude-code-hhl-toolkit.code-workspace rename to support-claude-mktplace.code-workspace index be325ef..0e8b59b 100644 --- a/claude-code-hhl-toolkit.code-workspace +++ b/support-claude-mktplace.code-workspace @@ -110,7 +110,7 @@ "group": "Core" }, { - "filePath": "claude-code-hhl-toolkit.code-workspace", + "filePath": "support-claude-mktplace.code-workspace", "group": "Core" }, { -- 2.49.1