Security hardening release addressing CodeQL and Dependabot alerts: - Fix stack trace exposure in error responses - Add SSRF protection with DNS resolution checking - Implement proper URL hostname validation (replaces substring matching) - Add centralized path sanitization to prevent path traversal - Fix ReDoS vulnerability in email validation regex - Improve HTML sanitization in validation utilities - Fix capability wildcard matching in auth utilities - Update glob dependency to address CVE - Add CodeQL suppression comments for verified false positives 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
238 lines
7.7 KiB
Python
238 lines
7.7 KiB
Python
"""
|
|
GT 2.0 MCP Registry API
|
|
|
|
Manages registration and discovery of MCP servers in the resource cluster.
|
|
Provides endpoints for:
|
|
- Registering MCP servers
|
|
- Listing available MCP servers and tools
|
|
- Getting tool schemas
|
|
- Server health monitoring
|
|
"""
|
|
|
|
from typing import Dict, Any, List, Optional
|
|
from fastapi import APIRouter, HTTPException, Header, Query
|
|
from pydantic import BaseModel
|
|
import logging
|
|
|
|
from app.core.security import verify_capability_token
|
|
from app.services.mcp_server import SecureMCPWrapper, MCPServerConfig
|
|
from app.services.mcp_rag_server import mcp_rag_server
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/mcp", tags=["mcp"])
|
|
|
|
|
|
# Request/Response Models
|
|
class MCPServerInfo(BaseModel):
|
|
"""Information about an MCP server"""
|
|
server_name: str
|
|
server_type: str
|
|
available_tools: List[str]
|
|
status: str
|
|
description: str
|
|
required_capabilities: List[str]
|
|
|
|
|
|
class MCPToolSchema(BaseModel):
|
|
"""MCP tool schema information"""
|
|
name: str
|
|
description: str
|
|
parameters: Dict[str, Any]
|
|
server_name: str
|
|
|
|
|
|
class ListServersResponse(BaseModel):
|
|
"""Response for listing MCP servers"""
|
|
servers: List[MCPServerInfo]
|
|
total_count: int
|
|
|
|
|
|
class ListToolsResponse(BaseModel):
|
|
"""Response for listing MCP tools"""
|
|
tools: List[MCPToolSchema]
|
|
total_count: int
|
|
servers_count: int
|
|
|
|
|
|
# Global MCP wrapper instance
|
|
mcp_wrapper = SecureMCPWrapper()
|
|
|
|
|
|
@router.get("/servers", response_model=ListServersResponse)
|
|
async def list_mcp_servers(
|
|
knowledge_search_enabled: bool = Query(True, description="Whether dataset/knowledge search is enabled"),
|
|
x_tenant_id: Optional[str] = Header(None, alias="X-Tenant-ID", description="Tenant ID for context")
|
|
):
|
|
"""
|
|
List all available MCP servers and their status.
|
|
|
|
Returns information about registered MCP servers that the user
|
|
can access based on their capability tokens.
|
|
"""
|
|
try:
|
|
servers = []
|
|
|
|
if knowledge_search_enabled:
|
|
rag_config = mcp_rag_server.get_server_config()
|
|
servers.append(MCPServerInfo(
|
|
server_name=rag_config.server_name,
|
|
server_type=rag_config.server_type,
|
|
available_tools=rag_config.available_tools,
|
|
status="healthy",
|
|
description="Dataset and document search capabilities for RAG operations",
|
|
required_capabilities=rag_config.required_capabilities
|
|
))
|
|
|
|
return ListServersResponse(
|
|
servers=servers,
|
|
total_count=len(servers)
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error listing MCP servers: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Failed to list servers: {str(e)}")
|
|
|
|
|
|
@router.get("/tools", response_model=ListToolsResponse)
|
|
async def list_mcp_tools(
|
|
server_name: Optional[str] = Query(None, description="Filter by server name"),
|
|
knowledge_search_enabled: bool = Query(True, description="Whether dataset/knowledge search is enabled"),
|
|
x_tenant_id: Optional[str] = Header(None, alias="X-Tenant-ID", description="Tenant ID for context")
|
|
):
|
|
"""
|
|
List all available MCP tools across servers.
|
|
|
|
Can be filtered by server name to get tools for a specific server.
|
|
"""
|
|
try:
|
|
all_tools = []
|
|
servers_included = 0
|
|
|
|
if knowledge_search_enabled and (not server_name or server_name == "rag_server"):
|
|
rag_schemas = mcp_rag_server.get_tool_schemas()
|
|
for tool_name, schema in rag_schemas.items():
|
|
all_tools.append(MCPToolSchema(
|
|
name=tool_name,
|
|
description=schema.get("description", ""),
|
|
parameters=schema.get("parameters", {}),
|
|
server_name="rag_server"
|
|
))
|
|
servers_included += 1
|
|
|
|
return ListToolsResponse(
|
|
tools=all_tools,
|
|
total_count=len(all_tools),
|
|
servers_count=servers_included
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error listing MCP tools: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Failed to list tools: {str(e)}")
|
|
|
|
|
|
@router.get("/servers/{server_name}/tools")
|
|
async def get_server_tools(
|
|
server_name: str,
|
|
knowledge_search_enabled: bool = Query(True, description="Whether dataset/knowledge search is enabled"),
|
|
x_tenant_id: Optional[str] = Header(None, alias="X-Tenant-ID", description="Tenant ID for context")
|
|
):
|
|
"""Get tools and schemas for a specific MCP server"""
|
|
try:
|
|
if server_name == "rag_server":
|
|
if knowledge_search_enabled:
|
|
return {
|
|
"server_name": server_name,
|
|
"server_type": "rag",
|
|
"tools": mcp_rag_server.get_tool_schemas()
|
|
}
|
|
else:
|
|
return {
|
|
"server_name": server_name,
|
|
"server_type": "rag",
|
|
"tools": {}
|
|
}
|
|
else:
|
|
raise HTTPException(status_code=404, detail=f"MCP server not found: {server_name}")
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error getting server tools for {server_name}: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Failed to get server tools: {str(e)}")
|
|
|
|
|
|
@router.get("/servers/{server_name}/health")
|
|
async def check_server_health(
|
|
server_name: str,
|
|
x_tenant_id: Optional[str] = Header(None, alias="X-Tenant-ID", description="Tenant ID for context")
|
|
):
|
|
"""Check health status of a specific MCP server"""
|
|
try:
|
|
if server_name == "rag_server":
|
|
return {
|
|
"server_name": server_name,
|
|
"status": "healthy",
|
|
"timestamp": "2025-01-15T12:00:00Z",
|
|
"response_time_ms": 5,
|
|
"tools_available": True
|
|
}
|
|
else:
|
|
raise HTTPException(status_code=404, detail=f"MCP server not found: {server_name}")
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error checking health for {server_name}: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Health check failed: {str(e)}")
|
|
|
|
|
|
@router.get("/capabilities")
|
|
async def get_mcp_capabilities(
|
|
x_tenant_id: Optional[str] = Header(None, alias="X-Tenant-ID", description="Tenant ID for context")
|
|
):
|
|
"""
|
|
Get MCP capabilities summary for the current user.
|
|
|
|
Returns what MCP servers and tools the user has access to
|
|
based on their capability tokens.
|
|
"""
|
|
try:
|
|
capabilities = {
|
|
"user_id": "resource_cluster_user",
|
|
"tenant_domain": x_tenant_id or "default",
|
|
"available_servers": [
|
|
{
|
|
"server_name": "rag_server",
|
|
"server_type": "rag",
|
|
"tools_count": len(mcp_rag_server.available_tools),
|
|
"required_capability": "mcp:rag:*"
|
|
}
|
|
],
|
|
"total_tools": len(mcp_rag_server.available_tools),
|
|
"access_level": "full"
|
|
}
|
|
|
|
return capabilities
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting MCP capabilities: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Failed to get capabilities: {str(e)}")
|
|
|
|
|
|
async def initialize_mcp_servers():
|
|
"""Initialize and register MCP servers"""
|
|
try:
|
|
logger.info("Initializing MCP servers...")
|
|
|
|
rag_config = mcp_rag_server.get_server_config()
|
|
logger.info(f"RAG server initialized with {len(rag_config.available_tools)} tools")
|
|
|
|
logger.info("All MCP servers initialized successfully")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error initializing MCP servers: {e}")
|
|
raise
|
|
|
|
|
|
# Export the initialization function
|
|
__all__ = ["router", "initialize_mcp_servers", "mcp_wrapper"] |