Skip to main content
Glama

NetBox Read/Write MCP Server

sites.py13.2 kB
#!/usr/bin/env python3 """ DCIM Site Management Tools High-level tools for managing NetBox sites, locations, regions, and site infrastructure with enterprise-grade functionality. """ from typing import Dict, Optional, Any import logging from ...registry import mcp_tool from ...client import NetBoxClient logger = logging.getLogger(__name__) @mcp_tool(category="dcim") def netbox_create_site( client: NetBoxClient, name: str, slug: str, status: str = "active", region: Optional[str] = None, description: Optional[str] = None, physical_address: Optional[str] = None, shipping_address: Optional[str] = None, contact_name: Optional[str] = None, contact_phone: Optional[str] = None, contact_email: Optional[str] = None, confirm: bool = False ) -> Dict[str, Any]: """ Create a new site in NetBox DCIM. Args: client: NetBoxClient instance (injected) name: Site name slug: URL-friendly identifier status: Site status (active, planned, staged, decommissioning, retired) region: Optional region name description: Optional description physical_address: Physical location address shipping_address: Shipping address if different contact_name: Primary contact name contact_phone: Contact phone number contact_email: Contact email address confirm: Must be True to execute (safety mechanism) Returns: Created site information or error details Example: netbox_create_site("Amsterdam DC", "amsterdam-dc", status="active", confirm=True) """ try: if not name or not slug: return { "success": False, "error": "Site name and slug are required", "error_type": "ValidationError" } logger.info(f"Creating site: {name} (slug: {slug})") # Build site data site_data = { "name": name, "slug": slug, "status": status } if region: site_data["region"] = region if description: site_data["description"] = description if physical_address: site_data["physical_address"] = physical_address if shipping_address: site_data["shipping_address"] = shipping_address if contact_name: site_data["contact_name"] = contact_name if contact_phone: site_data["contact_phone"] = contact_phone if contact_email: site_data["contact_email"] = contact_email # Use dynamic API with safety result = client.dcim.sites.create(confirm=confirm, **site_data) return { "success": True, "action": "created", "object_type": "site", "site": result, "dry_run": result.get("dry_run", False) } except Exception as e: logger.error(f"Failed to create site {name}: {e}") return { "success": False, "error": str(e), "error_type": type(e).__name__ } @mcp_tool(category="dcim") def netbox_get_site_info( client: NetBoxClient, site_name: str ) -> Dict[str, Any]: """ Get detailed information about ONE specific site by name. This tool is exclusively intended for retrieving details of a single, already known site. Use 'netbox_list_all_sites' for open, exploratory questions. Args: client: NetBoxClient instance (injected) site_name: The exact name of the site to be retrieved Returns: Site information including racks, devices, and statistics Example: netbox_get_site_info("Amsterdam DC") """ try: logger.info(f"Getting site information: {site_name}") # Find the site # ULTRATHINK FIX 1: Expand search parameters with comprehensive relationship data\n site_search_params = {\n \"expand\": [\"region\", \"tenant\"],\n \"limit\": 50\n }\n\n # ULTRATHINK FIX 2: ID resolution with fallback patterns\n sites = None\n if site_name.isdigit():\n sites = list(client.dcim.sites.filter(id=int(site_name), **site_search_params))\n\n if not sites:\n sites = list(client.dcim.sites.filter(name=site_name, **site_search_params))\n\n # ULTRATHINK FIX 4: Slug-based fallback mechanisms\n if not sites:\n sites = list(client.dcim.sites.filter(slug=site_name, **site_search_params)) if not sites: return { "success": False, "error": f"Site '{site_name}' not found", "error_type": "SiteNotFound" } site = sites[0] site_id = site["id"] # Get related objects racks = client.dcim.racks.filter(site_id=site_id) devices = client.dcim.devices.filter(site_id=site_id) return { "success": True, "site": site, "statistics": { "rack_count": len(racks), "device_count": len(devices), "total_rack_units": sum(rack.get("u_height", 0) for rack in racks) }, "racks": racks, "devices": devices[:10] # Limit to first 10 devices } except Exception as e: logger.error(f"Failed to get site info for {site_name}: {e}") return { "success": False, "error": str(e), "error_type": type(e).__name__ } @mcp_tool(category="dcim") def netbox_list_all_sites( client: NetBoxClient, limit: int = 100, region_name: Optional[str] = None, status: Optional[str] = None, tenant_name: Optional[str] = None ) -> Dict[str, Any]: """ Get a summarized list of all sites in NetBox. This function is the correct choice for open, exploratory questions like "what sites are there?" or "show all active locations". Use 'netbox_get_site_info' for detailed information about one specific site. Args: client: NetBoxClient instance (injected by dependency system) limit: Maximum number of results to return (default: 100) region_name: Filter by region name (optional) status: Filter by site status (active, planned, retired, etc.) tenant_name: Filter by tenant name (optional) Returns: Dictionary containing: - count: Total number of sites found - sites: List of summarized site information - filters_applied: Dictionary of filters that were applied - summary_stats: Aggregate statistics about the sites Example: netbox_list_all_sites(status="active", region_name="europe") netbox_list_all_sites(tenant_name="customer-a", limit=20) """ try: logger.info(f"Listing sites with filters - region: {region_name}, status: {status}, tenant: {tenant_name}") # Build filters dictionary - only include non-None values filters = {} if region_name: filters['region'] = region_name if status: filters['status'] = status if tenant_name: filters['tenant'] = tenant_name # Execute filtered query with limit sites = list(client.dcim.sites.filter(**filters)) # Apply limit after fetching if len(sites) > limit: sites = sites[:limit] # Generate summary statistics status_counts = {} region_counts = {} tenant_counts = {} # Collect device and rack statistics for each site total_devices = 0 total_racks = 0 for site in sites: # Status breakdown with defensive checks for dictionary access status_obj = site.get("status", {}) if isinstance(status_obj, dict): status = status_obj.get("label", "N/A") else: status = str(status_obj) if status_obj else "N/A" status_counts[status] = status_counts.get(status, 0) + 1 # Region breakdown with defensive checks for dictionary access region_obj = site.get("region") if region_obj: if isinstance(region_obj, dict): region_name = region_obj.get("name", str(region_obj)) else: region_name = str(region_obj) region_counts[region_name] = region_counts.get(region_name, 0) + 1 # Tenant breakdown with defensive checks for dictionary access tenant_obj = site.get("tenant") if tenant_obj: if isinstance(tenant_obj, dict): tenant_name = tenant_obj.get("name", str(tenant_obj)) else: tenant_name = str(tenant_obj) tenant_counts[tenant_name] = tenant_counts.get(tenant_name, 0) + 1 # Get basic counts for this site (efficient queries) site_id = site.get("id") site_devices = list(client.dcim.devices.filter(site_id=site_id)) site_racks = list(client.dcim.racks.filter(site_id=site_id)) total_devices += len(site_devices) total_racks += len(site_racks) # Create human-readable site list site_list = [] for site in sites: # Get counts for this specific site site_id = site.get("id") site_devices = list(client.dcim.devices.filter(site_id=site_id)) site_racks = list(client.dcim.racks.filter(site_id=site_id)) # DEFENSIVE CHECK: Handle dictionary access for all site attributes status_obj = site.get("status", {}) if isinstance(status_obj, dict): status = status_obj.get("label", "N/A") else: status = str(status_obj) if status_obj else "N/A" region_obj = site.get("region") region_name = None if region_obj: if isinstance(region_obj, dict): region_name = region_obj.get("name") else: region_name = str(region_obj) tenant_obj = site.get("tenant") if tenant_obj: if isinstance(tenant_obj, dict): tenant_name = tenant_obj.get("name", "N/A") else: tenant_name = str(tenant_obj) else: tenant_name = "N/A" site_info = { "name": site.get("name", "Unknown"), "slug": site.get("slug", ""), "status": status, "region": region_name, "tenant": tenant_name, # DEFENSIVE CHECK: Ensure description is never None "description": site.get("description", ""), "physical_address": site.get("physical_address"), "device_count": len(site_devices), "rack_count": len(site_racks), "total_rack_units": sum(rack.get("u_height", 0) for rack in site_racks if rack.get("u_height")), "contact_name": site.get("contact_name"), "contact_email": site.get("contact_email") } site_list.append(site_info) result = { "count": len(site_list), "sites": site_list, "filters_applied": {k: v for k, v in filters.items() if v is not None}, "summary_stats": { "total_sites": len(site_list), "status_breakdown": status_counts, "region_breakdown": region_counts, "tenant_breakdown": tenant_counts, "total_devices_across_sites": total_devices, "total_racks_across_sites": total_racks, "sites_with_contact": len([s for s in site_list if s['contact_name']]), "sites_with_address": len([s for s in site_list if s['physical_address']]) } } logger.info(f"Found {len(site_list)} sites matching criteria. Status breakdown: {status_counts}") return result except Exception as e: logger.error(f"Error listing sites: {e}") return { "count": 0, "sites": [], "error": str(e), "error_type": type(e).__name__, "filters_applied": {k: v for k, v in { 'region_name': region_name, 'status': status, 'tenant_name': tenant_name }.items() if v is not None} } # TODO: Implement advanced site management tools: # - netbox_plan_site_capacity # - netbox_manage_multi_site_infrastructure # - netbox_report_site_utilization # - netbox_analyze_site_geography # - netbox_manage_site_interconnections

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Deployment-Team/netbox-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server