chore(ui): sync custom agents config with credentials profile

This commit is contained in:
Soumyadas15 2025-06-28 16:07:09 +05:30
parent 720e31ad78
commit 333777213e
15 changed files with 912 additions and 432 deletions

View File

@ -1,13 +1,15 @@
import React, { useState } from 'react';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Loader2, Save, Sparkles } from 'lucide-react';
import { Loader2, Save, Sparkles, AlertTriangle } from 'lucide-react';
import { useMCPServerDetails } from '@/hooks/react-query/mcp/use-mcp-servers';
import { CredentialProfileSelector } from '@/components/workflows/CredentialProfileSelector';
import { useCredentialProfilesForMcp, type CredentialProfile } from '@/hooks/react-query/mcp/use-credential-profiles';
import { cn } from '@/lib/utils';
import { MCPConfiguration } from './types';
import { Card, CardContent } from '@/components/ui/card';
import { Alert, AlertDescription } from '@/components/ui/alert';
interface ConfigDialogProps {
server: any;
@ -22,20 +24,27 @@ export const ConfigDialog: React.FC<ConfigDialogProps> = ({
onSave,
onCancel
}) => {
const [config, setConfig] = useState<Record<string, any>>(existingConfig?.config || {});
const [selectedTools, setSelectedTools] = useState<Set<string>>(
new Set(existingConfig?.enabledTools || [])
);
const [selectedProfileId, setSelectedProfileId] = useState<string | null>(
existingConfig?.selectedProfileId || null
);
const { data: serverDetails, isLoading } = useMCPServerDetails(server.qualifiedName);
const requiresConfig = serverDetails?.connections?.[0]?.configSchema?.properties &&
Object.keys(serverDetails.connections[0].configSchema.properties).length > 0;
const handleSave = () => {
onSave({
const mcpConfig: MCPConfiguration = {
name: server.displayName || server.name || server.qualifiedName,
qualifiedName: server.qualifiedName,
config,
config: {}, // Always use empty config since we're using profiles
enabledTools: Array.from(selectedTools),
});
selectedProfileId: selectedProfileId || undefined,
};
onSave(mcpConfig);
};
const handleToolToggle = (toolName: string) => {
@ -48,6 +57,10 @@ export const ConfigDialog: React.FC<ConfigDialogProps> = ({
setSelectedTools(newTools);
};
const handleProfileSelect = (profileId: string | null, profile: CredentialProfile | null) => {
setSelectedProfileId(profileId);
};
return (
<DialogContent className="max-w-5xl max-h-[85vh] overflow-hidden flex flex-col">
<DialogHeader className="flex-shrink-0">
@ -86,32 +99,32 @@ export const ConfigDialog: React.FC<ConfigDialogProps> = ({
<div className="w-1 h-4 bg-primary rounded-full" />
Connection Settings
</h3>
{serverDetails?.connections?.[0]?.configSchema?.properties ? (
<ScrollArea className="border bg-muted/30 rounded-lg p-4 flex-1 min-h-0">
<div>
{Object.entries(serverDetails.connections[0].configSchema.properties).map(([key, schema]: [string, any]) => (
<div key={key} className="space-y-2">
<Label htmlFor={key} className="text-sm font-medium">
{schema.title || key}
{serverDetails.connections[0].configSchema.required?.includes(key) && (
<span className="text-destructive ml-1">*</span>
)}
</Label>
<Input
id={key}
type={schema.format === 'password' ? 'password' : 'text'}
placeholder={schema.description || `Enter ${key}`}
value={config[key] || ''}
onChange={(e) => setConfig({ ...config, [key]: e.target.value })}
className="bg-background"
/>
</div>
))}
</div>
</ScrollArea>
{requiresConfig ? (
<div className="space-y-4">
<CredentialProfileSelector
mcpQualifiedName={server.qualifiedName}
mcpDisplayName={server.displayName || server.name}
selectedProfileId={selectedProfileId || undefined}
onProfileSelect={handleProfileSelect}
/>
{!selectedProfileId && (
<Alert>
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
Please select or create a credential profile to configure this MCP server.
</AlertDescription>
</Alert>
)}
</div>
) : (
<div className="flex-1 flex items-center justify-center text-muted-foreground">
<p className="text-sm">No configuration required</p>
<Card className="border-dashed border-2 w-full">
<CardContent className="p-6 text-center">
<p className="text-sm">No configuration required for this MCP server</p>
</CardContent>
</Card>
</div>
)}
</div>
@ -164,7 +177,11 @@ export const ConfigDialog: React.FC<ConfigDialogProps> = ({
</ScrollArea>
) : (
<div className="flex-1 flex items-center justify-center text-muted-foreground">
<p className="text-sm">No tools available</p>
<Card className="border-dashed border-2 w-full">
<CardContent className="p-6 text-center">
<p className="text-sm">No tools available for this MCP server</p>
</CardContent>
</Card>
</div>
)}
</div>
@ -177,7 +194,7 @@ export const ConfigDialog: React.FC<ConfigDialogProps> = ({
</Button>
<Button
onClick={handleSave}
disabled={isLoading}
disabled={isLoading || (requiresConfig && !selectedProfileId)}
className="bg-primary hover:bg-primary/90"
>
<Save className="h-4 w-4" />

View File

@ -1,9 +1,10 @@
import React from 'react';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Settings, X, Sparkles } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { Settings, X, Sparkles, Key, AlertTriangle } from 'lucide-react';
import { MCPConfiguration } from './types';
import { useCredentialProfilesForMcp } from '@/hooks/react-query/mcp/use-credential-profiles';
interface ConfiguredMcpListProps {
configuredMCPs: MCPConfiguration[];
@ -11,6 +12,74 @@ interface ConfiguredMcpListProps {
onRemove: (index: number) => void;
}
const MCPConfigurationItem: React.FC<{
mcp: MCPConfiguration;
index: number;
onEdit: (index: number) => void;
onRemove: (index: number) => void;
}> = ({ mcp, index, onEdit, onRemove }) => {
const { data: profiles = [] } = useCredentialProfilesForMcp(mcp.qualifiedName);
const selectedProfile = profiles.find(p => p.profile_id === mcp.selectedProfileId);
const hasCredentialProfile = !!mcp.selectedProfileId && !!selectedProfile;
const needsConfiguration = !hasCredentialProfile;
return (
<Card className="p-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className="w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center flex-shrink-0">
<Sparkles className="h-4 w-4 text-primary" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<div className="font-medium text-sm truncate">{mcp.name}</div>
{mcp.isCustom && (
<Badge variant="outline" className="text-xs">
Custom
</Badge>
)}
</div>
<div className="flex items-center gap-3 text-xs text-muted-foreground">
<span>{mcp.enabledTools?.length || 0} tools enabled</span>
{hasCredentialProfile && (
<div className="flex items-center gap-1">
<Key className="h-3 w-3 text-green-600" />
<span className="text-green-600 font-medium truncate max-w-24">
{selectedProfile.profile_name}
</span>
</div>
)}
{needsConfiguration && !mcp.isCustom && (
<div className="flex items-center gap-1">
<AlertTriangle className="h-3 w-3 text-amber-600" />
<span className="text-amber-600">Needs config</span>
</div>
)}
</div>
</div>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<Button
size="sm"
variant="ghost"
onClick={() => onEdit(index)}
>
<Settings className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => onRemove(index)}
>
<X className="h-4 w-4" />
</Button>
</div>
</div>
</Card>
);
};
export const ConfiguredMcpList: React.FC<ConfiguredMcpListProps> = ({
configuredMCPs,
onEdit,
@ -21,37 +90,13 @@ export const ConfiguredMcpList: React.FC<ConfiguredMcpListProps> = ({
return (
<div className="space-y-2">
{configuredMCPs.map((mcp, index) => (
<Card key={index} className="p-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center">
<Sparkles className="h-4 w-4 text-primary" />
</div>
<div>
<div className="font-medium text-sm">{mcp.name}</div>
<div className="text-xs text-muted-foreground">
{mcp.enabledTools?.length || 0} tools enabled
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Button
size="sm"
variant="ghost"
onClick={() => onEdit(index)}
>
<Settings className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => onRemove(index)}
>
<X className="h-4 w-4" />
</Button>
</div>
</div>
</Card>
<MCPConfigurationItem
key={index}
mcp={mcp}
index={index}
onEdit={onEdit}
onRemove={onRemove}
/>
))}
</div>
);

View File

@ -11,6 +11,7 @@ import { Checkbox } from '@/components/ui/checkbox';
import { cn } from '@/lib/utils';
import { createClient } from '@/lib/supabase/client';
import { Input } from '@/components/ui/input';
import { CredentialProfile } from '@/hooks/react-query/mcp/use-credential-profiles';
const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL || '';
@ -25,6 +26,7 @@ interface CustomMCPConfiguration {
type: 'http' | 'sse';
config: any;
enabledTools: string[];
selectedProfileId?: string;
}
interface MCPTool {
@ -119,6 +121,16 @@ export const CustomMCPDialog: React.FC<CustomMCPDialogProps> = ({
}
};
const handleToolsNext = () => {
if (selectedTools.size === 0) {
setValidationError('Please select at least one tool to continue.');
return;
}
setValidationError(null);
// Custom MCPs don't need credentials, so save directly
handleSave();
};
const handleSave = () => {
if (discoveredTools.length === 0 || selectedTools.size === 0) {
setValidationError('Please select at least one tool to continue.');
@ -137,7 +149,9 @@ export const CustomMCPDialog: React.FC<CustomMCPDialogProps> = ({
name: serverName,
type: serverType,
config: configToSave,
enabledTools: Array.from(selectedTools)
enabledTools: Array.from(selectedTools),
// Custom MCPs don't need credential profiles since they're just URLs
selectedProfileId: undefined
});
setConfigText('');
@ -146,6 +160,7 @@ export const CustomMCPDialog: React.FC<CustomMCPDialogProps> = ({
setSelectedTools(new Set());
setServerName('');
setProcessedConfig(null);
setValidationError(null);
setStep('setup');
onOpenChange(false);
@ -165,7 +180,9 @@ export const CustomMCPDialog: React.FC<CustomMCPDialogProps> = ({
};
const handleBack = () => {
setStep('setup');
if (step === 'tools') {
setStep('setup');
}
setValidationError(null);
};
@ -176,6 +193,7 @@ export const CustomMCPDialog: React.FC<CustomMCPDialogProps> = ({
setSelectedTools(new Set());
setServerName('');
setProcessedConfig(null);
setValidationError(null);
setStep('setup');
};
@ -325,7 +343,7 @@ export const CustomMCPDialog: React.FC<CustomMCPDialogProps> = ({
</Alert>
)}
</div>
) : (
) : step === 'tools' ? (
<div className="space-y-6 p-1 flex-1 flex flex-col">
<Alert className="border-green-200 bg-green-50 text-green-800">
<CheckCircle2 className="h-5 w-5 text-green-600" />
@ -409,7 +427,7 @@ export const CustomMCPDialog: React.FC<CustomMCPDialogProps> = ({
</Alert>
)}
</div>
)}
) : null}
</div>
<DialogFooter className="flex-shrink-0 pt-4">
@ -422,7 +440,7 @@ export const CustomMCPDialog: React.FC<CustomMCPDialogProps> = ({
Cancel
</Button>
<Button
onClick={handleSave}
onClick={handleToolsNext}
disabled={selectedTools.size === 0}
>
Add Connection ({selectedTools.size} tools)

View File

@ -67,6 +67,7 @@ export const MCPConfigurationNew: React.FC<MCPConfigurationProps> = ({
qualifiedName: `custom_${customConfig.type}_${Date.now()}`,
config: customConfig.config,
enabledTools: customConfig.enabledTools,
selectedProfileId: customConfig.selectedProfileId,
isCustom: true,
customType: customConfig.type as 'http' | 'sse'
};

View File

@ -3,6 +3,7 @@ export interface MCPConfiguration {
qualifiedName: string;
config: Record<string, any>;
enabledTools?: string[];
selectedProfileId?: string;
isCustom?: boolean;
customType?: 'http' | 'sse';
}

View File

@ -1,5 +1,10 @@
import React from 'react';
import { Button } from '@/components/ui/button';
import { Plus, FileText, Loader2 } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { useCreateAgent } from '@/hooks/react-query/agents/use-agents';
import { DEFAULT_AGENTPRESS_TOOLS } from '../_data/tools';
import { generateRandomAvatar } from '../_utils/_avatar-generator';
interface ResultsInfoProps {
isLoading: boolean;
@ -22,6 +27,40 @@ export const ResultsInfo = ({
currentPage,
totalPages
}: ResultsInfoProps) => {
const router = useRouter();
const createAgentMutation = useCreateAgent();
const handleCreateNewAgent = async () => {
try {
const { avatar, avatar_color } = generateRandomAvatar();
const defaultAgentData = {
name: 'New Agent',
description: 'A newly created agent',
system_prompt: 'You are a helpful assistant. Provide clear, accurate, and helpful responses to user queries.',
avatar,
avatar_color,
configured_mcps: [],
agentpress_tools: Object.fromEntries(
Object.entries(DEFAULT_AGENTPRESS_TOOLS).map(([key, value]) => [
key,
{ enabled: value.enabled, description: value.description }
])
),
is_default: false,
};
const newAgent = await createAgentMutation.mutateAsync(defaultAgentData);
router.push(`/agents/new/${newAgent.agent_id}`);
} catch (error) {
console.error('Error creating agent:', error);
}
};
const handleMyTemplates = () => {
router.push('/marketplace/my-templates');
};
if (isLoading || totalAgents === 0) {
return null;
}
@ -39,11 +78,31 @@ export const ResultsInfo = ({
{showingText()}
{searchQuery && ` for "${searchQuery}"`}
</span>
{activeFiltersCount > 0 && (
<Button variant="ghost" size="sm" onClick={clearFilters} className="h-auto p-0">
Clear all filters
<div className="flex items-center gap-2">
{activeFiltersCount > 0 && (
<Button variant="ghost" size="sm" onClick={clearFilters} className="h-auto p-0">
Clear all filters
</Button>
)}
<Button variant="outline" size="sm" onClick={handleMyTemplates}>
<FileText className="h-4 w-4" />
My Templates
</Button>
)}
<Button
size="sm"
onClick={handleCreateNewAgent}
disabled={createAgentMutation.isPending}
>
{createAgentMutation.isPending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<>
<Plus className="h-4 w-4" />
New Agent
</>
)}
</Button>
</div>
</div>
);
}

View File

@ -271,70 +271,32 @@ const InstallDialog: React.FC<InstallDialogProps> = ({
}
}, [item, open]);
// Add a function to refresh requirements when profiles are created
const refreshRequirements = React.useCallback(() => {
if (item && open) {
setIsCheckingRequirements(true);
checkRequirementsAndSetupSteps();
}
}, [item, open]);
const checkRequirementsAndSetupSteps = async () => {
if (!item?.mcp_requirements) return;
const steps: SetupStep[] = [];
const missing: MissingProfile[] = [];
const customServers = item.mcp_requirements.filter(req => req.custom_type);
const regularServices = item.mcp_requirements.filter(req => !req.custom_type);
// Check for credential profiles for regular services and create profile selection steps
for (const req of regularServices) {
try {
const supabase = createClient();
const { data: { session } } = await supabase.auth.getSession();
if (!session) {
missing.push({
qualified_name: req.qualified_name,
display_name: req.display_name,
required_config: req.required_config
});
continue;
}
const response = await fetch(`${process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000'}/secure-mcp/credential-profiles/${encodeURIComponent(req.qualified_name)}`, {
headers: {
'Authorization': `Bearer ${session.access_token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const profiles = await response.json();
if (!profiles || profiles.length === 0) {
missing.push({
qualified_name: req.qualified_name,
display_name: req.display_name,
required_config: req.required_config
});
} else {
steps.push({
id: req.qualified_name,
title: `Select Credential Profile for ${req.display_name}`,
description: `Choose which credential profile to use for ${req.display_name}.`,
type: 'credential_profile',
service_name: req.display_name,
qualified_name: req.qualified_name,
});
}
} else {
missing.push({
qualified_name: req.qualified_name,
display_name: req.display_name,
required_config: req.required_config
});
}
} catch (error) {
missing.push({
qualified_name: req.qualified_name,
display_name: req.display_name,
required_config: req.required_config
});
}
steps.push({
id: req.qualified_name,
title: `Select Credential Profile for ${req.display_name}`,
description: `Choose or create a credential profile for ${req.display_name}.`,
type: 'credential_profile',
service_name: req.display_name,
qualified_name: req.qualified_name,
});
}
// Add custom server configuration steps (just ask for URL, no credentials needed)
for (const req of customServers) {
steps.push({
id: req.qualified_name,
@ -355,7 +317,7 @@ const InstallDialog: React.FC<InstallDialogProps> = ({
}
setSetupSteps(steps);
setMissingProfiles(missing);
setMissingProfiles([]); // Clear missing profiles since we handle them in setup steps
setIsCheckingRequirements(false);
};
@ -425,7 +387,6 @@ const InstallDialog: React.FC<InstallDialogProps> = ({
const canInstall = () => {
if (!item) return false;
if (!instanceName.trim()) return false;
if (missingProfiles.length > 0) return false;
// Check if all required profile mappings are selected
const regularRequirements = item.mcp_requirements?.filter(req => !req.custom_type) || [];
@ -458,41 +419,6 @@ const InstallDialog: React.FC<InstallDialogProps> = ({
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
<p className="text-sm text-muted-foreground">Checking requirements...</p>
</div>
) : missingProfiles.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 Credential Profiles</AlertTitle>
<AlertDescription className="text-destructive/80">
This agent requires profiles for the following services:
</AlertDescription>
</Alert>
<div className="space-y-3">
{missingProfiles.map((profile) => (
<Card key={profile.qualified_name} className="py-0 border-destructive/20 shadow-none bg-transparent">
<CardContent className="flex items-center justify-between p-4">
<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>
<div>
<span className="font-medium">{profile.display_name}</span>
<div className="flex flex-wrap gap-1 mt-1">
{profile.required_config.map((config) => (
<Badge key={config} variant="outline" className="text-xs">
{config}
</Badge>
))}
</div>
</div>
</div>
<Badge variant="destructive">Missing</Badge>
</CardContent>
</Card>
))}
</div>
</div>
) : setupSteps.length === 0 ? (
<div className="space-y-6">
<div className="space-y-3">
@ -527,9 +453,9 @@ const InstallDialog: React.FC<InstallDialogProps> = ({
</Badge>
)}
</div>
<p className="text-sm text-muted-foreground">
{/* <p className="text-sm text-muted-foreground">
{currentStepData.description}
</p>
</p> */}
</div>
</div>
@ -541,6 +467,9 @@ const InstallDialog: React.FC<InstallDialogProps> = ({
selectedProfileId={profileMappings[currentStepData.qualified_name]}
onProfileSelect={(profileId, profile) => {
handleProfileSelect(currentStepData.qualified_name, profileId);
if (profile && !profileMappings[currentStepData.qualified_name]) {
refreshRequirements();
}
}}
/>
) : (
@ -607,73 +536,56 @@ const InstallDialog: React.FC<InstallDialogProps> = ({
</div>
<DialogFooter className="flex-col gap-3">
{missingProfiles.length > 0 ? (
<div className="flex gap-3 w-full justify-end">
<Button
variant="outline"
onClick={() => onOpenChange(false)}
>
Cancel
<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={() => window.open('/settings/credentials', '_blank')}
onClick={handleInstall}
disabled={isInstalling || !canInstall()}
>
<Settings className="h-4 w-4" />
Set Up Credential Profiles
{isInstalling ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Installing...
</>
) : (
<>
<Download className="h-4 w-4" />
Install Agent
</>
)}
</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" />
Installing...
</>
) : (
<>
<Download className="h-4 w-4" />
Install Agent
</>
)}
</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>
)}
) : 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>
@ -1099,32 +1011,26 @@ export default function MarketplacePage() {
<User className="h-4 w-4 text-green-600 dark:text-green-400" />
</div>
<div>
<h2 className="text-xl font-semibold text-foreground">Agents from Community</h2>
<p className="text-sm text-muted-foreground">Templates created by the community</p>
<h2 className="text-lg font-semibold text-foreground">Agents from Community</h2>
</div>
</div>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{communityItems.map((item) => {
const { avatar, color } = getItemStyling(item);
return (
<div
key={item.id}
className="bg-neutral-100 dark:bg-sidebar border border-border rounded-2xl overflow-hidden hover:bg-muted/50 transition-all duration-200 cursor-pointer group flex flex-col h-full"
onClick={() => handleItemClick(item)}
>
<div className={`h-50 flex items-center justify-center relative`} style={{ backgroundColor: color }}>
<div className="text-4xl">
{avatar}
</div>
<div className="absolute top-3 right-3 flex gap-2">
<div className="flex items-center gap-1 bg-white/20 backdrop-blur-sm px-2 py-1 rounded-full">
<Download className="h-3 w-3 text-white" />
<span className="text-white text-xs font-medium">{item.download_count}</span>
<div className='p-4'>
<div className={`h-12 w-12 flex items-center justify-center rounded-lg`} style={{ backgroundColor: color }}>
<div className="text-2xl">
{avatar}
</div>
</div>
</div>
<div className="p-4 flex flex-col flex-1">
<div className="p-4 -mt-4 flex flex-col flex-1">
<div className="flex items-center gap-2 mb-1">
<h3 className="text-foreground font-medium text-lg line-clamp-1 flex-1">
{item.name}
@ -1153,19 +1059,24 @@ export default function MarketplacePage() {
)}
</div>
)}
<div className="space-y-1 mb-4">
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<User className="h-3 w-3" />
<span>By {item.creator_name}</span>
</div>
{item.marketplace_published_at && (
<div className="mb-4 w-full flex justify-between">
<div className='space-y-1'>
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<Calendar className="h-3 w-3" />
<span>{new Date(item.marketplace_published_at).toLocaleDateString()}</span>
<User className="h-3 w-3" />
<span>By {item.creator_name}</span>
</div>
)}
{item.marketplace_published_at && (
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<Calendar className="h-3 w-3" />
<span>{new Date(item.marketplace_published_at).toLocaleDateString()}</span>
</div>
)}
</div>
<div className="flex items-center gap-1">
<Download className="h-3 w-3 text-muted-foreground" />
<span className="text-muted-foreground text-xs font-medium">{item.download_count}</span>
</div>
</div>
<Button
onClick={(e) => handleInstallClick(item, e)}
disabled={installingItemId === item.id}
@ -1180,7 +1091,7 @@ export default function MarketplacePage() {
) : (
<>
<Download className="h-4 w-4" />
Install Template
Add to Library
</>
)}
</Button>

View File

@ -636,7 +636,7 @@ export const EnhancedAddCredentialDialog: React.FC<EnhancedAddCredentialDialogPr
{step === 'browse' ? 'Cancel' :
createProfileMutation.isPending ? (
<>
<Loader2 className="h-4 w-4 animate-spin mr-2" />
<Loader2 className="h-4 w-4 animate-spin" />
Creating...
</>
) : 'Create Profile'}

View File

@ -1,11 +1,13 @@
'use client';
import { useEffect, useState } from 'react';
import { Loader2, Server, RefreshCw, AlertCircle } from 'lucide-react';
import { Loader2, Server, RefreshCw, AlertCircle, Clock, Wrench } from 'lucide-react';
import { useApiHealth } from '@/hooks/react-query';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { isLocalMode } from '@/lib/config';
export function MaintenancePage() {
@ -31,58 +33,85 @@ export function MaintenancePage() {
}, []);
return (
<div className="flex flex-col items-center justify-center min-h-screen p-4 bg-background">
<div className="w-full max-w-md space-y-8 text-center">
<div className="space-y-4">
<div className="flex justify-center">
<div className="relative">
<Server className="h-16 w-16 text-primary animate-pulse" />
<div className="absolute inset-0 flex items-center justify-center"></div>
<div className="min-h-screen bg-gradient-to-br from-background via-background to-muted/20 flex items-center justify-center p-4">
<div className="w-full max-w-2xl">
<Card className="border-none shadow-none backdrop-blur-sm bg-transparent">
<CardContent className="p-8">
<div className="text-center space-y-6">
<div className="flex justify-center mb-4">
<div className="relative">
<div className="relative p-4 rounded-full border-2 bg-primary/10">
<Wrench className="h-10 w-10" />
</div>
</div>
</div>
<div className="flex justify-center">
<Badge variant="outline" className="px-3 py-1 bg-blue-50 dark:bg-blue-900/30 border-blue-200 dark:border-blue-700 text-blue-800 dark:text-blue-200 font-medium">
<AlertCircle className="h-4 w-4" />
System Under Maintenance
</Badge>
</div>
<div className="space-y-4">
<h1 className="text-3xl font-bold tracking-tight bg-gradient-to-r from-foreground to-foreground/70 bg-clip-text text-transparent">
We'll Be Right Back
</h1>
<p className="text-base text-muted-foreground max-w-lg mx-auto leading-relaxed">
{isLocalMode() ? (
"The backend server appears to be offline. Please ensure your backend server is running and try again."
) : (
"We're performing scheduled maintenance to improve your experience. Our team is working diligently to restore all services."
)}
</p>
</div>
<div className="grid grid-cols-1 gap-3 mt-6 md:px-4">
<Card className="bg-muted-foreground/10 border-none shadow-none">
<CardContent className="text-center">
<div className="flex items-center justify-center mb-1">
<div className="h-3 w-3 bg-red-500 dark:bg-red-400 rounded-full mr-2 animate-pulse"></div>
<span className="font-medium text-red-700 dark:text-red-300">Services Offline</span>
</div>
<p className="text-sm text-red-600 dark:text-red-400">All agent executions are currently paused.</p>
</CardContent>
</Card>
</div>
<div className="space-y-4 pt-4">
<Button
onClick={checkHealth}
disabled={isCheckingHealth}
size="lg"
className="w-full md:w-auto px-6 py-2 bg-primary hover:bg-primary/90 text-primary-foreground font-medium transition-all duration-200"
>
{isCheckingHealth ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Checking Status...
</>
) : (
<>
<RefreshCw className="h-4 w-4" />
Check System Status
</>
)}
</Button>
{lastChecked && (
<div className="flex items-center justify-center text-sm text-muted-foreground">
<Clock className="h-4 w-4 mr-2" />
Last checked: {lastChecked.toLocaleTimeString()}
</div>
)}
</div>
<div className="pt-4 border-t border-border/50">
<p className="text-sm text-muted-foreground">
{isLocalMode() ? (
"Need help? Check the documentation for setup instructions."
) : (
"For urgent matters, please contact our support team."
)}
</p>
</div>
</div>
</div>
<h1 className="text-3xl font-bold tracking-tight">
System Maintenance
</h1>
<p className="text-muted-foreground">
{isLocalMode() ? (
"The backend server appears to be offline. Please check that your backend server is running."
) : (
"We're currently performing maintenance on our systems. Our team is working to get everything back up and running as soon as possible."
)}
</p>
<Alert className="mt-6">
<AlertTitle>Agent Executions Stopped</AlertTitle>
<AlertDescription>
{isLocalMode() ? (
"The backend server needs to be running for agent executions to work. Please start the backend server and try again."
) : (
"Any running agent executions have been stopped during maintenance. You'll need to manually continue these executions once the system is back online."
)}
</AlertDescription>
</Alert>
</div>
<div className="space-y-4">
<Button
onClick={checkHealth}
disabled={isCheckingHealth}
className="w-full"
>
<RefreshCw
className={cn('h-4 w-4', isCheckingHealth && 'animate-spin')}
/>
{isCheckingHealth ? 'Checking...' : 'Check Again'}
</Button>
{lastChecked && (
<p className="text-sm text-muted-foreground">
Last checked: {lastChecked.toLocaleTimeString()}
</p>
)}
</div>
</CardContent>
</Card>
</div>
</div>
);

View File

@ -13,6 +13,7 @@ import {
getUserFriendlyToolName,
safeJsonParse,
} from '@/components/thread/utils';
import { formatMCPToolDisplayName } from '@/components/thread/tool-views/mcp-tool/_utils';
import { KortixLogo } from '@/components/sidebar/kortix-logo';
import { AgentLoader } from './loader';
import { parseXmlToolCalls, isNewXmlFormat, extractToolNameFromStream } from '@/components/thread/tool-views/xml-parser';
@ -54,6 +55,22 @@ const HIDE_STREAMING_XML_TAGS = new Set([
'execute-data-provider-endpoint',
]);
function getEnhancedToolDisplayName(toolName: string, rawXml?: string): string {
if (toolName === 'call-mcp-tool' && rawXml) {
const toolNameMatch = rawXml.match(/tool_name="([^"]+)"/);
if (toolNameMatch) {
const fullToolName = toolNameMatch[1];
const parts = fullToolName.split('_');
if (parts.length >= 3 && fullToolName.startsWith('mcp_')) {
const serverName = parts[1];
const toolNamePart = parts.slice(2).join('_');
return formatMCPToolDisplayName(serverName, toolNamePart);
}
}
}
return getUserFriendlyToolName(toolName);
}
// Helper function to render attachments (keeping original implementation for now)
export function renderAttachments(attachments: string[], fileViewerHandler?: (filePath?: string, filePathList?: string[]) => void, sandboxId?: string, project?: Project) {
if (!attachments || attachments.length === 0) return null;
@ -772,7 +789,10 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
<CircleDashed className="h-3.5 w-3.5 text-primary flex-shrink-0 animate-spin animation-duration-2000" />
</div>
<span className="font-mono text-xs text-primary">
{extractToolNameFromStream(streamingTextContent) || 'Using Tool...'}
{(() => {
const extractedToolName = extractToolNameFromStream(streamingTextContent);
return extractedToolName ? getUserFriendlyToolName(extractedToolName) : 'Using Tool...';
})()}
</span>
</button>
</div>
@ -857,7 +877,13 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
>
<CircleDashed className="h-3.5 w-3.5 text-primary flex-shrink-0 animate-spin animation-duration-2000" />
<span className="font-mono text-xs text-primary">
{detectedTag === 'function_calls' ? (extractToolNameFromStream(streamingText) || 'Using Tool...') : detectedTag}
{detectedTag === 'function_calls' ?
(() => {
const extractedToolName = extractToolNameFromStream(streamingText);
return extractedToolName ? getUserFriendlyToolName(extractedToolName) : 'Using Tool...';
})() :
getUserFriendlyToolName(detectedTag)
}
</span>
</button>
</div>

View File

@ -25,6 +25,7 @@ import {
parseMCPToolCall,
getMCPServerIcon,
getMCPServerColor,
formatMCPToolDisplayName,
MCPResult,
ParsedMCPTool
} from './_utils';
@ -52,7 +53,7 @@ export function McpToolView({
const argumentsCount = result?.mcp_metadata?.arguments_count ?? Object.keys(parsedTool.arguments).length;
const displayName = result?.mcp_metadata ?
`${serverName.charAt(0).toUpperCase() + serverName.slice(1)}: ${toolName.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')}` :
formatMCPToolDisplayName(serverName, toolName) :
parsedTool.displayName;
const ServerIcon = getMCPServerIcon(serverName);

View File

@ -24,6 +24,84 @@ export interface ParsedMCPTool {
arguments: Record<string, any>;
}
/**
* Enhanced MCP tool name formatter that handles various naming conventions
*/
export function formatMCPToolDisplayName(serverName: string, toolName: string): string {
// Handle server name formatting
const formattedServerName = formatServerName(serverName);
// Handle tool name formatting
const formattedToolName = formatToolName(toolName);
return `${formattedServerName}: ${formattedToolName}`;
}
/**
* Format server names with special handling for known services
*/
function formatServerName(serverName: string): string {
const serverMappings: Record<string, string> = {
'exa': 'Exa Search',
'github': 'GitHub',
'notion': 'Notion',
'slack': 'Slack',
'filesystem': 'File System',
'memory': 'Memory',
'anthropic': 'Anthropic',
'openai': 'OpenAI',
'composio': 'Composio',
'langchain': 'LangChain',
'llamaindex': 'LlamaIndex'
};
const lowerName = serverName.toLowerCase();
return serverMappings[lowerName] || capitalizeWords(serverName);
}
function formatToolName(toolName: string): string {
if (toolName.includes('-')) {
return toolName
.split('-')
.map(word => capitalizeWord(word))
.join(' ');
}
if (toolName.includes('_')) {
return toolName
.split('_')
.map(word => capitalizeWord(word))
.join(' ');
}
if (/[a-z][A-Z]/.test(toolName)) {
return toolName
.replace(/([a-z])([A-Z])/g, '$1 $2')
.split(' ')
.map(word => capitalizeWord(word))
.join(' ');
}
return capitalizeWord(toolName);
}
function capitalizeWord(word: string): string {
if (!word) return '';
const upperCaseWords = new Set([
'api', 'url', 'http', 'https', 'json', 'xml', 'csv', 'pdf', 'id', 'uuid',
'oauth', 'jwt', 'sql', 'html', 'css', 'js', 'ts', 'ai', 'ml', 'ui', 'ux'
]);
const lowerWord = word.toLowerCase();
if (upperCaseWords.has(lowerWord)) {
return word.toUpperCase();
}
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
}
function capitalizeWords(text: string): string {
return text
.split(/[\s_-]+/)
.map(word => capitalizeWord(word))
.join(' ');
}
function extractFromNewFormat(toolContent: string | object): MCPResult | null {
try {
let parsed: any;
@ -220,16 +298,14 @@ export function parseMCPToolCall(assistantContent: string): ParsedMCPTool {
const serverName = parts.length > 1 ? parts[1] : 'unknown';
const toolName = parts.length > 2 ? parts.slice(2).join('_') : fullToolName;
const serverDisplayName = serverName.charAt(0).toUpperCase() + serverName.slice(1);
const toolDisplayName = toolName.split('_').map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join(' ');
// Use the enhanced formatting function
const displayName = formatMCPToolDisplayName(serverName, toolName);
return {
serverName,
toolName,
fullToolName,
displayName: `${serverDisplayName}: ${toolDisplayName}`,
displayName,
arguments: args
};
} catch (e) {

View File

@ -114,6 +114,32 @@ export function extractToolNameFromStream(content: string): string | null {
}
export function formatToolNameForDisplay(toolName: string): string {
if (toolName.startsWith('mcp_')) {
const parts = toolName.split('_');
if (parts.length >= 3) {
const serverName = parts[1];
const toolNamePart = parts.slice(2).join('_');
const formattedServerName = serverName.charAt(0).toUpperCase() + serverName.slice(1);
let formattedToolName = toolNamePart;
if (toolNamePart.includes('-')) {
formattedToolName = toolNamePart
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
} else if (toolNamePart.includes('_')) {
formattedToolName = toolNamePart
.split('_')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
} else {
formattedToolName = toolNamePart.charAt(0).toUpperCase() + toolNamePart.slice(1);
}
return `${formattedServerName}: ${formattedToolName}`;
}
}
return toolName
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))

View File

@ -399,37 +399,68 @@ const MCP_SERVER_NAMES = new Map([
['memory', 'Memory'],
]);
const MCP_TOOL_MAPPINGS = new Map([
['web_search_exa', 'Web Search'],
['research_paper_search', 'Research Papers'],
['search', 'Search'],
['find_content', 'Find Content'],
['get_content', 'Get Content'],
['read_file', 'Read File'],
['write_file', 'Write File'],
['list_files', 'List Files'],
]);
function formatMCPToolName(serverName: string, toolName: string): string {
const serverMappings: Record<string, string> = {
'exa': 'Exa Search',
'github': 'GitHub',
'notion': 'Notion',
'slack': 'Slack',
'filesystem': 'File System',
'memory': 'Memory',
'anthropic': 'Anthropic',
'openai': 'OpenAI',
'composio': 'Composio',
'langchain': 'LangChain',
'llamaindex': 'LlamaIndex'
};
const formattedServerName = serverMappings[serverName.toLowerCase()] ||
serverName.charAt(0).toUpperCase() + serverName.slice(1);
let formattedToolName = toolName;
if (toolName.includes('-')) {
formattedToolName = toolName
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
else if (toolName.includes('_')) {
formattedToolName = toolName
.split('_')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
else if (/[a-z][A-Z]/.test(toolName)) {
formattedToolName = toolName
.replace(/([a-z])([A-Z])/g, '$1 $2')
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
else {
formattedToolName = toolName.charAt(0).toUpperCase() + toolName.slice(1);
}
return `${formattedServerName}: ${formattedToolName}`;
}
export function getUserFriendlyToolName(toolName: string): string {
if (toolName?.startsWith('mcp_')) {
if (toolName.startsWith('mcp_')) {
const parts = toolName.split('_');
if (parts.length >= 3) {
const serverName = parts[1];
const toolNamePart = parts.slice(2).join('_');
// Get friendly server name
const friendlyServerName = MCP_SERVER_NAMES.get(serverName) ||
serverName.charAt(0).toUpperCase() + serverName.slice(1);
// Get friendly tool name
const friendlyToolName = MCP_TOOL_MAPPINGS.get(toolNamePart) ||
toolNamePart.split('_').map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join(' ');
return `${friendlyServerName}: ${friendlyToolName}`;
return formatMCPToolName(serverName, toolNamePart);
}
}
if (toolName.includes('-') && !TOOL_DISPLAY_NAMES.has(toolName)) {
const parts = toolName.split('-');
if (parts.length >= 2) {
const serverName = parts[0];
const toolNamePart = parts.slice(1).join('-');
return formatMCPToolName(serverName, toolNamePart);
}
}
return TOOL_DISPLAY_NAMES.get(toolName) || toolName;
}

View File

@ -1,6 +1,6 @@
"use client";
import React from 'react';
import React, { useState } from 'react';
import { Button } from '@/components/ui/button';
import {
Select,
@ -10,21 +10,32 @@ import {
SelectValue
} from '@/components/ui/select';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent } from '@/components/ui/card';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import {
Plus,
Settings,
Star,
Clock,
CheckCircle,
AlertCircle
AlertCircle,
Key,
Shield,
Loader2,
ExternalLink
} from 'lucide-react';
import {
useCredentialProfilesForMcp,
useSetDefaultProfile,
type CredentialProfile
useCreateCredentialProfile,
type CredentialProfile,
type CreateCredentialProfileRequest
} from '@/hooks/react-query/mcp/use-credential-profiles';
import { useMCPServerDetails } from '@/hooks/react-query/mcp/use-mcp-servers';
import { toast } from 'sonner';
interface CredentialProfileSelectorProps {
mcpQualifiedName: string;
@ -34,6 +45,213 @@ interface CredentialProfileSelectorProps {
disabled?: boolean;
}
interface InlineCreateProfileDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
mcpQualifiedName: string;
mcpDisplayName: string;
onSuccess: (profile: CredentialProfile) => void;
}
const InlineCreateProfileDialog: React.FC<InlineCreateProfileDialogProps> = ({
open,
onOpenChange,
mcpQualifiedName,
mcpDisplayName,
onSuccess
}) => {
const [formData, setFormData] = useState<{
profile_name: string;
display_name: string;
config: Record<string, string>;
is_default: boolean;
}>({
profile_name: `${mcpDisplayName} Profile`,
display_name: mcpDisplayName,
config: {},
is_default: false
});
const { data: serverDetails, isLoading: isLoadingDetails } = useMCPServerDetails(mcpQualifiedName);
const createProfileMutation = useCreateCredentialProfile();
const getConfigProperties = () => {
const schema = serverDetails?.connections?.[0]?.configSchema;
return schema?.properties || {};
};
const getRequiredFields = () => {
const schema = serverDetails?.connections?.[0]?.configSchema;
return schema?.required || [];
};
const isFieldRequired = (fieldName: string) => {
return getRequiredFields().includes(fieldName);
};
const handleConfigChange = (key: string, value: string) => {
setFormData(prev => ({
...prev,
config: {
...prev.config,
[key]: value
}
}));
};
const handleSubmit = async () => {
try {
const request: CreateCredentialProfileRequest = {
mcp_qualified_name: mcpQualifiedName,
profile_name: formData.profile_name,
display_name: formData.display_name,
config: formData.config,
is_default: formData.is_default
};
const response = await createProfileMutation.mutateAsync(request);
toast.success('Credential profile created successfully!');
// Create a profile object to return
const newProfile: CredentialProfile = {
profile_id: response.profile_id || 'new-profile',
mcp_qualified_name: mcpQualifiedName,
profile_name: formData.profile_name,
display_name: formData.display_name,
config_keys: Object.keys(formData.config),
is_active: true,
is_default: formData.is_default,
last_used_at: null,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
};
onSuccess(newProfile);
onOpenChange(false);
// Reset form
setFormData({
profile_name: `${mcpDisplayName} Profile`,
display_name: mcpDisplayName,
config: {},
is_default: false
});
} catch (error: any) {
toast.error(error.message || 'Failed to create credential profile');
}
};
const configProperties = getConfigProperties();
const hasConfigFields = Object.keys(configProperties).length > 0;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl max-h-[85vh] overflow-hidden flex flex-col">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Key className="h-5 w-5 text-primary" />
Create Credential Profile
</DialogTitle>
<DialogDescription>
Create a new credential profile for <strong>{mcpDisplayName}</strong>
</DialogDescription>
</DialogHeader>
{isLoadingDetails ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin mr-2" />
<span>Loading server configuration...</span>
</div>
) : (
<div className="space-y-6 flex-1 overflow-y-auto">
<div className="space-y-4">
<div className="grid grid-cols-1 gap-4">
<div className="space-y-2">
<Label htmlFor="profile_name">Profile Name *</Label>
<Input
id="profile_name"
value={formData.profile_name}
onChange={(e) => setFormData(prev => ({ ...prev, profile_name: e.target.value }))}
placeholder="Enter a name for this profile"
/>
<p className="text-xs text-muted-foreground">
This helps you identify different configurations for the same MCP server
</p>
</div>
</div>
{hasConfigFields ? (
<div className="space-y-4">
<h3 className="text-sm font-semibold flex items-center gap-2">
<Settings className="h-4 w-4" />
Connection Settings
</h3>
{Object.entries(configProperties).map(([key, schema]: [string, any]) => (
<div key={key} className="space-y-2">
<Label htmlFor={key}>
{schema.title || key}
{isFieldRequired(key) && (
<span className="text-destructive ml-1">*</span>
)}
</Label>
<Input
id={key}
type={schema.format === 'password' ? 'password' : 'text'}
placeholder={schema.description || `Enter ${key}`}
value={formData.config[key] || ''}
onChange={(e) => handleConfigChange(key, e.target.value)}
/>
{schema.description && (
<p className="text-xs text-muted-foreground">{schema.description}</p>
)}
</div>
))}
</div>
) : (
<Alert>
<Key className="h-4 w-4" />
<AlertDescription>
This MCP server doesn't require any API credentials to use.
</AlertDescription>
</Alert>
)}
<Alert>
<Shield className="h-4 w-4" />
<AlertDescription>
Your credentials will be encrypted and stored securely. You can create multiple profiles for the same MCP server to handle different use cases.
</AlertDescription>
</Alert>
</div>
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button
onClick={handleSubmit}
disabled={!formData.profile_name.trim() || createProfileMutation.isPending}
>
{createProfileMutation.isPending ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Creating...
</>
) : (
<>
<Plus className="h-4 w-4" />
Create Profile
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export function CredentialProfileSelector({
mcpQualifiedName,
mcpDisplayName,
@ -41,10 +259,13 @@ export function CredentialProfileSelector({
onProfileSelect,
disabled = false
}: CredentialProfileSelectorProps) {
const [showCreateDialog, setShowCreateDialog] = useState(false);
const {
data: profiles = [],
isLoading,
error
error,
refetch
} = useCredentialProfilesForMcp(mcpQualifiedName);
const setDefaultMutation = useSetDefaultProfile();
@ -60,7 +281,15 @@ export function CredentialProfileSelector({
};
const handleCreateNewProfile = () => {
window.open('/settings/credentials', '_blank');
setShowCreateDialog(true);
};
const handleCreateSuccess = (newProfile: CredentialProfile) => {
// Refetch profiles to get the updated list
refetch();
// Auto-select the newly created profile
onProfileSelect(newProfile.profile_id, newProfile);
toast.success(`Profile "${newProfile.profile_name}" created and selected!`);
};
if (isLoading) {
@ -85,100 +314,110 @@ export function CredentialProfileSelector({
}
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<Label className="text-sm font-medium">Credential Profile</Label>
<Button
variant="outline"
size="sm"
disabled={disabled}
onClick={handleCreateNewProfile}
>
<Plus className="h-4 w-4" />
New Profile
</Button>
</div>
{profiles.length === 0 ? (
<Card className="border-dashed">
<CardContent className="p-6 text-center">
<Settings className="h-8 w-8 mx-auto mb-2 text-muted-foreground" />
<p className="text-sm text-muted-foreground mb-3">
No credential profiles found for {mcpDisplayName}
</p>
<Button
variant="outline"
size="sm"
onClick={handleCreateNewProfile}
<>
<div className="space-y-4">
<div className="flex items-center justify-between">
<Label className="text-sm font-medium">Credential Profile</Label>
{/* <Button
variant="outline"
size="sm"
disabled={disabled}
onClick={handleCreateNewProfile}
>
<Plus className="h-4 w-4" />
New Profile
</Button> */}
</div>
{profiles.length === 0 ? (
<Card className="border-dashed">
<CardContent className="p-6 text-center">
<Settings className="h-8 w-8 mx-auto mb-2 text-muted-foreground" />
<p className="text-sm text-muted-foreground mb-3">
No credential profiles found for {mcpDisplayName}
</p>
<Button
variant="outline"
size="sm"
onClick={handleCreateNewProfile}
disabled={disabled}
>
<Plus className="h-4 w-4" />
Create Profile
</Button>
</CardContent>
</Card>
) : (
<div className="space-y-3">
<Select
value={selectedProfileId || ''}
onValueChange={(value) => {
const profile = profiles.find(p => p.profile_id === value);
onProfileSelect(value || null, profile || null);
}}
disabled={disabled}
>
<Plus className="h-4 w-4 mr-1" />
Create First Profile
</Button>
</CardContent>
</Card>
) : (
<div className="space-y-3">
<Select
value={selectedProfileId || ''}
onValueChange={(value) => {
const profile = profiles.find(p => p.profile_id === value);
onProfileSelect(value || null, profile || null);
}}
disabled={disabled}
>
<SelectTrigger>
<SelectValue placeholder="Select a credential profile..." />
</SelectTrigger>
<SelectContent>
{profiles.map((profile) => (
<SelectItem key={profile.profile_id} value={profile.profile_id}>
<div className="flex items-center gap-2">
<span>{profile.profile_name}</span>
{profile.is_default && (
<Badge variant="outline" className="text-xs">
Default
</Badge>
)}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
{selectedProfile && (
<Card className="bg-muted/30 py-0">
<CardContent className="p-3">
<div className="flex items-start justify-between">
<div className="space-y-1">
<SelectTrigger>
<SelectValue placeholder="Select a credential profile..." />
</SelectTrigger>
<SelectContent>
{profiles.map((profile) => (
<SelectItem key={profile.profile_id} value={profile.profile_id}>
<div className="flex items-center gap-2">
<h4 className="text-sm font-medium">{selectedProfile.profile_name}</h4>
{selectedProfile.is_default && (
<span>{profile.profile_name}</span>
{profile.is_default && (
<Badge variant="outline" className="text-xs">
Default
</Badge>
)}
</div>
<p className="text-xs text-muted-foreground">
{selectedProfile.display_name}
</p>
</SelectItem>
))}
</SelectContent>
</Select>
{selectedProfile && (
<Card className="bg-muted/30 py-0">
<CardContent className="p-3">
<div className="flex items-start justify-between">
<div className="space-y-1">
<div className="flex items-center gap-2">
<h4 className="text-sm font-medium">{selectedProfile.profile_name}</h4>
{selectedProfile.is_default && (
<Badge variant="outline" className="text-xs">
Default
</Badge>
)}
</div>
<p className="text-xs text-muted-foreground">
{selectedProfile.display_name}
</p>
</div>
{!selectedProfile.is_default && (
<Button
variant="ghost"
size="sm"
onClick={() => handleSetDefault(selectedProfile.profile_id)}
disabled={setDefaultMutation.isPending}
>
<Star className="h-3 w-3" />
</Button>
)}
</div>
{!selectedProfile.is_default && (
<Button
variant="ghost"
size="sm"
onClick={() => handleSetDefault(selectedProfile.profile_id)}
disabled={setDefaultMutation.isPending}
>
<Star className="h-3 w-3" />
</Button>
)}
</div>
</CardContent>
</Card>
)}
</div>
)}
</div>
</CardContent>
</Card>
)}
</div>
)}
</div>
<InlineCreateProfileDialog
open={showCreateDialog}
onOpenChange={setShowCreateDialog}
mcpQualifiedName={mcpQualifiedName}
mcpDisplayName={mcpDisplayName}
onSuccess={handleCreateSuccess}
/>
</>
);
}