|
| 1 | +"""MCP Gateway router for RAG Modulo API. |
| 2 | +
|
| 3 | +This module provides FastAPI router endpoints for MCP (Model Context Protocol) |
| 4 | +Gateway integration, enabling tool discovery and invocation capabilities. |
| 5 | +
|
| 6 | +API Endpoints: |
| 7 | +- GET /api/v1/mcp/tools - List available MCP tools |
| 8 | +- POST /api/v1/mcp/tools/{name}/invoke - Invoke a specific MCP tool |
| 9 | +- GET /api/v1/mcp/health - Check MCP gateway health |
| 10 | +""" |
| 11 | + |
| 12 | +from typing import Annotated |
| 13 | + |
| 14 | +from fastapi import APIRouter, Depends, HTTPException, status |
| 15 | + |
| 16 | +from core.config import Settings, get_settings |
| 17 | +from core.logging_utils import get_logger |
| 18 | +from rag_solution.core.dependencies import get_current_user |
| 19 | +from rag_solution.schemas.mcp_schema import ( |
| 20 | + MCPHealthStatus, |
| 21 | + MCPInvocationInput, |
| 22 | + MCPInvocationOutput, |
| 23 | + MCPInvocationStatus, |
| 24 | + MCPToolsResponse, |
| 25 | +) |
| 26 | +from rag_solution.services.mcp_gateway_client import ResilientMCPGatewayClient |
| 27 | + |
| 28 | +logger = get_logger(__name__) |
| 29 | + |
| 30 | +router = APIRouter(prefix="/api/v1/mcp", tags=["mcp"]) |
| 31 | + |
| 32 | + |
| 33 | +def get_mcp_client( |
| 34 | + settings: Annotated[Settings, Depends(get_settings)], |
| 35 | +) -> ResilientMCPGatewayClient: |
| 36 | + """Dependency to create MCP gateway client. |
| 37 | +
|
| 38 | + Args: |
| 39 | + settings: Application settings from dependency injection |
| 40 | +
|
| 41 | + Returns: |
| 42 | + ResilientMCPGatewayClient: Initialized MCP client instance |
| 43 | + """ |
| 44 | + return ResilientMCPGatewayClient(settings) |
| 45 | + |
| 46 | + |
| 47 | +@router.get( |
| 48 | + "/health", |
| 49 | + response_model=MCPHealthStatus, |
| 50 | + summary="Check MCP gateway health", |
| 51 | + description="Perform a health check on the MCP Context Forge gateway", |
| 52 | + responses={ |
| 53 | + 200: {"description": "Health check completed (see healthy field for status)"}, |
| 54 | + 503: {"description": "MCP integration is disabled"}, |
| 55 | + }, |
| 56 | +) |
| 57 | +async def mcp_health( |
| 58 | + settings: Annotated[Settings, Depends(get_settings)], |
| 59 | + mcp_client: Annotated[ResilientMCPGatewayClient, Depends(get_mcp_client)], |
| 60 | +) -> MCPHealthStatus: |
| 61 | + """Check MCP gateway health status. |
| 62 | +
|
| 63 | + Returns health information including: |
| 64 | + - Gateway availability |
| 65 | + - Latency |
| 66 | + - Circuit breaker state |
| 67 | +
|
| 68 | + Args: |
| 69 | + settings: Application settings |
| 70 | + mcp_client: MCP gateway client |
| 71 | +
|
| 72 | + Returns: |
| 73 | + MCPHealthStatus: Health status information |
| 74 | + """ |
| 75 | + if not settings.mcp_enabled: |
| 76 | + raise HTTPException( |
| 77 | + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, |
| 78 | + detail="MCP integration is disabled", |
| 79 | + ) |
| 80 | + |
| 81 | + return await mcp_client.check_health() |
| 82 | + |
| 83 | + |
| 84 | +@router.get( |
| 85 | + "/tools", |
| 86 | + response_model=MCPToolsResponse, |
| 87 | + summary="List available MCP tools", |
| 88 | + description="Retrieve a list of all available MCP tools from the gateway", |
| 89 | + responses={ |
| 90 | + 200: {"description": "List of available MCP tools"}, |
| 91 | + 503: {"description": "MCP integration is disabled or gateway unavailable"}, |
| 92 | + }, |
| 93 | +) |
| 94 | +async def list_tools( |
| 95 | + current_user: Annotated[dict, Depends(get_current_user)], |
| 96 | + settings: Annotated[Settings, Depends(get_settings)], |
| 97 | + mcp_client: Annotated[ResilientMCPGatewayClient, Depends(get_mcp_client)], |
| 98 | +) -> MCPToolsResponse: |
| 99 | + """List all available MCP tools. |
| 100 | +
|
| 101 | + Returns tools available for invocation, including their: |
| 102 | + - Name and description |
| 103 | + - Input parameters |
| 104 | + - Category and version |
| 105 | +
|
| 106 | + SECURITY: Requires authentication. |
| 107 | +
|
| 108 | + Args: |
| 109 | + current_user: Authenticated user from JWT token |
| 110 | + settings: Application settings |
| 111 | + mcp_client: MCP gateway client |
| 112 | +
|
| 113 | + Returns: |
| 114 | + MCPToolsResponse: List of available tools |
| 115 | +
|
| 116 | + Raises: |
| 117 | + HTTPException: If MCP is disabled or gateway unavailable |
| 118 | + """ |
| 119 | + if not settings.mcp_enabled: |
| 120 | + raise HTTPException( |
| 121 | + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, |
| 122 | + detail="MCP integration is disabled", |
| 123 | + ) |
| 124 | + |
| 125 | + logger.info( |
| 126 | + "Listing MCP tools", |
| 127 | + extra={ |
| 128 | + "user_id": current_user.get("uuid"), |
| 129 | + }, |
| 130 | + ) |
| 131 | + |
| 132 | + response = await mcp_client.list_tools() |
| 133 | + |
| 134 | + if not response.gateway_healthy: |
| 135 | + logger.warning( |
| 136 | + "MCP gateway unhealthy when listing tools", |
| 137 | + extra={ |
| 138 | + "user_id": current_user.get("uuid"), |
| 139 | + }, |
| 140 | + ) |
| 141 | + |
| 142 | + return response |
| 143 | + |
| 144 | + |
| 145 | +@router.post( |
| 146 | + "/tools/{tool_name}/invoke", |
| 147 | + response_model=MCPInvocationOutput, |
| 148 | + summary="Invoke an MCP tool", |
| 149 | + description="Invoke a specific MCP tool with the provided arguments", |
| 150 | + responses={ |
| 151 | + 200: {"description": "Tool invocation completed (check status field)"}, |
| 152 | + 400: {"description": "Invalid input data"}, |
| 153 | + 404: {"description": "Tool not found"}, |
| 154 | + 503: {"description": "MCP integration is disabled"}, |
| 155 | + }, |
| 156 | +) |
| 157 | +async def invoke_tool( |
| 158 | + tool_name: str, |
| 159 | + invocation_input: MCPInvocationInput, |
| 160 | + current_user: Annotated[dict, Depends(get_current_user)], |
| 161 | + settings: Annotated[Settings, Depends(get_settings)], |
| 162 | + mcp_client: Annotated[ResilientMCPGatewayClient, Depends(get_mcp_client)], |
| 163 | +) -> MCPInvocationOutput: |
| 164 | + """Invoke a specific MCP tool. |
| 165 | +
|
| 166 | + Executes the named tool with provided arguments. Implements graceful |
| 167 | + degradation - tool failures are returned in the response status rather |
| 168 | + than throwing exceptions (except for validation errors). |
| 169 | +
|
| 170 | + SECURITY: Requires authentication. |
| 171 | +
|
| 172 | + Args: |
| 173 | + tool_name: Name of the tool to invoke |
| 174 | + invocation_input: Tool arguments and optional timeout |
| 175 | + current_user: Authenticated user from JWT token |
| 176 | + settings: Application settings |
| 177 | + mcp_client: MCP gateway client |
| 178 | +
|
| 179 | + Returns: |
| 180 | + MCPInvocationOutput: Tool execution result |
| 181 | +
|
| 182 | + Raises: |
| 183 | + HTTPException: If MCP is disabled or input validation fails |
| 184 | + """ |
| 185 | + if not settings.mcp_enabled: |
| 186 | + raise HTTPException( |
| 187 | + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, |
| 188 | + detail="MCP integration is disabled", |
| 189 | + ) |
| 190 | + |
| 191 | + if not tool_name or not tool_name.strip(): |
| 192 | + raise HTTPException( |
| 193 | + status_code=status.HTTP_400_BAD_REQUEST, |
| 194 | + detail="Tool name is required", |
| 195 | + ) |
| 196 | + |
| 197 | + user_id = current_user.get("uuid") |
| 198 | + |
| 199 | + logger.info( |
| 200 | + "Invoking MCP tool", |
| 201 | + extra={ |
| 202 | + "tool_name": tool_name, |
| 203 | + "user_id": user_id, |
| 204 | + "has_arguments": bool(invocation_input.arguments), |
| 205 | + }, |
| 206 | + ) |
| 207 | + |
| 208 | + result = await mcp_client.invoke_tool( |
| 209 | + tool_name=tool_name.strip(), |
| 210 | + arguments=invocation_input.arguments, |
| 211 | + timeout=invocation_input.timeout, |
| 212 | + ) |
| 213 | + |
| 214 | + # Log result status |
| 215 | + if result.status == MCPInvocationStatus.SUCCESS: |
| 216 | + logger.info( |
| 217 | + "MCP tool invocation succeeded", |
| 218 | + extra={ |
| 219 | + "tool_name": tool_name, |
| 220 | + "user_id": user_id, |
| 221 | + "execution_time_ms": result.execution_time_ms, |
| 222 | + }, |
| 223 | + ) |
| 224 | + else: |
| 225 | + logger.warning( |
| 226 | + "MCP tool invocation failed", |
| 227 | + extra={ |
| 228 | + "tool_name": tool_name, |
| 229 | + "user_id": user_id, |
| 230 | + "status": result.status.value, |
| 231 | + "error": result.error, |
| 232 | + }, |
| 233 | + ) |
| 234 | + |
| 235 | + return result |
| 236 | + |
| 237 | + |
| 238 | +@router.get( |
| 239 | + "/metrics", |
| 240 | + summary="Get MCP client metrics", |
| 241 | + description="Retrieve Prometheus-ready metrics from the MCP client", |
| 242 | + responses={ |
| 243 | + 200: {"description": "Client metrics"}, |
| 244 | + 503: {"description": "MCP integration is disabled"}, |
| 245 | + }, |
| 246 | +) |
| 247 | +async def get_metrics( |
| 248 | + current_user: Annotated[dict, Depends(get_current_user)], |
| 249 | + settings: Annotated[Settings, Depends(get_settings)], |
| 250 | + mcp_client: Annotated[ResilientMCPGatewayClient, Depends(get_mcp_client)], |
| 251 | +) -> dict: |
| 252 | + """Get MCP client metrics for monitoring. |
| 253 | +
|
| 254 | + Returns Prometheus-ready metrics including: |
| 255 | + - Request counts (total, success, failed) |
| 256 | + - Circuit breaker state |
| 257 | + - Health check statistics |
| 258 | +
|
| 259 | + SECURITY: Requires authentication. |
| 260 | +
|
| 261 | + Args: |
| 262 | + current_user: Authenticated user from JWT token |
| 263 | + settings: Application settings |
| 264 | + mcp_client: MCP gateway client |
| 265 | +
|
| 266 | + Returns: |
| 267 | + dict: Client metrics |
| 268 | + """ |
| 269 | + if not settings.mcp_enabled: |
| 270 | + raise HTTPException( |
| 271 | + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, |
| 272 | + detail="MCP integration is disabled", |
| 273 | + ) |
| 274 | + |
| 275 | + return mcp_client.get_metrics() |
0 commit comments