Files
leo-claude-mktplace/mcp-servers/netbox/mcp_server/server.py
lmiranda 8129be5ef3 feat(netbox)!: gut server from 182 to 37 tools
BREAKING CHANGE: Removed circuits, tenancy, VPN, wireless modules entirely.
Stripped DCIM, IPAM, virtualization, extras to only essential tools.
Deleted NETBOX_ENABLED_MODULES filtering — no longer needed.

- Delete circuits.py, tenancy.py, vpn.py, wireless.py
- Strip dcim.py to sites, devices, interfaces only (11 tools)
- Strip ipam.py to IPs, prefixes, services only (10 tools)
- Strip virtualization.py to clusters, VMs, VM interfaces only (10 tools)
- Strip extras.py to tags, journal entries only (6 tools)
- Remove all module filtering code from config.py and server.py
- Rewrite README.md with accurate 37-tool documentation
- Update CHANGELOG.md with breaking change entry
- Token reduction: ~19,810 → ~3,700 (~81%)

Remaining work: Update cmdb-assistant plugin skills (10 files) in follow-up PR

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-08 21:25:43 -05:00

466 lines
18 KiB
Python

"""
MCP Server entry point for NetBox integration.
Provides essential NetBox tools to Claude Code via JSON-RPC 2.0 over stdio.
Covers DCIM, IPAM, Virtualization, and Extras modules.
"""
import asyncio
import logging
import json
from typing import Optional
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.virtualization import VirtualizationTools
from .tools.extras import ExtrasTools
# Suppress noisy MCP validation warnings on stderr
logging.basicConfig(level=logging.INFO)
logging.getLogger("root").setLevel(logging.ERROR)
logging.getLogger("mcp").setLevel(logging.ERROR)
logger = logging.getLogger(__name__)
# Tool definitions - 37 essential tools for tracking servers, services, IPs, and databases
TOOL_DEFINITIONS = {
# ==================== DCIM: Servers, Sites, Interfaces ====================
'dcim_list_sites': {
'description': 'List all sites in NetBox',
'properties': {
'name': {'type': 'string', 'description': 'Filter by name'},
'status': {'type': 'string', 'description': 'Filter by status'},
}
},
'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': 'Status'},
},
'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_list_devices': {
'description': 'List all devices (servers/VPS) in NetBox',
'properties': {
'name': {'type': 'string', 'description': 'Filter by name'},
'site_id': {'type': 'integer', 'description': 'Filter by site'},
'status': {'type': 'string', 'description': 'Filter by status'},
'role_id': {'type': 'integer', 'description': 'Filter by role'},
}
},
'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': 'Status'},
},
'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_list_interfaces': {
'description': 'List device interfaces',
'properties': {
'device_id': {'type': 'integer', 'description': 'Filter by device'},
'name': {'type': 'string', 'description': 'Filter by name'},
'type': {'type': 'string', 'description': 'Filter by 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 interface on a device',
'properties': {
'device': {'type': 'integer', 'description': 'Device ID'},
'name': {'type': 'string', 'description': 'Interface name'},
'type': {'type': 'string', 'description': 'Interface type'},
},
'required': ['device', 'name', 'type']
},
# ==================== IPAM: IPs, Prefixes, Services ====================
'ipam_list_ip_addresses': {
'description': 'List IP addresses',
'properties': {
'address': {'type': 'string', 'description': 'Filter by address'},
'device_id': {'type': 'integer', 'description': 'Filter by device'},
'status': {'type': 'string', 'description': 'Filter by status'},
}
},
'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 an IP address',
'properties': {
'address': {'type': 'string', 'description': 'IP address with prefix (e.g., 192.168.1.1/24)'},
'status': {'type': 'string', 'description': 'Status'},
'assigned_object_type': {'type': 'string', 'description': 'Object type (dcim.interface or virtualization.vminterface)'},
'assigned_object_id': {'type': 'integer', 'description': 'Object ID'},
},
'required': ['address']
},
'ipam_update_ip_address': {
'description': 'Update an IP address',
'properties': {
'id': {'type': 'integer', 'description': 'IP address ID'},
'status': {'type': 'string', 'description': 'New status'},
},
'required': ['id']
},
'ipam_list_prefixes': {
'description': 'List IP prefixes',
'properties': {
'prefix': {'type': 'string', 'description': 'Filter by prefix'},
'site_id': {'type': 'integer', 'description': 'Filter by site'},
'status': {'type': 'string', 'description': 'Filter by status'},
}
},
'ipam_get_prefix': {
'description': 'Get a specific prefix by ID',
'properties': {'id': {'type': 'integer', 'description': 'Prefix ID'}},
'required': ['id']
},
'ipam_create_prefix': {
'description': 'Create an IP prefix',
'properties': {
'prefix': {'type': 'string', 'description': 'IP prefix (e.g., 192.168.1.0/24)'},
'status': {'type': 'string', 'description': 'Status'},
'site': {'type': 'integer', 'description': 'Site ID'},
},
'required': ['prefix']
},
'ipam_list_services': {
'description': 'List services (applications, databases, etc.)',
'properties': {
'device_id': {'type': 'integer', 'description': 'Filter by device'},
'virtual_machine_id': {'type': 'integer', 'description': 'Filter by VM'},
'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 service (app, database, etc.)',
'properties': {
'name': {'type': 'string', 'description': 'Service name'},
'ports': {'type': 'array', 'description': 'Port numbers', 'items': {'type': 'integer'}},
'protocol': {'type': 'string', 'description': 'Protocol (tcp/udp)'},
'device': {'type': 'integer', 'description': 'Device ID (if on physical server)'},
'virtual_machine': {'type': 'integer', 'description': 'VM ID (if on VM)'},
},
'required': ['name', 'ports', 'protocol']
},
# ==================== Virtualization: VMs, Clusters ====================
'virt_list_clusters': {
'description': 'List virtualization clusters',
'properties': {
'name': {'type': 'string', 'description': 'Filter by name'},
'site_id': {'type': 'integer', 'description': 'Filter by site'},
}
},
'virt_get_cluster': {
'description': 'Get a specific cluster by ID',
'properties': {'id': {'type': 'integer', 'description': 'Cluster ID'}},
'required': ['id']
},
'virt_create_cluster': {
'description': 'Create a virtualization cluster',
'properties': {
'name': {'type': 'string', 'description': 'Cluster name'},
'type': {'type': 'integer', 'description': 'Cluster type ID'},
'site': {'type': 'integer', 'description': 'Site ID'},
},
'required': ['name', 'type']
},
'virt_list_vms': {
'description': 'List virtual machines',
'properties': {
'name': {'type': 'string', 'description': 'Filter by name'},
'cluster_id': {'type': 'integer', 'description': 'Filter by cluster'},
'site_id': {'type': 'integer', 'description': 'Filter by site'},
'status': {'type': 'string', 'description': 'Filter by status'},
}
},
'virt_get_vm': {
'description': 'Get a specific VM by ID',
'properties': {'id': {'type': 'integer', 'description': 'VM ID'}},
'required': ['id']
},
'virt_create_vm': {
'description': 'Create a virtual machine',
'properties': {
'name': {'type': 'string', 'description': 'VM name'},
'cluster': {'type': 'integer', 'description': 'Cluster ID'},
'status': {'type': 'string', 'description': 'Status'},
'vcpus': {'type': 'number', 'description': 'vCPU count'},
'memory': {'type': 'integer', 'description': 'Memory in MB'},
'disk': {'type': 'integer', 'description': 'Disk in GB'},
},
'required': ['name', 'cluster']
},
'virt_update_vm': {
'description': 'Update a virtual machine',
'properties': {
'id': {'type': 'integer', 'description': 'VM ID'},
'name': {'type': 'string', 'description': 'New name'},
'status': {'type': 'string', 'description': 'New status'},
},
'required': ['id']
},
'virt_list_vm_ifaces': {
'description': 'List VM interfaces',
'properties': {
'virtual_machine_id': {'type': 'integer', 'description': 'Filter by VM'},
}
},
'virt_get_vm_iface': {
'description': 'Get a specific VM interface by ID',
'properties': {'id': {'type': 'integer', 'description': 'VM interface ID'}},
'required': ['id']
},
'virt_create_vm_iface': {
'description': 'Create a VM interface',
'properties': {
'virtual_machine': {'type': 'integer', 'description': 'VM ID'},
'name': {'type': 'string', 'description': 'Interface name'},
},
'required': ['virtual_machine', 'name']
},
# ==================== Extras: Tags, Journal Entries ====================
'extras_list_tags': {
'description': 'List all tags in NetBox',
'properties': {
'name': {'type': 'string', 'description': 'Filter by name'},
}
},
'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_list_journal_entries': {
'description': 'List journal entries (audit/notes)',
'properties': {
'assigned_object_type': {'type': 'string', 'description': 'Object type'},
'assigned_object_id': {'type': 'integer', 'description': 'Object ID'},
}
},
'extras_get_journal_entry': {
'description': 'Get a specific journal entry by ID',
'properties': {'id': {'type': 'integer', 'description': 'Entry ID'}},
'required': ['id']
},
'extras_create_journal_entry': {
'description': 'Create a journal entry',
'properties': {
'assigned_object_type': {'type': 'string', 'description': 'Object type (e.g., dcim.device)'},
'assigned_object_id': {'type': 'integer', 'description': 'Object ID'},
'comments': {'type': 'string', 'description': 'Journal entry text'},
'kind': {'type': 'string', 'description': 'Kind (info, success, warning, danger)'},
},
'required': ['assigned_object_type', 'assigned_object_id', 'comments']
},
}
# Tool name mappings for shortened virtualization tools (virt_ prefix)
TOOL_NAME_MAP = {
# Virtualization tools (virt_ prefix -> virtualization category)
'virt_list_clusters': ('virtualization', 'list_clusters'),
'virt_get_cluster': ('virtualization', 'get_cluster'),
'virt_create_cluster': ('virtualization', 'create_cluster'),
'virt_list_vms': ('virtualization', 'list_virtual_machines'),
'virt_get_vm': ('virtualization', 'get_virtual_machine'),
'virt_create_vm': ('virtualization', 'create_virtual_machine'),
'virt_update_vm': ('virtualization', 'update_virtual_machine'),
'virt_list_vm_ifaces': ('virtualization', 'list_vm_interfaces'),
'virt_get_vm_iface': ('virtualization', 'get_vm_interface'),
'virt_create_vm_iface': ('virtualization', 'create_vm_interface'),
}
class NetBoxMCPServer:
"""MCP Server for NetBox integration"""
def __init__(self):
self.server = Server("netbox-mcp")
self.config = None
self.client = None
# Tool instances - always instantiate all 4 modules
self.dcim_tools = None
self.ipam_tools = None
self.virtualization_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()
# Always instantiate all 4 modules
self.dcim_tools = DCIMTools(self.client)
self.ipam_tools = IPAMTools(self.client)
self.virtualization_tools = VirtualizationTools(self.client)
self.extras_tools = ExtrasTools(self.client)
tool_count = len(TOOL_DEFINITIONS)
logger.info(
f"NetBox MCP Server initialized: {tool_count} tools registered "
f"(modules: dcim, ipam, virtualization, extras)"
)
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.
Tool names may be shortened (e.g., 'virt_list_vms' instead of
'virtualization_list_virtual_machines') to meet the 28-character
limit. TOOL_NAME_MAP handles the translation to actual method names.
"""
# Check if this is a mapped short name
if name in TOOL_NAME_MAP:
category, method_name = TOOL_NAME_MAP[name]
else:
# Fall back to original logic for unchanged tools
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,
'virtualization': self.virtualization_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())