| """ |
| MCP Service Client for HTTP-based microservice communication |
| |
| This module provides HTTP clients for the MCP service to communicate with |
| other services (API and Agents) instead of importing their modules directly. |
| """ |
|
|
| import uuid |
| from typing import Any |
| from urllib.parse import urljoin |
|
|
| import httpx |
|
|
| from ..config.logfire_config import mcp_logger |
| from ..config.service_discovery import get_agents_url, get_api_url |
|
|
|
|
| class MCPServiceClient: |
| """ |
| Client for MCP service to communicate with other microservices via HTTP. |
| Replaces direct module imports with proper service-to-service communication. |
| """ |
|
|
| def __init__(self): |
| self.api_url = get_api_url() |
| self.agents_url = get_agents_url() |
| self.service_auth = "mcp-service-key" |
| self.timeout = httpx.Timeout( |
| connect=5.0, |
| read=300.0, |
| write=30.0, |
| pool=5.0, |
| ) |
|
|
| def _get_headers(self, request_id: str | None = None) -> dict[str, str]: |
| """Get common headers for internal requests""" |
| headers = {"X-Service-Auth": self.service_auth, "Content-Type": "application/json"} |
| if request_id: |
| headers["X-Request-ID"] = request_id |
| else: |
| headers["X-Request-ID"] = str(uuid.uuid4()) |
| return headers |
|
|
| async def crawl_url(self, url: str, options: dict[str, Any] | None = None) -> dict[str, Any]: |
| """ |
| Crawl a URL by calling the API service's knowledge-items/crawl endpoint. |
| Transforms MCP's simple format to the API's KnowledgeItemRequest format. |
| |
| Args: |
| url: URL to crawl |
| options: Crawling options (max_depth, chunk_size, smart_crawl) |
| |
| Returns: |
| Crawl response with success status and results |
| """ |
| endpoint = urljoin(self.api_url, "/api/knowledge-items/crawl") |
|
|
| |
| request_data = { |
| "url": url, |
| "knowledge_type": "documentation", |
| "tags": [], |
| "update_frequency": 7, |
| "metadata": options or {}, |
| } |
|
|
| mcp_logger.info(f"Calling API service to crawl {url}") |
|
|
| try: |
| async with httpx.AsyncClient(timeout=self.timeout) as client: |
| response = await client.post(endpoint, json=request_data, headers=self._get_headers()) |
| response.raise_for_status() |
| result = response.json() |
|
|
| |
| return { |
| "success": result.get("success", False), |
| "progressId": result.get("progressId"), |
| "message": result.get("message", "Crawling started"), |
| "error": None if result.get("success") else {"message": "Crawl failed"}, |
| } |
| except httpx.TimeoutException: |
| mcp_logger.error(f"Timeout crawling {url}") |
| return { |
| "success": False, |
| "error": {"code": "TIMEOUT", "message": "Crawl operation timed out"}, |
| } |
| except httpx.HTTPStatusError as e: |
| mcp_logger.error(f"HTTP error crawling {url}: {e.response.status_code}") |
| return {"success": False, "error": {"code": "HTTP_ERROR", "message": str(e)}} |
| except Exception as e: |
| mcp_logger.error(f"Error crawling {url}: {str(e)}") |
| return {"success": False, "error": {"code": "CRAWL_FAILED", "message": str(e)}} |
|
|
| async def search( |
| self, |
| query: str, |
| source_filter: str | None = None, |
| match_count: int = 5, |
| use_reranking: bool = False, |
| ) -> dict[str, Any]: |
| """ |
| Perform a search by calling the API service's rag/query endpoint. |
| Transforms MCP's simple format to the API's RagQueryRequest format. |
| |
| Args: |
| query: Search query |
| source_filter: Optional source ID to filter results |
| match_count: Number of results to return |
| use_reranking: Whether to rerank results (handled in Server's service layer) |
| |
| Returns: |
| Search response with results |
| """ |
| endpoint = urljoin(self.api_url, "/api/rag/query") |
| request_data = {"query": query, "source": source_filter, "match_count": match_count} |
|
|
| mcp_logger.info(f"Calling API service to search: {query}") |
|
|
| try: |
| async with httpx.AsyncClient(timeout=self.timeout) as client: |
| |
| response = await client.post(endpoint, json=request_data, headers=self._get_headers()) |
| response.raise_for_status() |
| result = response.json() |
|
|
| |
| return { |
| "success": result.get("success", True), |
| "results": result.get("results", []), |
| "reranked": False, |
| "error": None, |
| } |
|
|
| except Exception as e: |
| mcp_logger.error(f"Error searching: {str(e)}") |
| return { |
| "success": False, |
| "results": [], |
| "error": {"code": "SEARCH_FAILED", "message": str(e)}, |
| } |
|
|
| |
|
|
| async def store_documents( |
| self, documents: list[dict[str, Any]], generate_embeddings: bool = True |
| ) -> dict[str, Any]: |
| """ |
| Store documents by transforming them into the format expected by the API. |
| Note: The regular API expects file uploads, so this is a simplified version. |
| |
| Args: |
| documents: List of documents to store |
| generate_embeddings: Whether to generate embeddings |
| |
| Returns: |
| Storage response |
| """ |
| |
| |
| mcp_logger.info("Document storage through regular API not yet implemented") |
| return { |
| "success": True, |
| "documents_stored": len(documents), |
| "chunks_created": len(documents), |
| "message": "Document storage should be handled by Server's service layer", |
| } |
|
|
| async def generate_embeddings(self, texts: list[str], model: str = "text-embedding-3-small") -> dict[str, Any]: |
| """ |
| Generate embeddings - this should be handled by Server's service layer. |
| MCP tools shouldn't need to directly generate embeddings. |
| |
| Args: |
| texts: List of texts to embed |
| model: Embedding model to use |
| |
| Returns: |
| Embeddings response |
| """ |
| mcp_logger.warning("Direct embedding generation not needed for MCP tools") |
| raise NotImplementedError("Embeddings should be handled by Server's service layer") |
|
|
| |
|
|
| async def health_check(self) -> dict[str, Any]: |
| """ |
| Check health of all dependent services. |
| |
| Returns: |
| Combined health status |
| """ |
| health_status = {"api_service": False, "agents_service": False} |
|
|
| |
| api_health_url = urljoin(self.api_url, "/api/health") |
| try: |
| async with httpx.AsyncClient(timeout=httpx.Timeout(5.0)) as client: |
| mcp_logger.info(f"Checking API service health at: {api_health_url}") |
| response = await client.get(api_health_url) |
| health_status["api_service"] = response.status_code == 200 |
| mcp_logger.info(f"API service health check: {response.status_code}") |
| except Exception as e: |
| health_status["api_service"] = False |
| mcp_logger.warning(f"API service health check failed: {e}") |
|
|
| |
| try: |
| async with httpx.AsyncClient(timeout=httpx.Timeout(5.0)) as client: |
| response = await client.get(urljoin(self.agents_url, "/health")) |
| health_status["agents_service"] = response.status_code == 200 |
| except Exception: |
| pass |
|
|
| return health_status |
|
|
|
|
| |
| _mcp_client = None |
|
|
|
|
| def get_mcp_service_client() -> MCPServiceClient: |
| """Get or create the global MCP service client""" |
| global _mcp_client |
| if _mcp_client is None: |
| _mcp_client = MCPServiceClient() |
| return _mcp_client |
|
|