From d5fb7e1c8de3da36850a6e6528b807e173f17a39 Mon Sep 17 00:00:00 2001 From: Soumyadas15 Date: Sun, 8 Jun 2025 14:01:38 +0530 Subject: [PATCH] chore(dev): improve ux of credentials manager --- backend/mcp_local/credential_manager.py | 31 +- backend/mcp_local/secure_api.py | 9 +- backend/mcp_local/template_manager.py | 143 ++-- .../src/app/(dashboard)/marketplace/page.tsx | 730 +++++++++++------- .../enhanced-add-credential-dialog.tsx | 164 +++- .../(dashboard)/settings/credentials/page.tsx | 14 +- .../react-query/secure-mcp/use-secure-mcp.ts | 12 +- 7 files changed, 725 insertions(+), 378 deletions(-) diff --git a/backend/mcp_local/credential_manager.py b/backend/mcp_local/credential_manager.py index 852c6580..f3faaf9a 100644 --- a/backend/mcp_local/credential_manager.py +++ b/backend/mcp_local/credential_manager.py @@ -47,6 +47,7 @@ class MCPRequirement: display_name: str enabled_tools: List[str] required_config: List[str] + custom_type: Optional[str] = None # 'sse' or 'http' for custom MCP servers class CredentialManager: @@ -381,8 +382,18 @@ class CredentialManager: missing = [] for req in requirements: - if req.qualified_name not in user_mcp_names: - missing.append(req) + if req.custom_type: + custom_pattern = f"custom_{req.custom_type}_" + found = any( + cred_name.startswith(custom_pattern) and + req.display_name.lower().replace(' ', '_') in cred_name + for cred_name in user_mcp_names + ) + if not found: + missing.append(req) + else: + if req.qualified_name not in user_mcp_names: + missing.append(req) return missing @@ -395,9 +406,19 @@ class CredentialManager: mappings = {} for req in requirements: - credential = await self.get_credential(account_id, req.qualified_name) - if credential: - mappings[req.qualified_name] = credential.credential_id + if req.custom_type: + user_credentials = await self.get_user_credentials(account_id) + custom_pattern = f"custom_{req.custom_type}_" + + for cred in user_credentials: + if (cred.mcp_qualified_name.startswith(custom_pattern) and + req.display_name.lower().replace(' ', '_') in cred.mcp_qualified_name): + mappings[req.qualified_name] = cred.credential_id + break + else: + credential = await self.get_credential(account_id, req.qualified_name) + if credential: + mappings[req.qualified_name] = credential.credential_id return mappings diff --git a/backend/mcp_local/secure_api.py b/backend/mcp_local/secure_api.py index b4b84461..b4623229 100644 --- a/backend/mcp_local/secure_api.py +++ b/backend/mcp_local/secure_api.py @@ -64,6 +64,7 @@ class InstallTemplateRequest(BaseModel): template_id: str instance_name: Optional[str] = None custom_system_prompt: Optional[str] = None + custom_mcp_configs: Optional[Dict[str, Dict[str, Any]]] = None class PublishTemplateRequest(BaseModel): """Request model for publishing template""" @@ -87,9 +88,10 @@ class TemplateResponse(BaseModel): class InstallationResponse(BaseModel): """Response model for template installation""" - status: str # 'installed' or 'credentials_required' + status: str # 'installed', 'configs_required' instance_id: Optional[str] = None - missing_credentials: Optional[List[Dict[str, Any]]] = None + missing_regular_credentials: Optional[List[Dict[str, Any]]] = None + missing_custom_configs: Optional[List[Dict[str, Any]]] = None template: Optional[Dict[str, Any]] = None # ===================================================== @@ -301,7 +303,8 @@ async def install_template( template_id=request.template_id, account_id=user_id, instance_name=request.instance_name, - custom_system_prompt=request.custom_system_prompt + custom_system_prompt=request.custom_system_prompt, + custom_mcp_configs=request.custom_mcp_configs ) return InstallationResponse(**result) diff --git a/backend/mcp_local/template_manager.py b/backend/mcp_local/template_manager.py index 518405fe..52cba196 100644 --- a/backend/mcp_local/template_manager.py +++ b/backend/mcp_local/template_manager.py @@ -119,6 +119,7 @@ class TemplateManager: 'required_config': list(custom_mcp.get('config', {}).keys()), 'custom_type': custom_mcp['type'] } + logger.info(f"Created custom MCP requirement: {requirement}") mcp_requirements.append(requirement) # Create template @@ -172,7 +173,8 @@ class TemplateManager: qualified_name=req_data.get('qualified_name') or req_data.get('qualifiedName'), display_name=req_data.get('display_name') or req_data.get('name'), enabled_tools=req_data.get('enabled_tools') or req_data.get('enabledTools', []), - required_config=req_data.get('required_config') or req_data.get('requiredConfig', []) + required_config=req_data.get('required_config') or req_data.get('requiredConfig', []), + custom_type=req_data.get('custom_type') )) return AgentTemplate( @@ -202,7 +204,8 @@ class TemplateManager: template_id: str, account_id: str, instance_name: Optional[str] = None, - custom_system_prompt: Optional[str] = None + custom_system_prompt: Optional[str] = None, + custom_mcp_configs: Optional[Dict[str, Dict[str, Any]]] = None ) -> Dict[str, Any]: """ Install a template as an agent instance for a user @@ -212,6 +215,7 @@ class TemplateManager: account_id: ID of the user installing the template instance_name: Optional custom name for the instance custom_system_prompt: Optional custom system prompt override + custom_mcp_configs: Optional dict mapping qualified_name to config for custom MCPs Returns: Dictionary with installation result and any missing credentials @@ -230,22 +234,44 @@ class TemplateManager: if template.creator_id != account_id: raise ValueError("Access denied to private template") - # Check for missing credentials - missing_credentials = await credential_manager.get_missing_credentials_for_requirements( - account_id, template.mcp_requirements + # Debug: Log template requirements + logger.info(f"Template MCP requirements: {[(req.qualified_name, req.display_name, getattr(req, 'custom_type', None)) for req in template.mcp_requirements]}") + + # Separate custom and regular MCP requirements + custom_requirements = [req for req in template.mcp_requirements if getattr(req, 'custom_type', None)] + regular_requirements = [req for req in template.mcp_requirements if not getattr(req, 'custom_type', None)] + + # Check for missing regular credentials + missing_regular_credentials = await credential_manager.get_missing_credentials_for_requirements( + account_id, regular_requirements ) - if missing_credentials: + # Check for missing custom MCP configs + missing_custom_configs = [] + if custom_requirements: + provided_custom_configs = custom_mcp_configs or {} + for req in custom_requirements: + if req.qualified_name not in provided_custom_configs: + missing_custom_configs.append({ + 'qualified_name': req.qualified_name, + 'display_name': req.display_name, + 'custom_type': req.custom_type, + 'required_config': req.required_config + }) + + # If we have any missing credentials or configs, return them + if missing_regular_credentials or missing_custom_configs: return { - 'status': 'credentials_required', - 'missing_credentials': [ + 'status': 'configs_required', + 'missing_regular_credentials': [ { 'qualified_name': req.qualified_name, 'display_name': req.display_name, 'required_config': req.required_config } - for req in missing_credentials + for req in missing_regular_credentials ], + 'missing_custom_configs': missing_custom_configs, 'template': { 'template_id': template.template_id, 'name': template.name, @@ -261,42 +287,58 @@ class TemplateManager: # Create regular agent with secure credentials client = await db.client - # Build configured_mcps with user's credentials + # Build configured_mcps and custom_mcps with user's credentials configured_mcps = [] + custom_mcps = [] for req in template.mcp_requirements: - credential_id = credential_mappings.get(req.qualified_name) - if not credential_id: - logger.warning(f"No credential mapping for {req.qualified_name}") - continue + logger.info(f"Processing requirement: {req.qualified_name}, custom_type: {getattr(req, 'custom_type', None)}") - # Get the credential - credential = await credential_manager.get_credential( - account_id, req.qualified_name - ) - - if not credential: - logger.warning(f"Credential not found for {req.qualified_name}") - continue - - # Build MCP config with actual credentials - mcp_config = { - 'name': req.display_name, - 'qualifiedName': req.qualified_name, - 'config': credential.config, - 'enabledTools': req.enabled_tools - } - - configured_mcps.append(mcp_config) + if hasattr(req, 'custom_type') and req.custom_type: + # For custom MCP servers, use the provided config from installation + if custom_mcp_configs and req.qualified_name in custom_mcp_configs: + provided_config = custom_mcp_configs[req.qualified_name] + + custom_mcp_config = { + 'name': req.display_name, + 'type': req.custom_type, + 'config': provided_config, + 'enabledTools': req.enabled_tools + } + custom_mcps.append(custom_mcp_config) + logger.info(f"Added custom MCP with provided config: {custom_mcp_config}") + else: + logger.warning(f"No custom config provided for {req.qualified_name}") + continue + else: + # For regular MCP servers, use stored credentials + credential = await credential_manager.get_credential( + account_id, req.qualified_name + ) + + if not credential: + logger.warning(f"Credential not found for {req.qualified_name}") + continue + + mcp_config = { + 'name': req.display_name, + 'qualifiedName': req.qualified_name, + 'config': credential.config, + 'enabledTools': req.enabled_tools + } + configured_mcps.append(mcp_config) + logger.info(f"Added regular MCP: {mcp_config}") + + logger.info(f"Final configured_mcps: {configured_mcps}") + logger.info(f"Final custom_mcps: {custom_mcps}") - # Create regular agent that will show up in agent playground agent_data = { 'account_id': account_id, 'name': instance_name or f"{template.name} (from marketplace)", 'description': template.description, 'system_prompt': custom_system_prompt or template.system_prompt, 'configured_mcps': configured_mcps, - 'custom_mcps': [], # TODO: Handle custom MCPs from templates + 'custom_mcps': custom_mcps, 'agentpress_tools': template.agentpress_tools, 'is_default': False, 'avatar': template.avatar, @@ -388,8 +430,9 @@ class TemplateManager: if not template: raise ValueError("Template not found") - # Build configured_mcps with user's credentials + # Build configured_mcps and custom_mcps with user's credentials configured_mcps = [] + custom_mcps = [] for req in template.mcp_requirements: credential_id = instance.credential_mappings.get(req.qualified_name) @@ -406,15 +449,25 @@ class TemplateManager: logger.warning(f"Credential not found for {req.qualified_name}") continue - # Build MCP config - mcp_config = { - 'name': req.display_name, - 'qualifiedName': req.qualified_name, - 'config': credential.config, - 'enabledTools': req.enabled_tools - } - - configured_mcps.append(mcp_config) + # Check if this is a custom MCP server + if req.custom_type: + # Build custom MCP config + custom_mcp_config = { + 'name': req.display_name, + 'type': req.custom_type, + 'config': credential.config, + 'enabledTools': req.enabled_tools + } + custom_mcps.append(custom_mcp_config) + else: + # Build regular MCP config + mcp_config = { + 'name': req.display_name, + 'qualifiedName': req.qualified_name, + 'config': credential.config, + 'enabledTools': req.enabled_tools + } + configured_mcps.append(mcp_config) # Build complete agent config agent_config = { @@ -424,7 +477,7 @@ class TemplateManager: 'description': instance.description, 'system_prompt': instance.custom_system_prompt or template.system_prompt, 'configured_mcps': configured_mcps, - 'custom_mcps': [], # TODO: Handle custom MCPs from templates + 'custom_mcps': custom_mcps, 'agentpress_tools': template.agentpress_tools, 'is_default': instance.is_default, 'avatar': instance.avatar, diff --git a/frontend/src/app/(dashboard)/marketplace/page.tsx b/frontend/src/app/(dashboard)/marketplace/page.tsx index 5db88f6b..a84dd561 100644 --- a/frontend/src/app/(dashboard)/marketplace/page.tsx +++ b/frontend/src/app/(dashboard)/marketplace/page.tsx @@ -1,15 +1,13 @@ 'use client'; import React, { useState, useMemo } from 'react'; -import { Search, Download, Star, Calendar, User, Tags, TrendingUp, Globe, Shield, AlertTriangle, CheckCircle, Loader2, Settings, X, Wrench, Zap } from 'lucide-react'; +import { Search, Download, Star, Calendar, User, Tags, TrendingUp, Globe, Shield, AlertTriangle, CheckCircle, Loader2, Settings, X, Wrench, Zap, Info } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; -import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet'; -import { Drawer, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle } from '@/components/ui/drawer'; import { Card, CardContent } from '@/components/ui/card'; import { Separator } from '@/components/ui/separator'; import { toast } from 'sonner'; @@ -42,28 +40,40 @@ interface UnifiedMarketplaceItem { avatar?: string; avatar_color?: string; type: 'legacy' | 'secure'; - // Legacy agent fields agent_id?: string; - // Secure template fields template_id?: string; mcp_requirements?: Array<{ qualified_name: string; display_name: string; enabled_tools?: string[]; + required_config: string[]; + custom_type?: 'sse' | 'http'; }>; } +interface SetupStep { + id: string; + title: string; + description: string; + type: 'credential' | 'custom_server'; + service_name: string; + qualified_name: string; + required_fields: Array<{ + key: string; + label: string; + type: 'text' | 'url' | 'password'; + placeholder: string; + description?: string; + }>; + custom_type?: 'sse' | 'http'; +} + interface InstallDialogProps { item: UnifiedMarketplaceItem | null; open: boolean; onOpenChange: (open: boolean) => void; - onInstall: (item: UnifiedMarketplaceItem, instanceName?: string) => void; + onInstall: (item: UnifiedMarketplaceItem, instanceName?: string, customMcpConfigs?: Record>) => void; isInstalling: boolean; - credentialStatus?: { - hasAllCredentials: boolean; - missingCount: number; - totalRequired: number; - }; } const InstallDialog: React.FC = ({ @@ -71,130 +81,372 @@ const InstallDialog: React.FC = ({ open, onOpenChange, onInstall, - isInstalling, - credentialStatus + isInstalling }) => { + const [currentStep, setCurrentStep] = useState(0); const [instanceName, setInstanceName] = useState(''); + const [setupData, setSetupData] = useState>>({}); + const [isCheckingRequirements, setIsCheckingRequirements] = useState(false); + const [setupSteps, setSetupSteps] = useState([]); + const [missingCredentials, setMissingCredentials] = useState([]); + + const { data: userCredentials } = useUserCredentials(); React.useEffect(() => { - if (item) { + if (item && open) { if (item.type === 'secure') { - setInstanceName(`${item.name} (My Copy)`); + setInstanceName(`${item.name}`); + checkRequirementsAndSetupSteps(); } else { setInstanceName(''); + setSetupSteps([]); + setMissingCredentials([]); } } - }, [item]); + }, [item, open, userCredentials]); + + const checkRequirementsAndSetupSteps = async () => { + if (!item?.mcp_requirements) return; + + setIsCheckingRequirements(true); + const userCredNames = new Set(userCredentials?.map(cred => cred.mcp_qualified_name) || []); + const steps: SetupStep[] = []; + const missing: string[] = []; + + const customServers = item.mcp_requirements.filter(req => req.custom_type); + const regularServices = item.mcp_requirements.filter(req => !req.custom_type); + + for (const req of regularServices) { + if (!userCredNames.has(req.qualified_name)) { + missing.push(req.display_name); + } + } + + for (const req of customServers) { + steps.push({ + id: req.qualified_name, + title: `Configure ${req.display_name}`, + description: `Enter your ${req.display_name} server URL to connect this agent.`, + type: 'custom_server', + service_name: req.display_name, + qualified_name: req.qualified_name, + custom_type: req.custom_type, + required_fields: req.required_config.map(key => ({ + key, + label: key === 'url' ? `${req.display_name} Server URL` : key, + type: key === 'url' ? 'url' : 'text', + placeholder: key === 'url' ? `https://your-${req.display_name.toLowerCase()}-server.com` : `Enter your ${key}`, + description: key === 'url' ? `Your personal ${req.display_name} server endpoint` : undefined + })) + }); + } + + setSetupSteps(steps); + setMissingCredentials(missing); + setIsCheckingRequirements(false); + }; + + const handleFieldChange = (stepId: string, fieldKey: string, value: string) => { + setSetupData(prev => ({ + ...prev, + [stepId]: { + ...prev[stepId], + [fieldKey]: value + } + })); + }; + + const isCurrentStepComplete = (): boolean => { + if (setupSteps.length === 0) return true; + if (currentStep >= setupSteps.length) return true; + + const step = setupSteps[currentStep]; + const stepData = setupData[step.id] || {}; + + return step.required_fields.every(field => { + const value = stepData[field.key]; + return value && value.trim().length > 0; + }); + }; + + const handleNext = () => { + if (currentStep < setupSteps.length - 1) { + setCurrentStep(currentStep + 1); + } else { + handleInstall(); + } + }; + + const handleBack = () => { + if (currentStep > 0) { + setCurrentStep(currentStep - 1); + } + }; + + const handleInstall = async () => { + if (!item) return; + + if (item.type === 'legacy' && item.agent_id) { + onInstall(item, instanceName); + return; + } + + const customMcpConfigs: Record> = {}; + setupSteps.forEach(step => { + if (step.type === 'custom_server') { + customMcpConfigs[step.qualified_name] = setupData[step.id] || {}; + } + }); + + onInstall(item, instanceName, customMcpConfigs); + }; + + const canInstall = () => { + if (!item || item.type !== 'secure') return true; + if (!instanceName.trim()) return false; + if (missingCredentials.length > 0) return false; + if (setupSteps.length > 0 && currentStep < setupSteps.length) return false; + return true; + }; if (!item) return null; return ( - - - - {item.type === 'secure' ? 'Install Secure Template' : 'Add Agent to Library'} - {item.type === 'secure' && ( - - - Secure - - )} - - - {item.type === 'secure' - ? `Install "${item.name}" as a secure agent instance` - : `Add "${item.name}" to your agent library` - } - - - -
- {item.type === 'secure' && ( -
- - setInstanceName(e.target.value)} - placeholder="Enter a name for your agent" - /> -
- )} - - {item.type === 'secure' && item.mcp_requirements && item.mcp_requirements.length > 0 && ( -
- -
- {item.mcp_requirements.map((req) => ( -
-
-

{req.display_name}

-

{req.qualified_name}

-
- - {req.enabled_tools?.length || 0} tools - -
- ))} -
- - {credentialStatus && !credentialStatus.hasAllCredentials && ( - - - - You're missing credentials for {credentialStatus.missingCount} of {credentialStatus.totalRequired} required services. - - Set them up first → - - - - )} - - {credentialStatus && credentialStatus.hasAllCredentials && ( - - - - All required credentials are configured. This agent will use your encrypted API keys. - - - )} -
- )} - - {item.type === 'legacy' && ( - - - - This is a legacy agent that may contain embedded API keys. Consider using secure templates for better security. - - - )} -
- - - - + + + +
+ {isCheckingRequirements ? ( +
+ +

Checking what you need...

+
+ ) : missingCredentials.length > 0 ? ( +
+ + + Missing API Credentials + + This agent requires API credentials for the following services: + + + +
+ {missingCredentials.map((serviceName) => ( + + +
+
+ +
+ {serviceName} +
+ Missing +
+
+ ))} +
+
+ ) : setupSteps.length === 0 ? ( +
+ {item.type === 'secure' && ( +
+ + setInstanceName(e.target.value)} + placeholder="Enter a name for this agent" + className="h-11" + /> +
+ )} + + + + + {item.type === 'secure' + ? "All requirements are configured. Ready to install!" + : "This agent will be added to your library." + } + + +
+ ) : currentStep < setupSteps.length ? ( +
+
+
+
+
+
+ {currentStep + 1} +
+

{setupSteps[currentStep].title}

+ {setupSteps[currentStep].custom_type && ( + + {setupSteps[currentStep].custom_type?.toUpperCase()} + + )} +
+

+ {setupSteps[currentStep].description} +

+
+
+ +
+ {setupSteps[currentStep].required_fields.map((field) => ( +
+ + handleFieldChange(setupSteps[currentStep].id, field.key, e.target.value)} + className="h-11" + /> + {field.description && ( +

{field.description}

+ )} +
+ ))} +
+
+
+
+ {setupSteps.map((_, index) => ( +
+ ))} +
+

+ Step {currentStep + 1} of {setupSteps.length} +

+
+
+ ) : ( +
+
+
+ +
+
+

Almost done!

+

+ Give your agent a name and we'll set everything up. +

+
+
+ +
+ + setInstanceName(e.target.value)} + placeholder="Enter a name for this agent" + className="h-11" + /> +
+
+ )} +
+ + + {missingCredentials.length > 0 ? ( +
+ + +
+ ) : ( +
+ {currentStep > 0 && ( + + )} + + {setupSteps.length === 0 ? ( + + ) : currentStep < setupSteps.length ? ( + + ) : ( + + )} +
+ )}
@@ -367,34 +619,46 @@ const AgentDetailsContent: React.FC<{

Required MCP Services

-
- {item.mcp_requirements.map((req) => ( - - -
-
-
-
- -
-

{req.display_name}

-
- {req.enabled_tools && req.enabled_tools.length > 0 && ( -
- {req.enabled_tools.map(tool => ( - - - {tool} +
+ {item.mcp_requirements.map((req) => ( + + +
+
+
+
+ +
+

{req.display_name}

+ {req.custom_type && ( + + Custom {req.custom_type.toUpperCase()} - ))} + )}
- )} +
+ {req.custom_type ? ( + Requires your own {req.custom_type.toUpperCase()} server URL + ) : ( + Requires: {req.required_config.join(', ')} + )} +
+ {req.enabled_tools && req.enabled_tools.length > 0 && ( +
+ {req.enabled_tools.map(tool => ( + + + {tool} + + ))} +
+ )} +
-
- - - ))} -
+ + + ))} +
@@ -423,141 +687,6 @@ const AgentDetailsContent: React.FC<{ ); }; -const AgentDetailsSheet: React.FC = ({ - item, - open, - onOpenChange, - onInstall, - isInstalling, - credentialStatus -}) => { - const isDesktop = useMediaQuery("(min-width: 768px)"); - const [instanceName, setInstanceName] = useState(''); - - React.useEffect(() => { - if (item) { - if (item.type === 'secure') { - setInstanceName(`${item.name} (My Copy)`); - } else { - setInstanceName(''); - } - } - }, [item]); - - if (!item) return null; - if (isDesktop) { - return ( - - - - Agent Details - - View details and install this {item.type === 'secure' ? 'secure template' : 'agent'} - - -
- -
-
- {credentialStatus && !credentialStatus.hasAllCredentials && ( - - - - You're missing credentials for {credentialStatus.missingCount} of {credentialStatus.totalRequired} required services. - - Set them up first → - - - - )} - -
-
-
- ); - } - - return ( - - - - Agent Details - - View details and install this {item.type === 'secure' ? 'secure template' : 'agent'} - - -
- -
-
- {credentialStatus && !credentialStatus.hasAllCredentials && ( - - - - You're missing credentials for {credentialStatus.missingCount} of {credentialStatus.totalRequired} required services. - - Set them up first → - - - - )} - -
-
-
- ); -}; - export default function UnifiedMarketplacePage() { const [page, setPage] = useState(1); const [searchQuery, setSearchQuery] = useState(''); @@ -678,13 +807,21 @@ export default function UnifiedMarketplacePage() { } const userCredNames = getUserCredentialNames(); - const requiredCreds = item.mcp_requirements.map(req => req.qualified_name); - const missingCreds = requiredCreds.filter(cred => !userCredNames.has(cred)); + const missingCreds = item.mcp_requirements.filter(req => { + if (req.custom_type) { + const customPattern = `custom_${req.custom_type}_`; + return !Array.from(userCredNames).some(credName => + credName.startsWith(customPattern) && + credName.includes(req.display_name.toLowerCase().replace(/\s+/g, '_')) + ); + } + return !userCredNames.has(req.qualified_name); + }); return { hasAllCredentials: missingCreds.length === 0, missingCount: missingCreds.length, - totalRequired: requiredCreds.length + totalRequired: item.mcp_requirements.length }; }; @@ -701,7 +838,7 @@ export default function UnifiedMarketplacePage() { setShowInstallDialog(true); }; - const handleInstall = async (item: UnifiedMarketplaceItem, instanceName?: string) => { + const handleInstall = async (item: UnifiedMarketplaceItem, instanceName?: string, customMcpConfigs?: Record>) => { setInstallingItemId(item.id); try { @@ -713,18 +850,29 @@ export default function UnifiedMarketplacePage() { } else if (item.type === 'secure' && item.template_id) { const result = await installTemplateMutation.mutateAsync({ template_id: item.template_id, - instance_name: instanceName + instance_name: instanceName, + custom_mcp_configs: customMcpConfigs }); if (result.status === 'installed') { toast.success('Agent installed successfully!'); setShowInstallDialog(false); setShowDetailsSheet(false); - } else if (result.status === 'credentials_required') { - setMissingCredentials(result.missing_credentials || []); - setShowInstallDialog(false); - setShowDetailsSheet(false); - setShowMissingCredsDialog(true); + } else if (result.status === 'configs_required') { + // Handle missing regular credentials + if (result.missing_regular_credentials && result.missing_regular_credentials.length > 0) { + setMissingCredentials(result.missing_regular_credentials); + setShowInstallDialog(false); + setShowDetailsSheet(false); + setShowMissingCredsDialog(true); + return; + } + + // Handle missing custom configs - this should be handled by the install dialog + if (result.missing_custom_configs && result.missing_custom_configs.length > 0) { + toast.error('Please provide all required custom MCP server configurations'); + return; + } } } } catch (error: any) { @@ -1042,22 +1190,12 @@ export default function UnifiedMarketplacePage() { )}
- - { - const [step, setStep] = useState<'browse' | 'configure'>('browse'); + const [step, setStep] = useState<'browse' | 'configure' | 'custom'>('browse'); const [selectedServer, setSelectedServer] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [selectedCategory, setSelectedCategory] = useState(null); const [currentPage, setCurrentPage] = useState(1); const [pageSize] = useState(50); + const [customServerType, setCustomServerType] = useState<'sse' | 'http'>('sse'); const [formData, setFormData] = useState<{ display_name: string; config: Record; @@ -156,6 +158,7 @@ export const EnhancedAddCredentialDialog: React.FC { - if (!selectedServer) return; - try { - const request: StoreCredentialRequest = { - mcp_qualified_name: selectedServer.qualifiedName, - display_name: formData.display_name, - config: formData.config - }; + let request: StoreCredentialRequest; + if (step === 'custom') { + const qualifiedName = `custom_${customServerType}_${formData.display_name.toLowerCase().replace(/\s+/g, '_')}`; + request = { + mcp_qualified_name: qualifiedName, + display_name: formData.display_name, + config: formData.config + }; + } else { + if (!selectedServer) return; + request = { + mcp_qualified_name: selectedServer.qualifiedName, + display_name: formData.display_name, + config: formData.config + }; + } await storeCredentialMutation.mutateAsync(request); toast.success('Credential stored successfully!'); @@ -211,6 +223,11 @@ export const EnhancedAddCredentialDialog: React.FC { + setStep('custom'); + setFormData({ display_name: '', config: {} }); + }; + const getConfigSchema = () => { return serverDetails?.connections?.[0]?.configSchema; }; @@ -251,11 +268,15 @@ export const EnhancedAddCredentialDialog: React.FC - {step === 'browse' ? 'Add MCP Credential' : `Configure ${selectedServer?.displayName}`} + {step === 'browse' ? 'Add MCP Credential' : + step === 'custom' ? 'Add Custom MCP Server' : + `Configure ${selectedServer?.displayName}`} {step === 'browse' - ? 'Select an MCP server to configure credentials for' + ? 'Select an MCP server to configure credentials for, or add a custom server' + : step === 'custom' + ? 'Configure your own custom MCP server connection' : 'Enter your API credentials for this MCP server' } @@ -263,14 +284,27 @@ export const EnhancedAddCredentialDialog: React.FC -
- - setSearchQuery(e.target.value)} - className="pl-10" - /> +
+
+ + setSearchQuery(e.target.value)} + className="pl-10" + /> +
+ +
+ +
@@ -431,8 +465,79 @@ export const EnhancedAddCredentialDialog: React.FC )} + {step === 'custom' && ( +
+
+
+ +
+
+

Custom MCP Server

+

Configure your own MCP server connection

+
+
+ +
+
+ + setFormData(prev => ({ ...prev, display_name: e.target.value }))} + placeholder="Enter a name for this custom server (e.g., 'My Tavily Server')" + /> +
+ +
+ + +

+ Choose the connection type for your MCP server +

+
+ +
+ + handleConfigChange('url', e.target.value)} + placeholder={`Enter your ${customServerType.toUpperCase()} server URL`} + /> +

+ The URL to your custom MCP server endpoint +

+
+ + + + + This will create a custom MCP server configuration that you can use in your agents. + Make sure your server URL is accessible and properly configured. + + + + + + + Your server URL will be encrypted and stored securely. It will only be used when you run agents that require this custom MCP server. + + +
+
+ )} + - {step === 'configure' && ( + {(step === 'configure' || step === 'custom') && (

{credential.display_name}

+ {isCustomServer && ( + + Custom {getCustomServerType()} + + )}

{credential.mcp_qualified_name} @@ -51,7 +63,7 @@ const CredentialCard: React.FC = ({

{credential.config_keys.map((key) => ( - {key} + {key === 'url' && isCustomServer ? 'Server URL' : key} ))}
diff --git a/frontend/src/hooks/react-query/secure-mcp/use-secure-mcp.ts b/frontend/src/hooks/react-query/secure-mcp/use-secure-mcp.ts index 65418f2b..8010e855 100644 --- a/frontend/src/hooks/react-query/secure-mcp/use-secure-mcp.ts +++ b/frontend/src/hooks/react-query/secure-mcp/use-secure-mcp.ts @@ -51,22 +51,30 @@ export interface MCPRequirement { display_name: string; enabled_tools: string[]; required_config: string[]; + custom_type?: 'sse' | 'http'; // For custom MCP servers } export interface InstallTemplateRequest { template_id: string; instance_name?: string; custom_system_prompt?: string; + custom_mcp_configs?: Record>; } export interface InstallationResponse { - status: 'installed' | 'credentials_required'; + status: 'installed' | 'configs_required'; instance_id?: string; - missing_credentials?: { + missing_regular_credentials?: { qualified_name: string; display_name: string; required_config: string[]; }[]; + missing_custom_configs?: { + qualified_name: string; + display_name: string; + custom_type: string; + required_config: string[]; + }[]; template?: { template_id: string; name: string;