mirror of https://github.com/kortix-ai/suna.git
chore(dev): improve ux of credentials manager
This commit is contained in:
parent
4ce617364f
commit
d5fb7e1c8d
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<string, Record<string, any>>) => void;
|
||||
isInstalling: boolean;
|
||||
credentialStatus?: {
|
||||
hasAllCredentials: boolean;
|
||||
missingCount: number;
|
||||
totalRequired: number;
|
||||
};
|
||||
}
|
||||
|
||||
const InstallDialog: React.FC<InstallDialogProps> = ({
|
||||
|
@ -71,130 +81,372 @@ const InstallDialog: React.FC<InstallDialogProps> = ({
|
|||
open,
|
||||
onOpenChange,
|
||||
onInstall,
|
||||
isInstalling,
|
||||
credentialStatus
|
||||
isInstalling
|
||||
}) => {
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [instanceName, setInstanceName] = useState('');
|
||||
const [setupData, setSetupData] = useState<Record<string, Record<string, string>>>({});
|
||||
const [isCheckingRequirements, setIsCheckingRequirements] = useState(false);
|
||||
const [setupSteps, setSetupSteps] = useState<SetupStep[]>([]);
|
||||
const [missingCredentials, setMissingCredentials] = useState<string[]>([]);
|
||||
|
||||
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<string, Record<string, any>> = {};
|
||||
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 (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
{item.type === 'secure' ? 'Install Secure Template' : 'Add Agent to Library'}
|
||||
{item.type === 'secure' && (
|
||||
<Badge variant="outline" className="bg-green-50 text-green-700 border-green-200">
|
||||
<Shield className="h-3 w-3 mr-1" />
|
||||
Secure
|
||||
</Badge>
|
||||
)}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{item.type === 'secure'
|
||||
? `Install "${item.name}" as a secure agent instance`
|
||||
: `Add "${item.name}" to your agent library`
|
||||
}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{item.type === 'secure' && (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Agent Name</label>
|
||||
<Input
|
||||
value={instanceName}
|
||||
onChange={(e) => setInstanceName(e.target.value)}
|
||||
placeholder="Enter a name for your agent"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item.type === 'secure' && item.mcp_requirements && item.mcp_requirements.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Required MCP Services</label>
|
||||
<div className="space-y-2">
|
||||
{item.mcp_requirements.map((req) => (
|
||||
<div key={req.qualified_name} className="flex items-center justify-between p-2 border rounded">
|
||||
<div>
|
||||
<p className="text-sm font-medium">{req.display_name}</p>
|
||||
<p className="text-xs text-muted-foreground">{req.qualified_name}</p>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{req.enabled_tools?.length || 0} tools
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{credentialStatus && !credentialStatus.hasAllCredentials && (
|
||||
<Alert className='border-destructive/20 bg-destructive/5 text-destructive'>
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription className="text-xs text-destructive">
|
||||
You're missing credentials for {credentialStatus.missingCount} of {credentialStatus.totalRequired} required services.
|
||||
<Link href="/settings/credentials" className="ml-1 underline">
|
||||
Set them up first →
|
||||
</Link>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{credentialStatus && credentialStatus.hasAllCredentials && (
|
||||
<Alert>
|
||||
<CheckCircle className="h-4 w-4" />
|
||||
<AlertDescription className="text-xs">
|
||||
All required credentials are configured. This agent will use your encrypted API keys.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item.type === 'legacy' && (
|
||||
<Alert>
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription className="text-xs">
|
||||
This is a legacy agent that may contain embedded API keys. Consider using secure templates for better security.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => onInstall(item, instanceName)}
|
||||
disabled={
|
||||
isInstalling ||
|
||||
(item.type === 'secure' && !instanceName.trim()) ||
|
||||
(item.type === 'secure' && credentialStatus && !credentialStatus.hasAllCredentials)
|
||||
}
|
||||
>
|
||||
{isInstalling ? (
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader className="space-y-3">
|
||||
<DialogTitle className="flex items-center gap-3 text-xl">
|
||||
{item.type === 'secure' ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||||
{item.type === 'secure' ? 'Installing...' : 'Adding...'}
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-green-100 dark:bg-green-900/20">
|
||||
<Shield className="h-5 w-5 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
Install {item.name}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
{item.type === 'secure' ? 'Install Agent' : 'Add to Library'}
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900/20">
|
||||
<Download className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
Add {item.name}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6 py-4">
|
||||
{isCheckingRequirements ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 space-y-4">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
<p className="text-sm text-muted-foreground">Checking what you need...</p>
|
||||
</div>
|
||||
) : missingCredentials.length > 0 ? (
|
||||
<div className="space-y-6">
|
||||
<Alert className="border-destructive/50 bg-destructive/10 dark:bg-destructive/5 text-destructive">
|
||||
<AlertTriangle className="h-4 w-4 text-destructive" />
|
||||
<AlertTitle className="text-destructive">Missing API Credentials</AlertTitle>
|
||||
<AlertDescription className="text-destructive/80">
|
||||
This agent requires API credentials for the following services:
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="space-y-3">
|
||||
{missingCredentials.map((serviceName) => (
|
||||
<Card key={serviceName} className="border-destructive/20 shadow-none bg-transparent">
|
||||
<CardContent className="flex items-center justify-between p-4 py-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-destructive/10">
|
||||
<AlertTriangle className="h-4 w-4 text-destructive" />
|
||||
</div>
|
||||
<span className="font-medium">{serviceName}</span>
|
||||
</div>
|
||||
<Badge variant="destructive">Missing</Badge>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : setupSteps.length === 0 ? (
|
||||
<div className="space-y-6">
|
||||
{item.type === 'secure' && (
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
||||
Agent Name
|
||||
</label>
|
||||
<Input
|
||||
value={instanceName}
|
||||
onChange={(e) => setInstanceName(e.target.value)}
|
||||
placeholder="Enter a name for this agent"
|
||||
className="h-11"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Alert className="border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950/50">
|
||||
<CheckCircle className="h-4 w-4 text-green-600 dark:text-green-400" />
|
||||
<AlertDescription className="text-green-800 dark:text-green-200">
|
||||
{item.type === 'secure'
|
||||
? "All requirements are configured. Ready to install!"
|
||||
: "This agent will be added to your library."
|
||||
}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
) : currentStep < setupSteps.length ? (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="space-y-2 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-primary text-primary-foreground text-sm font-semibold">
|
||||
{currentStep + 1}
|
||||
</div>
|
||||
<h3 className="font-semibold text-base">{setupSteps[currentStep].title}</h3>
|
||||
{setupSteps[currentStep].custom_type && (
|
||||
<Badge variant="outline" className="text-xs bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-950 dark:text-blue-300 dark:border-blue-800">
|
||||
{setupSteps[currentStep].custom_type?.toUpperCase()}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{setupSteps[currentStep].description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{setupSteps[currentStep].required_fields.map((field) => (
|
||||
<div key={field.key} className="space-y-2">
|
||||
<label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
||||
{field.label}
|
||||
</label>
|
||||
<Input
|
||||
type={field.type}
|
||||
placeholder={field.placeholder}
|
||||
value={setupData[setupSteps[currentStep].id]?.[field.key] || ''}
|
||||
onChange={(e) => handleFieldChange(setupSteps[currentStep].id, field.key, e.target.value)}
|
||||
className="h-11"
|
||||
/>
|
||||
{field.description && (
|
||||
<p className="text-xs text-muted-foreground">{field.description}</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{setupSteps.map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`h-1 flex-1 rounded-full transition-colors ${
|
||||
index <= currentStep ? 'bg-primary' : 'bg-muted'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Step {currentStep + 1} of {setupSteps.length}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-green-100 dark:bg-green-900/20">
|
||||
<CheckCircle className="h-5 w-5 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<div className="space-y-1 flex-1">
|
||||
<h3 className="font-semibold text-base">Almost done!</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Give your agent a name and we'll set everything up.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ml-12 space-y-3">
|
||||
<label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
||||
Agent Name
|
||||
</label>
|
||||
<Input
|
||||
value={instanceName}
|
||||
onChange={(e) => setInstanceName(e.target.value)}
|
||||
placeholder="Enter a name for this agent"
|
||||
className="h-11"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex-col gap-3">
|
||||
{missingCredentials.length > 0 ? (
|
||||
<div className="flex gap-3 w-full justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => window.open('/settings/credentials', '_blank')}
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
Set Up Credentials
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex gap-3 w-full justify-end">
|
||||
{currentStep > 0 && (
|
||||
<Button variant="outline" onClick={handleBack} className="flex-1">
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{setupSteps.length === 0 ? (
|
||||
<Button
|
||||
onClick={handleInstall}
|
||||
disabled={isInstalling || !canInstall()}
|
||||
>
|
||||
{isInstalling ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
{item.type === 'secure' ? 'Installing...' : 'Adding...'}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="h-4 w-4" />
|
||||
{item.type === 'secure' ? 'Install Agent' : 'Add to Library'}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
) : currentStep < setupSteps.length ? (
|
||||
<Button
|
||||
onClick={handleNext}
|
||||
disabled={!isCurrentStepComplete()}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={handleInstall}
|
||||
disabled={isInstalling || !canInstall()}
|
||||
>
|
||||
{isInstalling ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Installing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="h-4 w-4" />
|
||||
Install Agent
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
@ -367,34 +619,46 @@ const AgentDetailsContent: React.FC<{
|
|||
<h3 className="text-md font-medium">Required MCP Services</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{item.mcp_requirements.map((req) => (
|
||||
<Card key={req.qualified_name} className="border-border/50">
|
||||
<CardContent className="p-4 py-0">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-1.5 rounded-md bg-primary/10">
|
||||
<Zap className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
<h4 className="font-medium">{req.display_name}</h4>
|
||||
</div>
|
||||
{req.enabled_tools && req.enabled_tools.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{req.enabled_tools.map(tool => (
|
||||
<Badge key={tool} variant="outline" className="text-xs">
|
||||
<Wrench className="h-3 w-3 mr-1" />
|
||||
{tool}
|
||||
<div className="space-y-3">
|
||||
{item.mcp_requirements.map((req) => (
|
||||
<Card key={req.qualified_name} className="border-border/50">
|
||||
<CardContent className="p-4 py-0">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-1.5 rounded-md bg-primary/10">
|
||||
<Zap className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
<h4 className="font-medium">{req.display_name}</h4>
|
||||
{req.custom_type && (
|
||||
<Badge variant="outline" className="text-xs bg-blue-50 text-blue-700 border-blue-200">
|
||||
Custom {req.custom_type.toUpperCase()}
|
||||
</Badge>
|
||||
))}
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{req.custom_type ? (
|
||||
<span>Requires your own {req.custom_type.toUpperCase()} server URL</span>
|
||||
) : (
|
||||
<span>Requires: {req.required_config.join(', ')}</span>
|
||||
)}
|
||||
</div>
|
||||
{req.enabled_tools && req.enabled_tools.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{req.enabled_tools.map(tool => (
|
||||
<Badge key={tool} variant="outline" className="text-xs">
|
||||
<Wrench className="h-3 w-3 mr-1" />
|
||||
{tool}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
@ -423,141 +687,6 @@ const AgentDetailsContent: React.FC<{
|
|||
);
|
||||
};
|
||||
|
||||
const AgentDetailsSheet: React.FC<AgentDetailsProps> = ({
|
||||
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 (
|
||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||
<SheetContent className="w-[600px] sm:max-w-[600px] overflow-y-auto flex flex-col">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Agent Details</SheetTitle>
|
||||
<SheetDescription>
|
||||
View details and install this {item.type === 'secure' ? 'secure template' : 'agent'}
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="flex-1 overflow-y-auto px-4 mt-6">
|
||||
<AgentDetailsContent
|
||||
item={item}
|
||||
instanceName={instanceName}
|
||||
setInstanceName={setInstanceName}
|
||||
/>
|
||||
</div>
|
||||
<div className="border-t p-4 mt-4">
|
||||
{credentialStatus && !credentialStatus.hasAllCredentials && (
|
||||
<Alert className="border-destructive/20 bg-destructive/5 text-destructive mb-4">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription className="text-sm text-destructive">
|
||||
You're missing credentials for {credentialStatus.missingCount} of {credentialStatus.totalRequired} required services.
|
||||
<Link href="/settings/credentials" className="ml-1 underline">
|
||||
Set them up first →
|
||||
</Link>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => onInstall(item, instanceName)}
|
||||
disabled={
|
||||
isInstalling ||
|
||||
(item.type === 'secure' && !instanceName.trim()) ||
|
||||
(item.type === 'secure' && credentialStatus && !credentialStatus.hasAllCredentials)
|
||||
}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
>
|
||||
{isInstalling ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||||
{item.type === 'secure' ? 'Installing...' : 'Adding...'}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
{item.type === 'secure' ? 'Install Agent' : 'Add to Library'}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Drawer open={open} onOpenChange={onOpenChange}>
|
||||
<DrawerContent className="h-screen flex flex-col">
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>Agent Details</DrawerTitle>
|
||||
<DrawerDescription>
|
||||
View details and install this {item.type === 'secure' ? 'secure template' : 'agent'}
|
||||
</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
<div className="flex-1 overflow-y-auto px-4">
|
||||
<AgentDetailsContent
|
||||
item={item}
|
||||
instanceName={instanceName}
|
||||
setInstanceName={setInstanceName}
|
||||
/>
|
||||
</div>
|
||||
<div className="border-t p-4 mt-4">
|
||||
{credentialStatus && !credentialStatus.hasAllCredentials && (
|
||||
<Alert className="border-destructive/20 bg-destructive/5 text-destructive mb-4">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription className="text-sm text-destructive">
|
||||
You're missing credentials for {credentialStatus.missingCount} of {credentialStatus.totalRequired} required services.
|
||||
<Link href="/settings/credentials" className="ml-1 underline">
|
||||
Set them up first →
|
||||
</Link>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => onInstall(item, instanceName)}
|
||||
disabled={
|
||||
isInstalling ||
|
||||
(item.type === 'secure' && !instanceName.trim()) ||
|
||||
(item.type === 'secure' && credentialStatus && !credentialStatus.hasAllCredentials)
|
||||
}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
>
|
||||
{isInstalling ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||||
{item.type === 'secure' ? 'Installing...' : 'Adding...'}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
{item.type === 'secure' ? 'Install Agent' : 'Add to Library'}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
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<string, Record<string, any>>) => {
|
||||
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() {
|
|||
)}
|
||||
</div>
|
||||
|
||||
<AgentDetailsSheet
|
||||
item={selectedItem}
|
||||
open={showDetailsSheet}
|
||||
onOpenChange={setShowDetailsSheet}
|
||||
onInstall={handleInstall}
|
||||
isInstalling={installingItemId === selectedItem?.id}
|
||||
credentialStatus={selectedItem ? getItemCredentialStatus(selectedItem) : undefined}
|
||||
/>
|
||||
|
||||
<InstallDialog
|
||||
item={selectedItem}
|
||||
open={showInstallDialog}
|
||||
onOpenChange={setShowInstallDialog}
|
||||
onInstall={handleInstall}
|
||||
isInstalling={installingItemId === selectedItem?.id}
|
||||
credentialStatus={selectedItem ? getItemCredentialStatus(selectedItem) : undefined}
|
||||
/>
|
||||
|
||||
<MissingCredentialsDialog
|
||||
|
|
|
@ -6,7 +6,8 @@ import { Button } from '@/components/ui/button';
|
|||
import { Label } from '@/components/ui/label';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { Search, ChevronLeft, ChevronRight, Shield, Loader2, ArrowLeft, Key, ExternalLink } from 'lucide-react';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Search, ChevronLeft, ChevronRight, Shield, Loader2, ArrowLeft, Key, ExternalLink, Plus, Globe } from 'lucide-react';
|
||||
import { usePopularMCPServersV2, useMCPServers, useMCPServerDetails } from '@/hooks/react-query/mcp/use-mcp-servers';
|
||||
import { useStoreCredential, type StoreCredentialRequest } from '@/hooks/react-query/secure-mcp/use-secure-mcp';
|
||||
import { toast } from 'sonner';
|
||||
|
@ -119,12 +120,13 @@ export const EnhancedAddCredentialDialog: React.FC<EnhancedAddCredentialDialogPr
|
|||
onOpenChange,
|
||||
onSuccess
|
||||
}) => {
|
||||
const [step, setStep] = useState<'browse' | 'configure'>('browse');
|
||||
const [step, setStep] = useState<'browse' | 'configure' | 'custom'>('browse');
|
||||
const [selectedServer, setSelectedServer] = useState<any>(null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(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<string, string>;
|
||||
|
@ -156,6 +158,7 @@ export const EnhancedAddCredentialDialog: React.FC<EnhancedAddCredentialDialogPr
|
|||
setSearchQuery('');
|
||||
setStep('browse');
|
||||
setSelectedServer(null);
|
||||
setCustomServerType('sse');
|
||||
setFormData({ display_name: '', config: {} });
|
||||
}
|
||||
}, [open]);
|
||||
|
@ -188,14 +191,23 @@ export const EnhancedAddCredentialDialog: React.FC<EnhancedAddCredentialDialogPr
|
|||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
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<EnhancedAddCredentialDialogPr
|
|||
setSelectedServer(null);
|
||||
};
|
||||
|
||||
const handleCustomServerSetup = () => {
|
||||
setStep('custom');
|
||||
setFormData({ display_name: '', config: {} });
|
||||
};
|
||||
|
||||
const getConfigSchema = () => {
|
||||
return serverDetails?.connections?.[0]?.configSchema;
|
||||
};
|
||||
|
@ -251,11 +268,15 @@ export const EnhancedAddCredentialDialog: React.FC<EnhancedAddCredentialDialogPr
|
|||
<DialogContent className="max-w-6xl max-h-[85vh] overflow-hidden flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{step === 'browse' ? 'Add MCP Credential' : `Configure ${selectedServer?.displayName}`}
|
||||
{step === 'browse' ? 'Add MCP Credential' :
|
||||
step === 'custom' ? 'Add Custom MCP Server' :
|
||||
`Configure ${selectedServer?.displayName}`}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{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'
|
||||
}
|
||||
</DialogDescription>
|
||||
|
@ -263,14 +284,27 @@ export const EnhancedAddCredentialDialog: React.FC<EnhancedAddCredentialDialogPr
|
|||
|
||||
{step === 'browse' && (
|
||||
<>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search MCP servers..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
<div className="space-y-3">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search MCP servers..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleCustomServerSetup}
|
||||
className="gap-2"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Add Custom MCP Server
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 gap-4 overflow-hidden">
|
||||
|
@ -431,8 +465,79 @@ export const EnhancedAddCredentialDialog: React.FC<EnhancedAddCredentialDialogPr
|
|||
</div>
|
||||
)}
|
||||
|
||||
{step === 'custom' && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-3 p-4 bg-muted/50 rounded-lg">
|
||||
<div className="w-12 h-12 rounded-md bg-primary/10 flex items-center justify-center">
|
||||
<Globe className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium">Custom MCP Server</h3>
|
||||
<p className="text-sm text-muted-foreground">Configure your own MCP server connection</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="custom_display_name">Display Name *</Label>
|
||||
<Input
|
||||
id="custom_display_name"
|
||||
value={formData.display_name}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, display_name: e.target.value }))}
|
||||
placeholder="Enter a name for this custom server (e.g., 'My Tavily Server')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="server_type">Server Type *</Label>
|
||||
<Select value={customServerType} onValueChange={(value: 'sse' | 'http') => setCustomServerType(value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select server type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="sse">SSE (Server-Sent Events)</SelectItem>
|
||||
<SelectItem value="http">HTTP</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Choose the connection type for your MCP server
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="server_url">Server URL *</Label>
|
||||
<Input
|
||||
id="server_url"
|
||||
type="url"
|
||||
value={formData.config.url || ''}
|
||||
onChange={(e) => handleConfigChange('url', e.target.value)}
|
||||
placeholder={`Enter your ${customServerType.toUpperCase()} server URL`}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
The URL to your custom MCP server endpoint
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Alert>
|
||||
<Globe className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
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.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<Alert>
|
||||
<Shield className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Your server URL will be encrypted and stored securely. It will only be used when you run agents that require this custom MCP server.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
{step === 'configure' && (
|
||||
{(step === 'configure' || step === 'custom') && (
|
||||
<Button variant="outline" onClick={handleBack}>
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Back
|
||||
|
@ -440,12 +545,19 @@ export const EnhancedAddCredentialDialog: React.FC<EnhancedAddCredentialDialogPr
|
|||
)}
|
||||
<Button
|
||||
onClick={step === 'browse' ? () => onOpenChange(false) : handleSubmit}
|
||||
disabled={step === 'configure' && (
|
||||
!formData.display_name ||
|
||||
(getRequiredFields().length > 0 &&
|
||||
getRequiredFields().some(field => !formData.config[field])) ||
|
||||
storeCredentialMutation.isPending
|
||||
)}
|
||||
disabled={
|
||||
(step === 'configure' && (
|
||||
!formData.display_name ||
|
||||
(getRequiredFields().length > 0 &&
|
||||
getRequiredFields().some(field => !formData.config[field])) ||
|
||||
storeCredentialMutation.isPending
|
||||
)) ||
|
||||
(step === 'custom' && (
|
||||
!formData.display_name ||
|
||||
!formData.config.url ||
|
||||
storeCredentialMutation.isPending
|
||||
))
|
||||
}
|
||||
>
|
||||
{step === 'browse' ? 'Cancel' :
|
||||
storeCredentialMutation.isPending ? (
|
||||
|
|
|
@ -33,6 +33,13 @@ const CredentialCard: React.FC<CredentialCardProps> = ({
|
|||
}) => {
|
||||
const isTesting = isTestingId === credential.mcp_qualified_name;
|
||||
const isDeleting = isDeletingId === credential.mcp_qualified_name;
|
||||
const isCustomServer = credential.mcp_qualified_name.startsWith('custom_');
|
||||
|
||||
const getCustomServerType = () => {
|
||||
if (credential.mcp_qualified_name.startsWith('custom_sse_')) return 'SSE';
|
||||
if (credential.mcp_qualified_name.startsWith('custom_http_')) return 'HTTP';
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="border-border/50 hover:border-border transition-colors">
|
||||
|
@ -44,6 +51,11 @@ const CredentialCard: React.FC<CredentialCardProps> = ({
|
|||
<Key className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
<h3 className="text-md font-medium text-foreground">{credential.display_name}</h3>
|
||||
{isCustomServer && (
|
||||
<Badge variant="outline" className="text-xs bg-blue-50 text-blue-700 border-blue-200">
|
||||
Custom {getCustomServerType()}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground font-mono">
|
||||
{credential.mcp_qualified_name}
|
||||
|
@ -51,7 +63,7 @@ const CredentialCard: React.FC<CredentialCardProps> = ({
|
|||
<div className="flex flex-wrap gap-1">
|
||||
{credential.config_keys.map((key) => (
|
||||
<Badge key={key} variant="outline">
|
||||
{key}
|
||||
{key === 'url' && isCustomServer ? 'Server URL' : key}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -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<string, Record<string, any>>;
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
Loading…
Reference in New Issue