Files
leo-claude-mktplace/plugins/cmdb-assistant/mcp-servers/netbox/mcp_server/netbox_client.py
lmiranda d84425cbb0 refactor: bundle MCP servers inside plugins for cache compatibility
Claude Code only caches the plugin directory when installed from a
marketplace, not parent directories. This broke the shared mcp-servers/
architecture because relative paths like ../../mcp-servers/ resolved
to non-existent locations in the cache.

Changes:
- Move gitea and wikijs MCP servers into plugins/projman/mcp-servers/
- Move netbox MCP server into plugins/cmdb-assistant/mcp-servers/
- Update .mcp.json files to use ${CLAUDE_PLUGIN_ROOT}/mcp-servers/
- Update setup.sh to handle new bundled structure
- Add netbox.env config template to setup.sh
- Update CLAUDE.md and CANONICAL-PATHS.md documentation

This ensures plugins work correctly when installed and cached.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-15 17:23:02 -05:00

295 lines
8.7 KiB
Python

"""
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)