| 1 | from enum import Enum |
| 2 | from typing import Dict |
| 3 | from typing import Optional |
| 4 | |
| 5 | import httpx |
| 6 | from loguru import logger |
| 7 | |
| 8 | from app.integrations.copilot_mcp.schema.copilot_mcp import MCPQueryRequest |
| 9 | from app.integrations.copilot_mcp.schema.copilot_mcp import MCPQueryResponse |
| 10 | from app.integrations.copilot_mcp.schema.copilot_mcp import MCPServerType |
| 11 | |
| 12 | |
| 13 | class MCPServiceType(str, Enum): |
| 14 | """Enumeration of MCP service deployment types""" |
| 15 | |
| 16 | LOCAL = "local" |
| 17 | CLOUD = "cloud" |
| 18 | |
| 19 | |
| 20 | class MCPServerConfig: |
| 21 | """Configuration for MCP server endpoints""" |
| 22 | |
| 23 | def __init__(self, service_type: MCPServiceType, endpoint: str): |
| 24 | self.service_type = service_type |
| 25 | self.endpoint = endpoint |
| 26 | |
| 27 | |
| 28 | class MCPService: |
| 29 | """Service for handling MCP queries with modular server routing""" |
| 30 | |
| 31 | # Base URLs for different service types |
| 32 | _BASE_URLS = { |
| 33 | MCPServiceType.LOCAL: "http://copilot-mcp/mcp", |
| 34 | MCPServiceType.CLOUD: "https://mcp.socfortress.co/query", |
| 35 | } |
| 36 | |
| 37 | # Define the mapping of MCP server types to their configurations |
| 38 | _SERVER_CONFIGS: Dict[MCPServerType, MCPServerConfig] = { |
| 39 | # Local services |
| 40 | MCPServerType.WAZUH_INDEXER: MCPServerConfig(MCPServiceType.LOCAL, "opensearch-query"), |
| 41 | MCPServerType.WAZUH_MANAGER: MCPServerConfig(MCPServiceType.LOCAL, "wazuh-query"), |
| 42 | MCPServerType.COPILOT: MCPServerConfig(MCPServiceType.LOCAL, "mysql-query"), |
| 43 | MCPServerType.VELOCIRAPTOR: MCPServerConfig(MCPServiceType.LOCAL, "velociraptor-query"), |
| 44 | # Cloud services |
| 45 | MCPServerType.THREAT_INTEL: MCPServerConfig(MCPServiceType.CLOUD, "threat_intel"), |
| 46 | MCPServerType.CYBER_NEWS: MCPServerConfig(MCPServiceType.CLOUD, "cyber_news"), |
| 47 | MCPServerType.KNOWLEDGEBASE: MCPServerConfig(MCPServiceType.CLOUD, "knowledgebase"), |
| 48 | MCPServerType.ATTACK_SURFACE: MCPServerConfig(MCPServiceType.CLOUD, "attack_surface"), |
| 49 | } |
| 50 | |
| 51 | @classmethod |
| 52 | def get_server_config(cls, mcp_server: MCPServerType) -> MCPServerConfig: |
| 53 | """ |
| 54 | Get the configuration for a specific MCP server type. |
| 55 | |
| 56 | Args: |
| 57 | mcp_server: The MCP server type |
| 58 | |
| 59 | Returns: |
| 60 | MCPServerConfig: The configuration for the server |
| 61 | |
| 62 | Raises: |
| 63 | ValueError: If the server type is not supported |
| 64 | """ |
| 65 | config = cls._SERVER_CONFIGS.get(mcp_server) |
| 66 | if not config: |
| 67 | raise ValueError(f"Unsupported MCP server type: {mcp_server}") |
| 68 | return config |
| 69 | |
| 70 | @classmethod |
| 71 | def build_full_url(cls, mcp_server: MCPServerType) -> str: |
| 72 | """ |
| 73 | Build the full URL for a specific MCP server type. |
| 74 | |
| 75 | Args: |
| 76 | mcp_server: The MCP server type |
| 77 | |
| 78 | Returns: |
| 79 | str: The full URL for the server endpoint |
| 80 | """ |
| 81 | config = cls.get_server_config(mcp_server) |
| 82 | base_url = cls._BASE_URLS[config.service_type] |
| 83 | return f"{base_url}/{config.endpoint}" |
| 84 | |
| 85 | @classmethod |
| 86 | def is_cloud_service(cls, mcp_server: MCPServerType) -> bool: |
| 87 | """ |
| 88 | Check if the MCP server is a cloud service. |
| 89 | |
| 90 | Args: |
| 91 | mcp_server: The MCP server type |
| 92 | |
| 93 | Returns: |
| 94 | bool: True if it's a cloud service, False if local |
| 95 | """ |
| 96 | config = cls.get_server_config(mcp_server) |
| 97 | return config.service_type == MCPServiceType.CLOUD |
| 98 | |
| 99 | @classmethod |
| 100 | async def execute_query(cls, data: MCPQueryRequest, license_key: Optional[str] = None) -> MCPQueryResponse: |
| 101 | """ |
| 102 | Execute a query on the appropriate MCP server based on the request. |
| 103 | |
| 104 | Args: |
| 105 | data: The MCP query request containing the server type and query |
| 106 | license_key: Optional license key for cloud services authentication |
| 107 | |
| 108 | Returns: |
| 109 | MCPQueryResponse: The response from the MCP server |
| 110 | |
| 111 | Raises: |
| 112 | httpx.HTTPError: If the HTTP request fails |
| 113 | ValueError: If the server type is not supported |
| 114 | """ |
| 115 | try: |
| 116 | # Get the full URL for the specified server |
| 117 | full_url = cls.build_full_url(data.mcp_server) |
| 118 | is_cloud = cls.is_cloud_service(data.mcp_server) |
| 119 | |
| 120 | logger.info(f"Sending MCP query to {data.mcp_server.value} ({'cloud' if is_cloud else 'local'}) at {full_url}") |
| 121 | logger.debug(f"Query data: {data.model_dump()}") |
| 122 | |
| 123 | # Set different timeout for cloud vs local services |
| 124 | timeout = 300 if is_cloud else 300 |
| 125 | |
| 126 | # Prepare headers |
| 127 | headers = {"Content-Type": "application/json"} |
| 128 | |
| 129 | # Add license key as x-api-key header for cloud services |
| 130 | if is_cloud and license_key: |
| 131 | headers["x-api-key"] = license_key |
| 132 | logger.debug("Added x-api-key header for cloud service") |
| 133 | elif is_cloud and not license_key: |
| 134 | logger.warning(f"No license key provided for cloud service {data.mcp_server.value}") |
| 135 | |
| 136 | async with httpx.AsyncClient() as client: |
| 137 | response = await client.post( |
| 138 | full_url, |
| 139 | json=data.model_dump(), |
| 140 | headers=headers, |
| 141 | timeout=timeout, |
| 142 | ) |
| 143 | |
| 144 | # Raise an exception for HTTP error status codes |
| 145 | response.raise_for_status() |
| 146 | |
| 147 | logger.info(f"Successfully received response from {data.mcp_server.value}: {response.json()}") |
| 148 | return MCPQueryResponse(**response.json()) |
| 149 | |
| 150 | except ValueError as e: |
| 151 | logger.error(f"Invalid server type: {str(e)}") |
| 152 | return MCPQueryResponse(message=f"Error: {str(e)}", success=False, result=None, structured_result=None, execution_time=0.0) |
| 153 | |
| 154 | except httpx.HTTPError as e: |
| 155 | logger.error(f"HTTP error when querying {data.mcp_server.value}: {str(e)}") |
| 156 | return MCPQueryResponse( |
| 157 | message=f"HTTP error when querying {data.mcp_server.value}: {str(e)}", |
| 158 | success=False, |
| 159 | result=None, |
| 160 | structured_result=None, |
| 161 | execution_time=0.0, |
| 162 | ) |
| 163 | |
| 164 | except Exception as e: |
| 165 | logger.error(f"Unexpected error when querying {data.mcp_server.value}: {str(e)}") |
| 166 | return MCPQueryResponse( |
| 167 | message=f"Unexpected error: {str(e)}", |
| 168 | success=False, |
| 169 | result=None, |
| 170 | structured_result=None, |
| 171 | execution_time=0.0, |
| 172 | ) |
| 173 | |
| 174 | @classmethod |
| 175 | def add_local_service(cls, server_type: MCPServerType, endpoint: str) -> None: |
| 176 | """ |
| 177 | Add a new local MCP service. |
| 178 | |
| 179 | Args: |
| 180 | server_type: The MCP server type enum |
| 181 | endpoint: The endpoint path for the local service |
| 182 | """ |
| 183 | cls._SERVER_CONFIGS[server_type] = MCPServerConfig(MCPServiceType.LOCAL, endpoint) |
| 184 | |
| 185 | @classmethod |
| 186 | def add_cloud_service(cls, server_type: MCPServerType, endpoint: str) -> None: |
| 187 | """ |
| 188 | Add a new cloud MCP service. |
| 189 | |
| 190 | Args: |
| 191 | server_type: The MCP server type enum |
| 192 | endpoint: The endpoint path for the cloud service |
| 193 | """ |
| 194 | cls._SERVER_CONFIGS[server_type] = MCPServerConfig(MCPServiceType.CLOUD, endpoint) |
| 195 | |
| 196 | @classmethod |
| 197 | def get_service_info(cls) -> Dict[str, Dict[str, str]]: |
| 198 | """ |
| 199 | Get information about all configured services. |
| 200 | |
| 201 | Returns: |
| 202 | Dict containing service information grouped by type |
| 203 | """ |
| 204 | local_services = {} |
| 205 | cloud_services = {} |
| 206 | |
| 207 | for server_type, config in cls._SERVER_CONFIGS.items(): |
| 208 | service_info = {"endpoint": config.endpoint, "full_url": cls.build_full_url(server_type)} |
| 209 | |
| 210 | if config.service_type == MCPServiceType.LOCAL: |
| 211 | local_services[server_type.value] = service_info |
| 212 | else: |
| 213 | cloud_services[server_type.value] = service_info |
| 214 | |
| 215 | return {"local": local_services, "cloud": cloud_services} |
| 216 | |
| 217 | |
| 218 | # Convenience function to maintain backward compatibility |
| 219 | async def post_to_copilot_mcp(data: MCPQueryRequest) -> MCPQueryResponse: |
| 220 | """ |
| 221 | Send a POST request to the appropriate copilot-mcp endpoint based on server type. |
| 222 | |
| 223 | This function maintains backward compatibility while using the new modular service. |
| 224 | |
| 225 | Args: |
| 226 | data: The MCP query request |
| 227 | |
| 228 | Returns: |
| 229 | MCPQueryResponse: The response from the MCP server |
| 230 | """ |
| 231 | return await MCPService.execute_query(data) |