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"
{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>
</ScrollArea>
) : (
<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,30 +12,54 @@ interface ConfiguredMcpListProps {
onRemove: (index: number) => void;
}
export const ConfiguredMcpList: React.FC<ConfiguredMcpListProps> = ({
configuredMCPs,
onEdit,
onRemove,
}) => {
if (configuredMCPs.length === 0) return null;
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 (
<div className="space-y-2">
{configuredMCPs.map((mcp, index) => (
<Card key={index} className="p-3">
<Card 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">
<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>
<div className="font-medium text-sm">{mcp.name}</div>
<div className="text-xs text-muted-foreground">
{mcp.enabledTools?.length || 0} tools enabled
<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">
<div className="flex items-center gap-2 flex-shrink-0">
<Button
size="sm"
variant="ghost"
@ -52,6 +77,26 @@ export const ConfiguredMcpList: React.FC<ConfiguredMcpListProps> = ({
</div>
</div>
</Card>
);
};
export const ConfiguredMcpList: React.FC<ConfiguredMcpListProps> = ({
configuredMCPs,
onEdit,
onRemove,
}) => {
if (configuredMCPs.length === 0) return null;
return (
<div className="space-y-2">
{configuredMCPs.map((mcp, index) => (
<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 = () => {
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>
<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}.`,
description: `Choose or create a credential profile 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
});
}
}
// 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,22 +536,6 @@ 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
</Button>
<Button
onClick={() => window.open('/settings/credentials', '_blank')}
>
<Settings className="h-4 w-4" />
Set Up Credential Profiles
</Button>
</div>
) : (
<div className="flex gap-3 w-full justify-end">
{currentStep > 0 && (
<Button variant="outline" onClick={handleBack} className="flex-1">
@ -673,7 +586,6 @@ const InstallDialog: React.FC<InstallDialogProps> = ({
</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">
<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 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>
</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,7 +1059,8 @@ export default function MarketplacePage() {
)}
</div>
)}
<div className="space-y-1 mb-4">
<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">
<User className="h-3 w-3" />
<span>By {item.creator_name}</span>
@ -1165,7 +1072,11 @@ export default function MarketplacePage() {
</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="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">
<Server className="h-16 w-16 text-primary animate-pulse" />
<div className="absolute inset-0 flex items-center justify-center"></div>
<div className="relative p-4 rounded-full border-2 bg-primary/10">
<Wrench className="h-10 w-10" />
</div>
</div>
<h1 className="text-3xl font-bold tracking-tight">
System Maintenance
</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-muted-foreground">
<p className="text-base text-muted-foreground max-w-lg mx-auto leading-relaxed">
{isLocalMode() ? (
"The backend server appears to be offline. Please check that your backend server is running."
"The backend server appears to be offline. Please ensure your backend server is running and try again."
) : (
"We're currently performing maintenance on our systems. Our team is working to get everything back up and running as soon as possible."
"We're performing scheduled maintenance to improve your experience. Our team is working diligently to restore all services."
)}
</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">
<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}
className="w-full"
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"
>
<RefreshCw
className={cn('h-4 w-4', isCheckingHealth && 'animate-spin')}
/>
{isCheckingHealth ? 'Checking...' : 'Check Again'}
{isCheckingHealth ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Checking Status...
</>
) : (
<>
<RefreshCw className="h-4 w-4" />
Check System Status
</>
)}
</Button>
{lastChecked && (
<p className="text-sm text-muted-foreground">
<div className="flex items-center justify-center text-sm text-muted-foreground">
<Clock className="h-4 w-4 mr-2" />
Last checked: {lastChecked.toLocaleTimeString()}
</p>
</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>
</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,10 +314,11 @@ 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
{/* <Button
variant="outline"
size="sm"
disabled={disabled}
@ -96,7 +326,7 @@ export function CredentialProfileSelector({
>
<Plus className="h-4 w-4" />
New Profile
</Button>
</Button> */}
</div>
{profiles.length === 0 ? (
@ -112,8 +342,8 @@ export function CredentialProfileSelector({
onClick={handleCreateNewProfile}
disabled={disabled}
>
<Plus className="h-4 w-4 mr-1" />
Create First Profile
<Plus className="h-4 w-4" />
Create Profile
</Button>
</CardContent>
</Card>
@ -180,5 +410,14 @@ export function CredentialProfileSelector({
</div>
)}
</div>
<InlineCreateProfileDialog
open={showCreateDialog}
onOpenChange={setShowCreateDialog}
mcpQualifiedName={mcpQualifiedName}
mcpDisplayName={mcpDisplayName}
onSuccess={handleCreateSuccess}
/>
</>
);
}