from mcp.server.fastmcp import FastMCP from typing import Dict, List, Optional, Any import asyncio import logging import sys import os import glob import json # 导入我们创建的模块 from tool_manager import tool_manager, ToolCategory from network_devices import ( device_manager, NetworkDevice, DeviceCredential, DeviceVendor, DeviceType, DeviceStatus, ConnectionProtocol ) from device_connector import connection_manager, CommandResult, SCRAPLI_IMPORT_SUCCESS # 导入资源管理模块 from mcp_resources import ( resource_manager, ResourceType ) # 导入新的模板系统 from template_system import ( template_manager, render_template_with_resources, Message, UserMessage, AssistantMessage, SystemMessage ) # 导入设备命令提示模板系统 import device_prompts # 导入拓扑发现模块 from topology_discovery_improved import create_improved_topology_discovery # 导入网络扫描模块 from network_scanner import create_network_scanner # 引入相同的日志格式化处理 class JsonFormatter(logging.Formatter): def __init__(self, fmt=None, datefmt=None, style='%'): super().__init__(fmt, datefmt, style) def format(self, record): log_record = super().format(record) return log_record.encode('utf-8', errors='replace').decode('utf-8') # 配置日志格式 logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.StreamHandler(stream=sys.stdout) ] ) # 设置所有处理器使用UTF-8编码格式化 for handler in logging.root.handlers: if isinstance(handler, logging.StreamHandler): handler.setFormatter(JsonFormatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) logger = logging.getLogger("netbrain_mcp") # 创建一个MCP服务器 mcp = FastMCP("NetBrain MCP") # 创建拓扑发现引擎实例 topology_discovery = create_improved_topology_discovery(device_manager, connection_manager) # 创建网络扫描器实例 network_scanner = create_network_scanner(device_manager) # 工具分类:设备管理 @mcp.tool() async def list_devices( vendor: Optional[str] = None, device_type: Optional[str] = None, status: Optional[str] = None, tag: Optional[str] = None ) -> List[Dict[str, Any]]: """ 列出网络设备,支持过滤 Args: vendor: 设备厂商,可选值:cisco, huawei, h3c, juniper, arista, fortinet, checkpoint, other device_type: 设备类型,可选值:router, switch, firewall, load_balancer, wireless_controller, access_point, other status: 设备状态,可选值:online, offline, unreachable, maintenance, unknown tag: 标签过滤 Returns: 设备列表 """ # 转换过滤参数 vendor_enum = DeviceVendor(vendor) if vendor else None device_type_enum = DeviceType(device_type) if device_type else None status_enum = DeviceStatus(status) if status else None # 获取设备列表 devices = device_manager.list_devices( vendor=vendor_enum, device_type=device_type_enum, status=status_enum, tag=tag ) # 转换为字典列表 return [device.to_dict() for device in devices] @mcp.tool() async def add_device( name: str, ip_address: str, device_type: str, vendor: str, platform: str = "", model: str = "", os_version: str = "", location: str = "", description: str = "", tags: str = "" ) -> Dict[str, Any]: """ 添加新的网络设备 Args: name: 设备名称 ip_address: 设备IP地址 device_type: 设备类型,可选值:router, switch, firewall, load_balancer, wireless_controller, access_point, other vendor: 设备厂商,可选值:cisco, huawei, h3c, juniper, arista, fortinet, checkpoint, other platform: 设备平台(scrapli平台类型),如 cisco_iosxe, huawei_vrp, juniper_junos 等 model: 设备型号 os_version: 操作系统版本 location: 设备位置 description: 设备描述 tags: 设备标签,使用逗号分隔多个标签 Returns: 新添加的设备信息 """ # 转换参数 device_type_enum = DeviceType(device_type) vendor_enum = DeviceVendor(vendor) # 如果未提供platform,根据vendor推断 if not platform: if vendor.lower() == "cisco": platform = "cisco_iosxe" elif vendor.lower() == "huawei": platform = "huawei_vrp" elif vendor.lower() == "juniper": platform = "juniper_junos" elif vendor.lower() == "arista": platform = "arista_eos" else: platform = f"{vendor.lower()}" # 处理标签 tag_list = [] if tags: tag_list = [tag.strip() for tag in tags.split(',') if tag.strip()] # 创建设备对象 device = NetworkDevice( name=name, ip_address=ip_address, device_type=device_type_enum, vendor=vendor_enum, platform=platform, model=model, os_version=os_version, location=location, description=description, tags=tag_list ) # 添加设备 device_id = device_manager.add_device(device) return device.to_dict() @mcp.tool() async def get_device(device_id: str) -> Optional[Dict[str, Any]]: """ 获取设备详细信息 Args: device_id: 设备ID Returns: 设备信息或None """ device = device_manager.get_device(device_id) if device: return device.to_dict() return None @mcp.tool() async def update_device( device_id: str, name: str = "", ip_address: str = "", model: str = "", os_version: str = "", status: str = "", location: str = "", description: str = "", tags: str = "" ) -> Optional[Dict[str, Any]]: """ 更新设备信息 Args: device_id: 设备ID name: 设备名称 ip_address: 设备IP地址 model: 设备型号 os_version: 操作系统版本 status: 设备状态,可选值:online, offline, unreachable, maintenance, unknown location: 设备位置 description: 设备描述 tags: 设备标签,使用逗号分隔多个标签 Returns: 更新后的设备信息或None """ # 准备更新参数 update_kwargs = {} if name: update_kwargs["name"] = name if ip_address: update_kwargs["ip_address"] = ip_address if model: update_kwargs["model"] = model if os_version: update_kwargs["os_version"] = os_version if status: update_kwargs["status"] = DeviceStatus(status) if location: update_kwargs["location"] = location if description: update_kwargs["description"] = description if tags: # 将逗号分隔的标签转换为列表 tag_list = [tag.strip() for tag in tags.split(',') if tag.strip()] update_kwargs["tags"] = tag_list # 更新设备 device = device_manager.update_device(device_id, **update_kwargs) if device: return device.to_dict() return None @mcp.tool() async def delete_device(device_id: str) -> bool: """ 删除设备 Args: device_id: 设备ID Returns: 是否删除成功 """ return device_manager.delete_device(device_id) # 工具分类:凭据管理 @mcp.tool() async def add_credential( name: str, username: str, password: str = "", protocol: str = "ssh", port: Optional[int] = None, enable_password: str = "", ssh_key_file: str = "" ) -> Dict[str, Any]: """ 添加设备凭据 Args: name: 凭据名称 username: 用户名 password: 密码 protocol: 连接协议,可选值:ssh, telnet, snmp, http, https, netconf port: 端口号,可选(默认SSH=22, Telnet=23) enable_password: 特权模式密码(思科设备) ssh_key_file: SSH密钥文件路径 Returns: 凭据ID """ # 确保密码或SSH密钥的要求基于协议 if protocol.lower() == "ssh" and not password and not ssh_key_file: return {"success": False, "message": "SSH连接需要提供密码或SSH密钥文件"} elif protocol.lower() != "ssh" and not password: return {"success": False, "message": f"{protocol.upper()}连接需要提供密码"} # 转换参数 protocol_enum = ConnectionProtocol(protocol.lower()) # 设置默认端口 if port is None: if protocol_enum == ConnectionProtocol.SSH: port = 22 elif protocol_enum == ConnectionProtocol.TELNET: port = 23 elif protocol_enum == ConnectionProtocol.HTTP: port = 80 elif protocol_enum == ConnectionProtocol.HTTPS: port = 443 else: port = 0 # 记录凭据信息(不包含敏感信息) logger.info(f"添加凭据: name={name}, username={username}, protocol={protocol}, port={port}") if ssh_key_file: logger.info(f"使用SSH密钥文件: {ssh_key_file}") # 创建凭据对象 credential = DeviceCredential( name=name, username=username, password=password, protocol=protocol_enum, port=port, enable_password=enable_password, ssh_key_file=ssh_key_file ) # 添加凭据 credential_id = device_manager.add_credential(credential) return {"success": True, "id": credential_id, "name": name} @mcp.tool() async def list_credentials() -> List[Dict[str, Any]]: """ 列出所有设备凭据 Returns: 凭据列表 """ credentials = device_manager.list_credentials() return [ { "id": cred.id, "name": cred.name, "username": cred.username, "password": cred.password, "protocol": cred.protocol.value, "port": cred.port, "ssh_key_file": cred.ssh_key_file } for cred in credentials ] # 工具分类:设备连接 @mcp.tool() async def connect_device(device_id: str, credential_id: str) -> Dict[str, Any]: """ 连接到网络设备 Args: device_id: 设备ID credential_id: 凭据ID Returns: 连接结果 """ # 获取设备和凭据 device = device_manager.get_device(device_id) credential = device_manager.get_credential(credential_id) if not device: return {"success": False, "message": f"设备不存在: {device_id}"} if not credential: return {"success": False, "message": f"凭据不存在: {credential_id}"} # 记录连接信息 logger.info(f"尝试连接设备: {device.name} ({device.ip_address}) 使用凭据: {credential.name}") logger.info(f"连接协议: {credential.protocol.value}, 端口: {credential.port or '默认'}") # 连接设备 success, error = await connection_manager.connect_device(device, credential) if success: # 获取更多的连接信息 prompt = "" version_info = "" try: # 获取连接的connector connection_key = f"{device_id}_{credential_id}" connector = connection_manager.active_connections.get(connection_key) if connector and connector.connection: # 获取提示符 if hasattr(connector.connection, "get_prompt"): if asyncio.iscoroutinefunction(connector.connection.get_prompt): prompt = await connector.connection.get_prompt() else: prompt = await asyncio.to_thread(connector.connection.get_prompt) logger.info(f"成功获取设备提示符: {prompt}") # 使用与test_scrapli_connection相同的方式获取版本信息 try: from scrapli import Scrapli # 确定平台类型 platform = device.platform if hasattr(device, 'platform') and device.platform else "" # 根据平台类型选择命令 if "huawei" in platform.lower(): command = "display version" elif "cisco" in platform.lower(): command = "show version" else: command = "show version" logger.info(f"尝试获取版本信息,平台: {platform},命令: {command}") # 直接使用Scrapli连接对象发送命令 if hasattr(connector.connection, "send_command"): # 等待1秒确保连接稳定 await asyncio.sleep(1) if asyncio.iscoroutinefunction(connector.connection.send_command): resp = await connector.connection.send_command(command) if hasattr(resp, "result"): version_info = resp.result else: version_info = str(resp) else: # 同步调用需要运行在线程中 resp = await asyncio.to_thread(connector.connection.send_command, command) if hasattr(resp, "result"): version_info = resp.result else: version_info = str(resp) logger.info(f"成功获取版本信息,长度: {len(version_info)}") # 限制长度 if len(version_info) > 500: version_info = version_info[:500] + "..." else: logger.warning("连接对象没有send_command方法") version_info = "无法获取版本信息:连接对象不支持发送命令" except Exception as e: logger.warning(f"获取版本信息时出错: {str(e)}") # 回退到使用connection_manager发送命令 try: result, _ = await connection_manager.send_command( device_id=device_id, credential_id=credential_id, command=command, timeout=60 ) if result and result.success: version_info = result.output if len(version_info) > 500: version_info = version_info[:500] + "..." else: version_info = "无法获取版本信息" except Exception as e2: logger.error(f"备用方法获取版本信息也失败: {str(e2)}") version_info = "无法获取版本信息" except Exception as e: logger.warning(f"获取设备详细信息时出错: {str(e)}") version_info = "获取版本信息时出错" return { "success": True, "message": f"成功连接到设备: {device.name} ({device.ip_address})", "protocol": credential.protocol.value, "prompt": prompt, "version_info": version_info } else: logger.error(f"连接设备失败: {device.name} ({device.ip_address}), 错误: {error}") return {"success": False, "message": f"连接设备失败: {error}"} @mcp.tool() async def disconnect_device(device_id: str, credential_id: str) -> Dict[str, Any]: """ 断开与网络设备的连接 Args: device_id: 设备ID credential_id: 凭据ID Returns: 断开连接结果 """ # 断开连接 success, error = await connection_manager.disconnect_device(device_id, credential_id) if success: return {"success": True, "message": "成功断开连接"} else: return {"success": False, "message": f"断开连接失败: {error}"} @mcp.tool() async def send_command( device_id: str, credential_id: str, command: str, timeout: int = 30 ) -> Dict[str, Any]: """ 向网络设备发送命令 Args: device_id: 设备ID credential_id: 凭据ID command: 要执行的命令 timeout: 命令超时时间(秒) Returns: 命令执行结果 """ # 发送命令 result, error = await connection_manager.send_command( device_id=device_id, credential_id=credential_id, command=command, timeout=timeout ) if result: return { "success": result.success, "command": result.command, "output": result.output, "error_message": result.error_message, "execution_time": result.execution_time.isoformat() } else: return { "success": False, "command": command, "output": "", "error_message": error, "execution_time": None } @mcp.tool() async def send_commands( device_id: str, credential_id: str, commands: str, timeout: int = 30 ) -> List[Dict[str, Any]]: """ 向网络设备发送多个命令 Args: device_id: 设备ID credential_id: 凭据ID commands: 要执行的命令列表,使用分号(;)分隔多个命令 timeout: 每个命令的超时时间(秒) Returns: 命令执行结果列表 """ # 将分号分隔的命令字符串转换为列表 command_list = [cmd.strip() for cmd in commands.split(';') if cmd.strip()] if not command_list: return [{"success": False, "message": "未提供有效的命令"}] # 发送命令 results, error = await connection_manager.send_commands( device_id=device_id, credential_id=credential_id, commands=command_list, timeout=timeout ) if results: return [ { "success": result.success, "command": result.command, "output": result.output, "error_message": result.error_message, "execution_time": result.execution_time.isoformat() } for result in results ] else: return [ { "success": False, "command": command, "output": "", "error_message": error, "execution_time": None } for command in command_list ] @mcp.tool() async def get_active_connections() -> List[Dict[str, Any]]: """ 获取活动连接列表 Returns: 活动连接信息列表 """ return connection_manager.get_active_connections() # 工具分类:拓扑发现 @mcp.tool() async def discover_topology(device_ids: str) -> Dict[str, Any]: """ 从指定设备开始发现网络拓扑 Args: device_ids: 设备ID列表,用逗号分隔,如"device1,device2" Returns: 拓扑发现结果 """ try: # 解析设备ID列表 if isinstance(device_ids, str): device_id_list = [device_id.strip() for device_id in device_ids.split(',') if device_id.strip()] else: device_id_list = device_ids if not device_id_list: return {"success": False, "message": "请提供至少一个设备ID"} logger.info(f"开始拓扑发现,设备列表: {device_id_list}") # 验证设备是否存在 valid_devices = [] for device_id in device_id_list: device = device_manager.get_device(device_id) if device: valid_devices.append(device_id) else: logger.warning(f"设备不存在: {device_id}") if not valid_devices: return {"success": False, "message": "没有找到有效的设备"} # 开始拓扑发现 topology = await topology_discovery.discover_topology_from_devices(valid_devices) return { "success": True, "message": f"拓扑发现完成,发现{len(topology.nodes)}个节点,{len(topology.links)}条链路", "topology": topology.to_dict() } except Exception as e: logger.error(f"拓扑发现失败: {e}") return {"success": False, "message": f"拓扑发现失败: {str(e)}"} @mcp.tool() async def get_topology() -> Dict[str, Any]: """ 获取当前网络拓扑 Returns: 当前拓扑信息 """ try: topology = topology_discovery.get_topology() return { "success": True, "topology": topology.to_dict() } except Exception as e: logger.error(f"获取拓扑失败: {e}") return {"success": False, "message": f"获取拓扑失败: {str(e)}"} @mcp.tool() async def clear_topology() -> Dict[str, Any]: """ 清空拓扑数据 Returns: 操作结果 """ try: topology_discovery.clear_topology() return {"success": True, "message": "拓扑数据已清空"} except Exception as e: logger.error(f"清空拓扑失败: {e}") return {"success": False, "message": f"清空拓扑失败: {str(e)}"} @mcp.tool() async def get_device_neighbors(device_id: str) -> Dict[str, Any]: """ 获取指定设备的邻居设备 Args: device_id: 设备ID Returns: 邻居设备列表 """ try: topology = topology_discovery.get_topology() neighbors = topology.get_device_neighbors(device_id) # 获取邻居设备的详细信息 neighbor_details = [] for neighbor_id in neighbors: device = device_manager.get_device(neighbor_id) if device: neighbor_details.append(device.to_dict()) return { "success": True, "device_id": device_id, "neighbors": neighbor_details } except Exception as e: logger.error(f"获取设备邻居失败: {e}") return {"success": False, "message": f"获取设备邻居失败: {str(e)}"} @mcp.tool() async def discover_device_neighbors(device_id: str) -> Dict[str, Any]: """ 发现单个设备的邻居(实时发现) Args: device_id: 设备ID Returns: 发现的邻居信息 """ try: device = device_manager.get_device(device_id) if not device: return {"success": False, "message": f"设备不存在: {device_id}"} logger.info(f"开始发现设备 {device.name} 的邻居") # 发现设备邻居 neighbors, interfaces = await topology_discovery.discover_device_neighbors(device) return { "success": True, "device_id": device_id, "device_name": device.name, "neighbors": neighbors, "interfaces": [iface.to_dict() for iface in interfaces], "neighbor_count": len(neighbors), "interface_count": len(interfaces) } except Exception as e: logger.error(f"发现设备邻居失败: {e}") return {"success": False, "message": f"发现设备邻居失败: {str(e)}"} @mcp.tool() async def get_topology_statistics() -> Dict[str, Any]: """ 获取拓扑统计信息 Returns: 拓扑统计数据 """ try: topology = topology_discovery.get_topology() # 统计不同协议的链路数量 protocol_stats = {} for link in topology.links: protocol = link.protocol.value protocol_stats[protocol] = protocol_stats.get(protocol, 0) + 1 # 统计不同厂商的设备数量 vendor_stats = {} for node in topology.nodes.values(): vendor = node.vendor vendor_stats[vendor] = vendor_stats.get(vendor, 0) + 1 return { "success": True, "total_nodes": len(topology.nodes), "total_links": len(topology.links), "protocol_distribution": protocol_stats, "vendor_distribution": vendor_stats, "last_discovery": topology.last_discovery.isoformat() if topology.last_discovery else None, "discovery_scope": topology.discovery_scope } except Exception as e: logger.error(f"获取拓扑统计失败: {e}") return {"success": False, "message": f"获取拓扑统计失败: {str(e)}"} # 工具分类:网络扫描 @mcp.tool() async def scan_network_range( network: str, timeout: float = 3.0, max_concurrent: int = 50, ping_enabled: bool = True, port_scan_enabled: bool = True, snmp_enabled: bool = True, auto_create_devices: bool = False ) -> Dict[str, Any]: """ 扫描指定网络范围内的活跃设备 Args: network: 网络范围,如 "192.168.1.0/24" 或 "10.0.0.0/16" timeout: 扫描超时时间(秒) max_concurrent: 最大并发扫描数 ping_enabled: 是否启用ping扫描 port_scan_enabled: 是否启用端口扫描 snmp_enabled: 是否启用SNMP扫描 auto_create_devices: 是否自动创建发现的设备 Returns: 扫描结果 """ try: from network_scanner import ScanConfiguration # 创建扫描配置 config = ScanConfiguration( timeout=timeout, max_concurrent=max_concurrent, ping_enabled=ping_enabled, port_scan_enabled=port_scan_enabled, snmp_enabled=snmp_enabled ) logger.info(f"开始网络扫描: {network}") # 执行网络扫描 scan_results = await network_scanner.scan_network_range(network, config) # 如果启用自动创建设备,则创建发现的设备 discovered_devices = [] if auto_create_devices: discovered_devices = await network_scanner.discover_devices_from_scan(scan_results, auto_create=True) return { "success": True, "message": f"网络扫描完成,发现 {len(scan_results)} 个活跃主机", "network_range": network, "alive_hosts": len(scan_results), "scan_results": [result.to_dict() for result in scan_results], "auto_created_devices": len(discovered_devices), "discovered_devices": [device.to_dict() for device in discovered_devices] } except Exception as e: logger.error(f"网络扫描失败: {e}") return {"success": False, "message": f"网络扫描失败: {str(e)}"} @mcp.tool() async def get_scan_results( ip_address: Optional[str] = None, alive_only: bool = True ) -> Dict[str, Any]: """ 获取网络扫描结果 Args: ip_address: 特定IP地址,如果不提供则返回所有结果 alive_only: 是否只返回活跃主机 Returns: 扫描结果 """ try: if ip_address: # 获取特定IP的扫描结果 result = network_scanner.scan_results.get(ip_address) if result: return { "success": True, "scan_result": result.to_dict() } else: return {"success": False, "message": f"未找到IP {ip_address} 的扫描结果"} else: # 获取所有扫描结果 results = list(network_scanner.scan_results.values()) if alive_only: results = [r for r in results if r.is_alive] return { "success": True, "total_results": len(results), "scan_results": [result.to_dict() for result in results] } except Exception as e: logger.error(f"获取扫描结果失败: {e}") return {"success": False, "message": f"获取扫描结果失败: {str(e)}"} @mcp.tool() async def get_scan_statistics() -> Dict[str, Any]: """ 获取网络扫描统计信息 Returns: 扫描统计数据 """ try: stats = network_scanner.get_scan_statistics() return { "success": True, "statistics": stats } except Exception as e: logger.error(f"获取扫描统计失败: {e}") return {"success": False, "message": f"获取扫描统计失败: {str(e)}"} @mcp.tool() async def discover_devices_from_scan_results( min_response_time: Optional[float] = None, required_ports: str = "", vendor_filter: Optional[str] = None, device_type_filter: Optional[str] = None ) -> Dict[str, Any]: """ 从扫描结果中发现并创建设备 Args: min_response_time: 最小响应时间过滤(毫秒) required_ports: 必需的开放端口,使用逗号分隔,如"22,23,161" vendor_filter: 厂商过滤,如"cisco"、"huawei" device_type_filter: 设备类型过滤,如"switch"、"router" Returns: 发现和创建的设备信息 """ try: # 获取所有活跃的扫描结果 all_results = [r for r in network_scanner.scan_results.values() if r.is_alive] # 应用过滤条件 filtered_results = [] for result in all_results: # 响应时间过滤 if min_response_time and result.response_time and result.response_time > min_response_time: continue # 端口过滤 if required_ports: required_port_list = [int(p.strip()) for p in required_ports.split(',') if p.strip().isdigit()] if not all(port in result.open_ports for port in required_port_list): continue # 厂商过滤 if vendor_filter and result.vendor: if vendor_filter.lower() not in result.vendor.lower(): continue # 设备类型过滤 if device_type_filter and result.device_type: if device_type_filter.lower() not in result.device_type.lower(): continue filtered_results.append(result) # 从过滤后的结果创建设备 discovered_devices = await network_scanner.discover_devices_from_scan(filtered_results, auto_create=True) return { "success": True, "message": f"从 {len(filtered_results)} 个扫描结果中创建了 {len(discovered_devices)} 个设备", "filtered_results_count": len(filtered_results), "created_devices_count": len(discovered_devices), "created_devices": [device.to_dict() for device in discovered_devices] } except Exception as e: logger.error(f"从扫描结果创建设备失败: {e}") return {"success": False, "message": f"从扫描结果创建设备失败: {str(e)}"} @mcp.tool() async def clear_scan_results() -> Dict[str, Any]: """ 清空网络扫描结果 Returns: 操作结果 """ try: network_scanner.clear_scan_results() return {"success": True, "message": "扫描结果已清空"} except Exception as e: logger.error(f"清空扫描结果失败: {e}") return {"success": False, "message": f"清空扫描结果失败: {str(e)}"} @mcp.tool() async def scan_single_host( ip_address: str, timeout: float = 3.0, port_scan_enabled: bool = True, snmp_enabled: bool = True ) -> Dict[str, Any]: """ 扫描单个主机 Args: ip_address: 目标IP地址 timeout: 扫描超时时间(秒) port_scan_enabled: 是否启用端口扫描 snmp_enabled: 是否启用SNMP扫描 Returns: 单个主机扫描结果 """ try: from network_scanner import ScanConfiguration # 创建扫描配置 config = ScanConfiguration( timeout=timeout, port_scan_enabled=port_scan_enabled, snmp_enabled=snmp_enabled ) logger.info(f"开始扫描单个主机: {ip_address}") # 执行单个主机扫描 scan_result = await network_scanner.scan_single_host(ip_address, config) # 保存到扫描结果 network_scanner.scan_results[ip_address] = scan_result network_scanner.save_scan_results() return { "success": True, "message": f"主机扫描完成: {ip_address}", "is_alive": scan_result.is_alive, "scan_result": scan_result.to_dict() } except Exception as e: logger.error(f"扫描单个主机失败: {e}") return {"success": False, "message": f"扫描单个主机失败: {str(e)}"} # =================== 资源工具 =================== @mcp.tool() async def list_resources() -> List[Dict[str, Any]]: """ 列出可用的MCP资源 Returns: 资源列表 """ return resource_manager.list_available_resources() @mcp.tool() async def get_resource(uri: str, use_cache: bool = True) -> Dict[str, Any]: """ 获取指定URI的资源 Args: uri: 资源URI use_cache: 是否使用缓存 Returns: 资源内容 """ return await resource_manager.get_resource(uri, use_cache) @mcp.tool() async def clear_resource_cache(uri: Optional[str] = None) -> Dict[str, Any]: """ 清除资源缓存 Args: uri: 要清除的特定资源URI,如果为None则清除所有缓存 Returns: 操作结果 """ success = resource_manager.clear_cache(uri) if success: if uri: return {"success": True, "message": f"已清除资源缓存: {uri}"} else: return {"success": True, "message": "已清除所有资源缓存"} else: return {"success": False, "message": "清除缓存失败"} # =================== 模板工具 =================== @mcp.tool() async def list_templates() -> List[Dict[str, Any]]: """ 列出可用的提示模板 Returns: 模板列表 """ return template_manager.list_templates() @mcp.tool() async def render_template( template_name: str, context: str = "{}", resource_uris: str = "" ) -> Dict[str, Any]: """ 渲染提示模板 Args: template_name: 模板名称 context: JSON格式的渲染上下文,如 {"name": "value"} resource_uris: 资源URI映射,格式为 "key1=uri1,key2=uri2" Returns: 渲染结果 """ try: # 解析context context_dict = {} if context.strip(): try: context_dict = json.loads(context) if not isinstance(context_dict, dict): context_dict = {} except json.JSONDecodeError: logger.warning(f"无法解析context JSON: {context}") return {"success": False, "message": "无效的JSON上下文格式"} # 解析resource_uris resource_dict = {} if resource_uris.strip(): uri_pairs = resource_uris.split(',') for pair in uri_pairs: if '=' in pair: key, uri = pair.split('=', 1) resource_dict[key.strip()] = uri.strip() if resource_dict: # 渲染带有资源的模板 result = await render_template_with_resources( template_name=template_name, context=context_dict, resource_uris=resource_dict, resource_manager=resource_manager ) else: # 渲染简单模板 result = template_manager.render_template(template_name, context_dict) if result: if isinstance(result, list): # 处理消息列表 messages = [ { "role": msg.role, "content": msg.content } for msg in result ] return {"success": True, "messages": messages} else: # 处理字符串结果 return {"success": True, "rendered_template": result} else: return {"success": False, "message": f"渲染模板失败: {template_name}"} except Exception as e: logger.error(f"渲染模板异常: {str(e)}") return {"success": False, "message": f"渲染模板异常: {str(e)}"} # =================== 资源注册 =================== # 将资源处理函数注册到资源管理器 @resource_manager.register_resource("greeting/{name}", ResourceType.SYSTEM) async def get_greeting(name: str) -> str: """获取个性化问候语""" return f"你好,{name}!欢迎使用NetBrain MCP!" @resource_manager.register_resource("device/{device_id}", ResourceType.DEVICE) async def get_device_resource(device_id: str) -> Dict[str, Any]: """获取设备资源""" device = device_manager.get_device(device_id) if device: return device.to_dict() return {"error": f"设备不存在: {device_id}"} @resource_manager.register_resource("device/{device_id}/config", ResourceType.CONFIG) async def get_device_config(device_id: str) -> Dict[str, Any]: """获取设备配置资源""" device = device_manager.get_device(device_id) if not device: return {"error": f"设备不存在: {device_id}"} # 这里我们会尝试获取设备的运行配置 # 首先获取默认凭据 credentials = device_manager.list_credentials() if not credentials: return {"error": "没有可用的凭据"} credential_id = device.credential_id or credentials[0].id credential = device_manager.get_credential(credential_id) # 尝试获取设备配置 if device.vendor == DeviceVendor.CISCO: command = "show running-config" elif device.vendor == DeviceVendor.HUAWEI: command = "display current-configuration" elif device.vendor == DeviceVendor.H3C: command = "display current-configuration" elif device.vendor == DeviceVendor.JUNIPER: command = "show configuration" else: command = "show running-config" # 默认命令 try: # 连接设备 success, error = await connection_manager.connect_device(device, credential) if not success: return {"error": f"连接设备失败: {error}"} # 发送命令 result, error = await connection_manager.send_command( device_id=device_id, credential_id=credential_id, command=command, timeout=60 # 配置命令可能需要更长时间 ) # 断开连接 await connection_manager.disconnect_device(device_id, credential_id) if result and result.success: return { "device_id": device_id, "device_name": device.name, "config_type": "running", "config_content": result.output, "timestamp": result.execution_time.isoformat() } else: return {"error": f"获取配置失败: {error or '未知错误'}"} except Exception as e: return {"error": f"获取配置时发生错误: {str(e)}"} @resource_manager.register_resource("device/{device_id}/interfaces", ResourceType.DEVICE) async def get_device_interfaces(device_id: str) -> Dict[str, Any]: """获取设备接口资源""" device = device_manager.get_device(device_id) if not device: return {"error": f"设备不存在: {device_id}"} # 获取默认凭据 credentials = device_manager.list_credentials() if not credentials: return {"error": "没有可用的凭据"} credential_id = device.credential_id or credentials[0].id credential = device_manager.get_credential(credential_id) # 根据设备类型确定接口命令 if device.vendor == DeviceVendor.CISCO: command = "show interfaces" elif device.vendor == DeviceVendor.HUAWEI: command = "display interface" elif device.vendor == DeviceVendor.H3C: command = "display interface" elif device.vendor == DeviceVendor.JUNIPER: command = "show interfaces detail" else: command = "show interfaces" # 默认命令 try: # 连接设备 success, error = await connection_manager.connect_device(device, credential) if not success: return {"error": f"连接设备失败: {error}"} # 发送命令 result, error = await connection_manager.send_command( device_id=device_id, credential_id=credential_id, command=command, timeout=30 ) # 断开连接 await connection_manager.disconnect_device(device_id, credential_id) if result and result.success: return { "device_id": device_id, "device_name": device.name, "interfaces_output": result.output, "timestamp": result.execution_time.isoformat() } else: return {"error": f"获取接口信息失败: {error or '未知错误'}"} except Exception as e: return {"error": f"获取接口信息时发生错误: {str(e)}"} @resource_manager.register_resource("device/{device_id}/routes", ResourceType.DEVICE) async def get_device_routes(device_id: str) -> Dict[str, Any]: """获取设备路由表资源""" device = device_manager.get_device(device_id) if not device: return {"error": f"设备不存在: {device_id}"} # 获取默认凭据 credentials = device_manager.list_credentials() if not credentials: return {"error": "没有可用的凭据"} credential_id = device.credential_id or credentials[0].id credential = device_manager.get_credential(credential_id) # 根据设备类型确定路由命令 if device.vendor == DeviceVendor.CISCO: command = "show ip route" elif device.vendor == DeviceVendor.HUAWEI: command = "display ip routing-table" elif device.vendor == DeviceVendor.H3C: command = "display ip routing-table" elif device.vendor == DeviceVendor.JUNIPER: command = "show route" else: command = "show ip route" # 默认命令 try: # 连接设备 success, error = await connection_manager.connect_device(device, credential) if not success: return {"error": f"连接设备失败: {error}"} # 发送命令 result, error = await connection_manager.send_command( device_id=device_id, credential_id=credential_id, command=command, timeout=30 ) # 断开连接 await connection_manager.disconnect_device(device_id, credential_id) if result and result.success: return { "device_id": device_id, "device_name": device.name, "routes_output": result.output, "timestamp": result.execution_time.isoformat() } else: return {"error": f"获取路由表失败: {error or '未知错误'}"} except Exception as e: return {"error": f"获取路由表时发生错误: {str(e)}"} @resource_manager.register_resource("credentials", ResourceType.CREDENTIAL) async def list_credentials_resource() -> Dict[str, Any]: """ 列出所有设备凭据 Returns: 凭据列表 """ credentials = device_manager.list_credentials() return { "credentials": [ { "id": cred.id, "name": cred.name, "username": cred.username, "password": cred.password, "protocol": cred.protocol.value, "port": cred.port, "ssh_key_file": cred.ssh_key_file } for cred in credentials ] } @resource_manager.register_resource("system/status", ResourceType.SYSTEM) async def get_system_status() -> Dict[str, Any]: """获取系统状态资源""" devices_count = len(device_manager.list_devices()) credentials_count = len(device_manager.list_credentials()) active_connections = len(connection_manager.get_active_connections()) return { "status": "running", "devices_count": devices_count, "credentials_count": credentials_count, "active_connections": active_connections, "version": "0.1.0" } @resource_manager.register_resource("topology", ResourceType.TOPOLOGY) async def get_topology_resource() -> Dict[str, Any]: """获取网络拓扑资源""" try: topology = topology_discovery.get_topology() return topology.to_dict() except Exception as e: logger.error(f"获取拓扑资源失败: {e}") return {"error": f"获取拓扑资源失败: {str(e)}"} @resource_manager.register_resource("topology/statistics", ResourceType.TOPOLOGY) async def get_topology_statistics_resource() -> Dict[str, Any]: """获取拓扑统计资源""" try: topology = topology_discovery.get_topology() # 统计不同协议的链路数量 protocol_stats = {} for link in topology.links: protocol = link.protocol.value protocol_stats[protocol] = protocol_stats.get(protocol, 0) + 1 # 统计不同厂商的设备数量 vendor_stats = {} for node in topology.nodes.values(): vendor = node.vendor vendor_stats[vendor] = vendor_stats.get(vendor, 0) + 1 return { "total_nodes": len(topology.nodes), "total_links": len(topology.links), "protocol_distribution": protocol_stats, "vendor_distribution": vendor_stats, "last_discovery": topology.last_discovery.isoformat() if topology.last_discovery else None, "discovery_scope": topology.discovery_scope } except Exception as e: logger.error(f"获取拓扑统计资源失败: {e}") return {"error": f"获取拓扑统计资源失败: {str(e)}"} @resource_manager.register_resource("device/{device_id}/neighbors", ResourceType.TOPOLOGY) async def get_device_neighbors_resource(device_id: str) -> Dict[str, Any]: """获取设备邻居资源""" try: topology = topology_discovery.get_topology() neighbors = topology.get_device_neighbors(device_id) # 获取邻居设备的详细信息 neighbor_details = [] for neighbor_id in neighbors: device = device_manager.get_device(neighbor_id) if device: neighbor_details.append(device.to_dict()) return { "device_id": device_id, "neighbors": neighbor_details, "neighbor_count": len(neighbor_details) } except Exception as e: logger.error(f"获取设备邻居资源失败: {e}") return {"error": f"获取设备邻居资源失败: {str(e)}"} @resource_manager.register_resource("scan/results", ResourceType.SCAN) async def get_scan_results_resource() -> Dict[str, Any]: """获取网络扫描结果资源""" try: results = [r for r in network_scanner.scan_results.values() if r.is_alive] return { "total_results": len(results), "scan_results": [result.to_dict() for result in results] } except Exception as e: logger.error(f"获取扫描结果资源失败: {e}") return {"error": f"获取扫描结果资源失败: {str(e)}"} @resource_manager.register_resource("scan/statistics", ResourceType.SCAN) async def get_scan_statistics_resource() -> Dict[str, Any]: """获取网络扫描统计资源""" try: stats = network_scanner.get_scan_statistics() return stats except Exception as e: logger.error(f"获取扫描统计资源失败: {e}") return {"error": f"获取扫描统计资源失败: {str(e)}"} @resource_manager.register_resource("scan/result/{ip_address}", ResourceType.SCAN) async def get_scan_result_resource(ip_address: str) -> Dict[str, Any]: """获取特定IP的扫描结果资源""" try: result = network_scanner.scan_results.get(ip_address) if result: return result.to_dict() else: return {"error": f"未找到IP {ip_address} 的扫描结果"} except Exception as e: logger.error(f"获取扫描结果资源失败: {e}") return {"error": f"获取扫描结果资源失败: {str(e)}"} # 向MCP服务器注册资源获取方法 @mcp.resource("{uri}") async def mcp_resource_handler(uri: str) -> Any: """MCP资源处理器,处理所有资源请求""" logger.info(f"处理资源请求: {uri}") return await resource_manager.get_resource(uri) # 向MCP服务器注册提示模板 @mcp.prompt("{name}") async def mcp_prompt_handler(name: str) -> str: """MCP提示模板处理器,处理所有模板请求""" logger.info(f"处理模板请求: {name}") template = template_manager.get_template(name) if template: # 返回模板描述或模板名称 return template.description or template.name return f"错误: 未找到模板 '{name}'" # 测试工具 @mcp.tool() async def test_scrapli_connection( host: str, username: str, password: str = None, platform: str = "cisco_iosxe", port: int = 22, protocol: str = "ssh", ssh_key_file: Optional[str] = None, connect_timeout: int = 15 ) -> Dict[str, Any]: """ 测试Scrapli连接(直接使用Scrapli) Args: host: 设备主机名或IP地址 username: 用户名 password: 密码(与ssh_key_file至少提供一个) platform: 平台类型,如cisco_iosxe, huawei_vrp等 port: 端口号(默认22,Telnet使用23) protocol: 连接协议,ssh或telnet ssh_key_file: SSH密钥文件路径,可选 connect_timeout: 连接超时时间(秒) Returns: 连接测试结果 """ # 从device_connector中导入SCRAPLI_IMPORT_SUCCESS检查scrapli是否可用 if not SCRAPLI_IMPORT_SUCCESS: return { "success": False, "message": "Scrapli库导入失败,请确保已安装: pip install scrapli scrapli-community" } try: from scrapli import Scrapli # 修正平台名称处理 if platform.lower() == "huawei": platform = "huawei_vrp" # 确定连接协议 is_telnet = protocol.lower() == "telnet" if is_telnet and port == 22: # 如果是Telnet但端口仍为默认SSH端口,修正为默认Telnet端口 port = 23 logger.info(f"连接参数: host={host}, username={username}, platform={platform}, port={port}, protocol={protocol}") # 打印密码信息(安全地) if password: logger.info(f"密码类型: {type(password)}, 长度: {len(str(password))}") password = str(password) # 确保密码是字符串类型 # 准备连接参数 device_params = { "host": host, "auth_username": username, "auth_strict_key": False, "platform": platform, "port": port, "timeout_socket": connect_timeout, "timeout_transport": connect_timeout * 2, } # 根据协议设置传输方式 if is_telnet: device_params["transport"] = "telnet" logger.info("使用Telnet连接") else: # 指定Windows兼容的传输方式 device_params["transport"] = "paramiko" # 在Windows上使用paramiko而不是system logger.info("使用SSH连接 (paramiko)") # 设置认证方式,优先使用SSH密钥 if not is_telnet and ssh_key_file and os.path.exists(ssh_key_file): device_params["auth_private_key"] = ssh_key_file logger.info(f"使用SSH密钥认证: {ssh_key_file}") elif password: device_params["auth_password"] = password logger.info("使用密码认证") else: return {"success": False, "message": "必须提供密码或SSH密钥文件"} # 详细记录连接参数(移除敏感信息) safe_params = device_params.copy() if "auth_password" in safe_params: safe_params["auth_password"] = "******" if "auth_private_key" in safe_params: safe_params["auth_private_key"] = f"[使用密钥文件: {ssh_key_file}]" logger.info(f"Scrapli测试连接参数: {safe_params}") # 建立连接 conn = Scrapli(**device_params) conn.open() # 获取设备提示符 prompt = conn.get_prompt() # 测试发送命令 try: # 根据平台类型选择命令 if "huawei" in platform: command = "display version" elif "cisco" in platform: command = "show version" else: command = "show version" resp = conn.send_command(command) version_info = resp.result[:200] + "..." if len(resp.result) > 200 else resp.result except Exception as e: logger.warning(f"获取版本信息失败: {str(e)}") version_info = "无法获取版本信息" # 关闭连接 conn.close() return { "success": True, "message": f"连接成功!设备提示符: {prompt}", "prompt": prompt, "version_info": version_info } except ImportError as e: return {"success": False, "message": f"Scrapli库未安装: {str(e)}"} except Exception as e: error_type = type(e).__name__ error_msg = str(e) logger.error(f"Scrapli连接测试失败: {error_type} - {error_msg}") return { "success": False, "message": f"连接失败: {error_type} - {error_msg}", "error_type": error_type, "error_details": error_msg } @mcp.tool() async def test_telnet_connection( host: str, username: str, password: str, platform: str = "cisco_iosxe", port: int = 23, connect_timeout: int = 15 ) -> Dict[str, Any]: """ 测试Telnet连接(快速测试) Args: host: 设备主机名或IP地址 username: 用户名 password: 密码 platform: 平台类型,如cisco_iosxe, huawei_vrp等 port: 端口号(默认23) connect_timeout: 连接超时时间(秒) Returns: 连接测试结果 """ # 直接调用test_scrapli_connection,但固定protocol为telnet return await test_scrapli_connection( host=host, username=username, password=password, platform=platform, port=port, protocol="telnet", connect_timeout=connect_timeout ) @mcp.tool() async def send_telnet_command( device_id: str, credential_id: str, command: str, timeout: int = 60 ) -> Dict[str, Any]: """ 向Telnet连接的网络设备发送命令(使用更长的超时时间) Args: device_id: 设备ID credential_id: 凭据ID command: 要执行的命令 timeout: 命令超时时间(秒,默认比普通命令更长) Returns: 命令执行结果 """ # 获取设备和凭据 device = device_manager.get_device(device_id) credential = device_manager.get_credential(credential_id) if not device: return {"success": False, "message": f"设备不存在: {device_id}"} if not credential: return {"success": False, "message": f"凭据不存在: {credential_id}"} # 验证是否为Telnet凭据 if credential.protocol != ConnectionProtocol.TELNET: return {"success": False, "message": f"此函数仅支持Telnet连接,当前凭据协议为: {credential.protocol.value}"} # 记录命令执行信息 logger.info(f"通过Telnet向设备 {device.name} ({device.ip_address}) 发送命令: {command}") logger.info(f"超时设置: {timeout}秒") # 发送命令 result, error = await connection_manager.send_command( device_id=device_id, credential_id=credential_id, command=command, timeout=timeout ) if result: # 记录执行结果 if result.success: logger.info(f"命令执行成功,输出长度: {len(result.output)}") else: logger.warning(f"命令执行失败: {result.error_message}") return { "success": result.success, "command": result.command, "output": result.output, "error_message": result.error_message, "execution_time": result.execution_time.isoformat() } else: logger.error(f"执行命令失败: {error}") return { "success": False, "command": command, "output": "", "error_message": error, "execution_time": None } # 运行服务器 if __name__ == "__main__": # 检测启动方式 import os # 获取启动命令行参数 import sys command_args = " ".join(sys.argv) # 检查环境变量或命令行,判断是否通过MCP CLI运行 is_mcp_cli = os.environ.get("MCP_CLI_RUN", "0") == "1" or "mcp dev" in command_args or "mcp run" in command_args # 设置环境变量,使其他模块也能知道是通过MCP CLI运行 if is_mcp_cli: os.environ["MCP_CLI_RUN"] = "1" # 始终启动Web服务器(后台运行) import threading import uvicorn def start_web_server(): """启动Web服务器""" from web.app import app uvicorn.run(app, host="0.0.0.0", port=8088) # 在另一个线程中启动Web服务器 web_thread = threading.Thread(target=start_web_server, daemon=True) web_thread.start() print("NetBrain MCP Web界面已启动,访问 http://localhost:8088") if is_mcp_cli: print("MCP CLI模式已检测到") # 在8000端口运行,与MCP Inspector预期一致 mcp.run()