rtefactor backend

This commit is contained in:
Saumya 2025-07-31 09:07:09 +05:30
parent 1f73aa25ef
commit bf3904e860
10 changed files with 14 additions and 300 deletions

View File

@ -46,7 +46,7 @@ Before contributing, ensure you have access to:
**Optional:**
- RapidAPI key (for additional tools)
- Smithery API key (for custom agents)
- Custom MCP server configurations
## Code Style Guidelines

View File

@ -101,7 +101,7 @@ The setup process includes:
- Configuring web search and scraping capabilities (Tavily, Firecrawl)
- Setting up QStash for background job processing and workflows
- Configuring webhook handling for automated tasks
- Optional integrations (RapidAPI, Smithery for custom agents)
- Optional integrations (RapidAPI for data providers)
### Quick Start
@ -156,7 +156,7 @@ We welcome contributions from the community! Please see our [Contributing Guide]
- [Firecrawl](https://firecrawl.dev/) - Web scraping capabilities
- [QStash](https://upstash.com/qstash) - Background job processing and workflows
- [RapidAPI](https://rapidapi.com/) - API services
- [Smithery](https://smithery.ai/) - Custom agent development
- Custom MCP servers - Extend functionality with custom tools
## License

View File

@ -111,7 +111,6 @@ MCP_CREDENTIAL_ENCRYPTION_KEY=your-generated-encryption-key
# Optional APIs
RAPID_API_KEY=your-rapidapi-key
SMITHERY_API_KEY=your-smithery-key
NEXT_PUBLIC_URL=http://localhost:3000
```

View File

@ -2,22 +2,16 @@ from .mcp_service import (
MCPService,
mcp_service,
MCPServer,
MCPConnection,
MCPServerDetail,
MCPServerListResult,
PopularServersResult,
ToolExecutionResult,
CustomMCPConnectionResult,
MCPException,
MCPConnectionError,
MCPServerNotFoundError,
MCPToolNotFoundError,
MCPToolExecutionError,
MCPProviderError,
MCPConfigurationError,
MCPRegistryError,
MCPAuthenticationError,
CustomMCPError,
)
@ -25,21 +19,15 @@ from .mcp_service import (
__all__ = [
"MCPService",
"mcp_service",
"MCPServer",
"MCPConnection",
"MCPServerDetail",
"MCPServerListResult",
"PopularServersResult",
"ToolExecutionResult",
"CustomMCPConnectionResult",
"MCPException",
"MCPConnectionError",
"MCPServerNotFoundError",
"MCPToolNotFoundError",
"MCPToolExecutionError",
"MCPProviderError",
"MCPConfigurationError",
"MCPRegistryError",
"MCPAuthenticationError",
"CustomMCPError"
]

View File

@ -4,46 +4,10 @@ from pydantic import BaseModel
from utils.auth_utils import get_current_user_id_from_jwt
from utils.logger import logger
from .mcp_service import mcp_service, MCPException, MCPServerNotFoundError
from .mcp_service import mcp_service, MCPException
router = APIRouter()
class MCPServerResponse(BaseModel):
qualified_name: str
display_name: str
description: str
created_at: str
use_count: int
homepage: str
icon_url: Optional[str] = None
is_deployed: Optional[bool] = None
tools: Optional[List[Dict[str, Any]]] = None
security: Optional[Dict[str, Any]] = None
class MCPServerListResponse(BaseModel):
servers: List[MCPServerResponse]
pagination: Dict[str, int]
class MCPServerDetailResponse(BaseModel):
qualified_name: str
display_name: str
icon_url: Optional[str] = None
deployment_url: Optional[str] = None
connections: List[Dict[str, Any]]
security: Optional[Dict[str, Any]] = None
tools: Optional[List[Dict[str, Any]]] = None
class PopularServersResponse(BaseModel):
success: bool
servers: List[Dict[str, Any]]
categorized: Dict[str, List[Dict[str, Any]]]
total: int
category_count: int
pagination: Dict[str, int]
class CustomMCPConnectionRequest(BaseModel):
url: str
@ -65,31 +29,6 @@ class CustomMCPDiscoverRequest(BaseModel):
config: Dict[str, Any]
@router.get("/mcp/servers/{qualified_name:path}", response_model=MCPServerDetailResponse)
async def get_mcp_server_details(
qualified_name: str,
user_id: str = Depends(get_current_user_id_from_jwt)
):
try:
server_detail = await mcp_service.get_server_details(qualified_name)
return MCPServerDetailResponse(
qualified_name=server_detail.qualified_name,
display_name=server_detail.display_name,
icon_url=server_detail.icon_url,
deployment_url=server_detail.deployment_url,
connections=server_detail.connections,
security=server_detail.security,
tools=server_detail.tools
)
except MCPServerNotFoundError:
raise HTTPException(status_code=404, detail="MCP server not found")
except MCPException as e:
logger.error(f"Error getting MCP server details: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/mcp/discover-custom-tools")
async def discover_custom_mcp_tools(request: CustomMCPDiscoverRequest):
try:

View File

@ -7,8 +7,6 @@ from typing import Dict, List, Any, Optional, Tuple
from dataclasses import dataclass, field
from datetime import datetime
from collections import OrderedDict
import httpx
from urllib.parse import quote
from mcp import ClientSession
from mcp.client.sse import sse_client
@ -24,9 +22,6 @@ class MCPException(Exception):
class MCPConnectionError(MCPException):
pass
class MCPServerNotFoundError(MCPException):
pass
class MCPToolNotFoundError(MCPException):
pass
@ -39,9 +34,6 @@ class MCPProviderError(MCPException):
class MCPConfigurationError(MCPException):
pass
class MCPRegistryError(MCPException):
pass
class MCPAuthenticationError(MCPException):
pass
@ -49,59 +41,18 @@ class CustomMCPError(MCPException):
pass
@dataclass(frozen=True)
class MCPServer:
qualified_name: str
display_name: str
description: str
created_at: str
use_count: int
homepage: str
icon_url: Optional[str] = None
is_deployed: Optional[bool] = None
tools: Optional[List[Dict[str, Any]]] = None
security: Optional[Dict[str, Any]] = None
@dataclass(frozen=True)
class MCPConnection:
qualified_name: str
name: str
config: Dict[str, Any]
enabled_tools: List[str]
provider: str = 'smithery'
provider: str = 'custom'
external_user_id: Optional[str] = None
session: Optional[ClientSession] = field(default=None, compare=False)
tools: Optional[List[Any]] = field(default=None, compare=False)
@dataclass(frozen=True)
class MCPServerDetail:
qualified_name: str
display_name: str
icon_url: Optional[str] = None
deployment_url: Optional[str] = None
connections: List[Dict[str, Any]] = field(default_factory=list)
security: Optional[Dict[str, Any]] = None
tools: Optional[List[Dict[str, Any]]] = None
@dataclass(frozen=True)
class MCPServerListResult:
servers: List[MCPServer]
pagination: Dict[str, int]
@dataclass(frozen=True)
class PopularServersResult:
success: bool
servers: List[Dict[str, Any]]
categorized: Dict[str, List[Dict[str, Any]]]
total: int
category_count: int
pagination: Dict[str, int]
@dataclass(frozen=True)
class ToolInfo:
name: str
@ -126,7 +77,7 @@ class MCPConnectionRequest:
name: str
config: Dict[str, Any]
enabled_tools: List[str]
provider: str = 'smithery'
provider: str = 'custom'
external_user_id: Optional[str] = None
@ -149,106 +100,6 @@ class MCPService:
self._logger = logger
self._connections: Dict[str, MCPConnection] = {}
self._encryption_service = EncryptionService()
self._registry_base_url = "https://registry.smithery.ai"
self._server_base_url = "https://server.smithery.ai"
self._api_key = os.getenv("SMITHERY_API_KEY")
async def list_servers(
self,
query: Optional[str] = None,
page: int = 1,
page_size: int = 20
) -> MCPServerListResult:
self._logger.info(f"Listing MCP servers (page {page}, size {page_size})")
if query:
self._logger.info(f"Search query: {query}")
try:
params = {
"page": page,
"pageSize": page_size
}
if query:
params["q"] = query
async with httpx.AsyncClient() as client:
response = await client.get(f"{self._registry_base_url}/api/v1/packages", params=params)
response.raise_for_status()
data = response.json()
servers = []
for item in data.get("packages", []):
servers.append(MCPServer(
qualified_name=item["qualifiedName"],
display_name=item["displayName"],
description=item["description"],
created_at=item["createdAt"],
use_count=item["useCount"],
homepage=item["homepage"]
))
return MCPServerListResult(
servers=servers,
pagination=data.get("pagination", {})
)
except httpx.HTTPError as e:
self._logger.error(f"Error fetching MCP servers: {str(e)}")
raise MCPRegistryError(f"Failed to fetch MCP servers: {str(e)}")
async def get_server_details(self, qualified_name: str) -> MCPServerDetail:
self._logger.info(f"Getting details for MCP server: {qualified_name}")
try:
encoded_name = quote(qualified_name, safe='')
async with httpx.AsyncClient() as client:
response = await client.get(f"{self._registry_base_url}/api/v1/packages/{encoded_name}")
if response.status_code == 404:
raise MCPServerNotFoundError(f"MCP server not found: {qualified_name}")
response.raise_for_status()
data = response.json()
return MCPServerDetail(
qualified_name=data["qualifiedName"],
display_name=data["displayName"],
icon_url=data.get("iconUrl"),
deployment_url=data.get("deploymentUrl"),
connections=data.get("connections", []),
security=data.get("security"),
tools=data.get("tools")
)
except httpx.HTTPError as e:
if e.response and e.response.status_code == 404:
raise MCPServerNotFoundError(f"MCP server not found: {qualified_name}")
self._logger.error(f"Error fetching MCP server details: {str(e)}")
raise MCPRegistryError(f"Failed to fetch MCP server details: {str(e)}")
async def get_popular_servers(self) -> PopularServersResult:
self._logger.info("Getting popular MCP servers")
try:
async with httpx.AsyncClient() as client:
response = await client.get(f"{self._registry_base_url}/api/v1/packages/popular")
response.raise_for_status()
data = response.json()
return PopularServersResult(
success=data.get("success", True),
servers=data.get("servers", []),
categorized=data.get("categorized", {}),
total=data.get("total", 0),
category_count=data.get("categoryCount", 0),
pagination=data.get("pagination", {})
)
except httpx.HTTPError as e:
self._logger.error(f"Error fetching popular MCP servers: {str(e)}")
raise MCPRegistryError(f"Failed to fetch popular MCP servers: {str(e)}")
async def connect_server(self, mcp_config: Dict[str, Any], external_user_id: Optional[str] = None) -> MCPConnection:
request = MCPConnectionRequest(
@ -256,7 +107,7 @@ class MCPService:
name=mcp_config.get('name', ''),
config=mcp_config.get('config', {}),
enabled_tools=mcp_config.get('enabledTools', mcp_config.get('enabled_tools', [])),
provider=mcp_config.get('provider', 'smithery'),
provider=mcp_config.get('provider', 'custom'),
external_user_id=external_user_id
)
return await self._connect_server_internal(request)
@ -305,7 +156,7 @@ class MCPService:
name=config.get('name', ''),
config=config.get('config', {}),
enabled_tools=config.get('enabledTools', config.get('enabled_tools', [])),
provider=config.get('provider', 'smithery'),
provider=config.get('provider', 'custom'),
external_user_id=config.get('external_user_id')
)
requests.append(request)
@ -559,36 +410,17 @@ class MCPService:
)
def _get_server_url(self, qualified_name: str, config: Dict[str, Any], provider: str) -> str:
if provider == 'smithery':
return self._get_smithery_server_url(qualified_name, config)
elif provider in ['custom', 'http', 'sse']:
if provider in ['custom', 'http', 'sse']:
return self._get_custom_server_url(qualified_name, config)
else:
raise MCPProviderError(f"Unknown provider type: {provider}")
def _get_headers(self, qualified_name: str, config: Dict[str, Any], provider: str, external_user_id: Optional[str] = None) -> Dict[str, str]:
if provider == 'smithery':
return self._get_smithery_headers(qualified_name, config, external_user_id)
elif provider in ['custom', 'http', 'sse']:
if provider in ['custom', 'http', 'sse']:
return self._get_custom_headers(qualified_name, config, external_user_id)
else:
raise MCPProviderError(f"Unknown provider type: {provider}")
def _get_smithery_server_url(self, qualified_name: str, config: Dict[str, Any]) -> str:
if not self._api_key:
raise MCPAuthenticationError("SMITHERY_API_KEY environment variable is not set")
config_json = json.dumps(config)
config_b64 = base64.b64encode(config_json.encode()).decode()
return f"{self._server_base_url}/{qualified_name}/mcp?config={config_b64}&api_key={self._api_key}"
def _get_smithery_headers(self, qualified_name: str, config: Dict[str, Any], external_user_id: Optional[str] = None) -> Dict[str, str]:
headers = {"Content-Type": "application/json"}
if external_user_id:
headers["X-External-User-Id"] = external_user_id
return headers
def _get_custom_server_url(self, qualified_name: str, config: Dict[str, Any]) -> str:
url = config.get("url")
if not url:

View File

@ -43,7 +43,7 @@ ALTER TABLE workflows
ADD COLUMN mcp_credential_mappings JSONB DEFAULT '{}';
COMMENT ON COLUMN workflows.mcp_credential_mappings IS
'JSON mapping of MCP qualified names to credential profile IDs. Example: {"@smithery-ai/slack": "profile_id_123", "github": "profile_id_456"}';
'JSON mapping of MCP qualified names to credential profile IDs. Example: {"github": "profile_id_123", "slack": "profile_id_456"}';
-- Migrate existing credentials if the table exists
DO $$

View File

@ -64,7 +64,7 @@ Obtain the following API keys:
#### Optional
- **RapidAPI** - For accessing additional API services (enables LinkedIn scraping and other tools)
- **Smithery** - For custom agents and workflows ([Get API key](https://smithery.ai/))
- **Custom MCP Servers** - For extending functionality with custom tools
### 3. Required Software
@ -200,7 +200,7 @@ MCP_CREDENTIAL_ENCRYPTION_KEY=your-generated-encryption-key
# Optional APIs
RAPID_API_KEY=your-rapidapi-key
SMITHERY_API_KEY=your-smithery-key
# MCP server configurations in database
NEXT_PUBLIC_URL=http://localhost:3000
```

View File

@ -109,7 +109,7 @@ const mockMCPServers: MCPServer[] = [
name: "Slack",
description: "Send messages and interact with Slack workspaces",
icon: MessageSquare,
qualifiedName: "@smithery-ai/slack",
qualifiedName: "slack-mcp-server",
configSchema: {
properties: {
slackBotToken: {

View File

@ -141,9 +141,6 @@ def load_existing_env_vars():
"rapidapi": {
"RAPID_API_KEY": backend_env.get("RAPID_API_KEY", ""),
},
"smithery": {
"SMITHERY_API_KEY": backend_env.get("SMITHERY_API_KEY", ""),
},
"qstash": {
"QSTASH_URL": backend_env.get("QSTASH_URL", ""),
"QSTASH_TOKEN": backend_env.get("QSTASH_TOKEN", ""),
@ -261,7 +258,6 @@ class SetupWizard:
"llm": existing_env_vars["llm"],
"search": existing_env_vars["search"],
"rapidapi": existing_env_vars["rapidapi"],
"smithery": existing_env_vars["smithery"],
"qstash": existing_env_vars["qstash"],
"slack": existing_env_vars["slack"],
"webhook": existing_env_vars["webhook"],
@ -325,12 +321,6 @@ class SetupWizard:
else:
config_items.append(f"{Colors.CYAN}{Colors.ENDC} RapidAPI (optional)")
# Check Smithery (optional)
if self.env_vars["smithery"]["SMITHERY_API_KEY"]:
config_items.append(f"{Colors.GREEN}{Colors.ENDC} Smithery (optional)")
else:
config_items.append(f"{Colors.CYAN}{Colors.ENDC} Smithery (optional)")
# Check QStash (required)
if self.env_vars["qstash"]["QSTASH_TOKEN"]:
config_items.append(f"{Colors.GREEN}{Colors.ENDC} QStash & Webhooks")
@ -394,7 +384,6 @@ class SetupWizard:
self.run_step(6, self.collect_morph_api_key)
self.run_step(7, self.collect_search_api_keys)
self.run_step(8, self.collect_rapidapi_keys)
self.run_step(9, self.collect_smithery_keys)
self.run_step(10, self.collect_qstash_keys)
self.run_step(11, self.collect_mcp_keys)
self.run_step(12, self.collect_pipedream_keys)
@ -904,38 +893,6 @@ class SetupWizard:
else:
print_info("Skipping RapidAPI key.")
def collect_smithery_keys(self):
"""Collects the optional Smithery API key."""
print_step(9, self.total_steps, "Collecting Smithery API Key (Optional)")
# Check if we already have a value configured
existing_key = self.env_vars["smithery"]["SMITHERY_API_KEY"]
if existing_key:
print_info(
f"Found existing Smithery API key: {mask_sensitive_value(existing_key)}"
)
print_info("Press Enter to keep current value or type a new one.")
else:
print_info(
"A Smithery API key is only required for custom agents and workflows."
)
print_info(
"Get a key at https://smithery.ai/. You can skip this and add it later."
)
smithery_api_key = self._get_input(
"Enter your Smithery API key (or press Enter to skip): ",
validate_api_key,
"The key seems invalid, but continuing. You can edit it later in backend/.env",
allow_empty=True,
default_value=existing_key,
)
self.env_vars["smithery"]["SMITHERY_API_KEY"] = smithery_api_key
if smithery_api_key:
print_success("Smithery API key saved.")
else:
print_info("Skipping Smithery API key.")
def collect_qstash_keys(self):
"""Collects the required QStash configuration."""
print_step(
@ -1169,7 +1126,6 @@ class SetupWizard:
**self.env_vars["llm"],
**self.env_vars["search"],
**self.env_vars["rapidapi"],
**self.env_vars["smithery"],
**self.env_vars["qstash"],
**self.env_vars["slack"],
**self.env_vars["webhook"],