From 333777213ef3a3a80fe9c7438566f609bb1c2c2b Mon Sep 17 00:00:00 2001 From: Soumyadas15 Date: Sat, 28 Jun 2025 16:07:09 +0530 Subject: [PATCH] chore(ui): sync custom agents config with credentials profile --- .../agents/_components/mcp/config-dialog.tsx | 83 ++-- .../_components/mcp/configured-mcp-list.tsx | 111 +++-- .../_components/mcp/custom-mcp-dialog.tsx | 28 +- .../_components/mcp/mcp-configuration-new.tsx | 1 + .../agents/_components/mcp/types.ts | 1 + .../agents/_components/results-info.tsx | 67 ++- .../src/app/(dashboard)/marketplace/page.tsx | 271 ++++------- .../enhanced-add-credential-dialog.tsx | 2 +- .../maintenance/maintenance-page.tsx | 133 +++--- .../thread/content/ThreadContent.tsx | 30 +- .../tool-views/mcp-tool/McpToolView.tsx | 3 +- .../thread/tool-views/mcp-tool/_utils.ts | 86 +++- .../thread/tool-views/xml-parser.ts | 26 ++ frontend/src/components/thread/utils.ts | 79 +++- .../workflows/CredentialProfileSelector.tsx | 423 ++++++++++++++---- 15 files changed, 912 insertions(+), 432 deletions(-) diff --git a/frontend/src/app/(dashboard)/agents/_components/mcp/config-dialog.tsx b/frontend/src/app/(dashboard)/agents/_components/mcp/config-dialog.tsx index 0c865d3c..9740d59b 100644 --- a/frontend/src/app/(dashboard)/agents/_components/mcp/config-dialog.tsx +++ b/frontend/src/app/(dashboard)/agents/_components/mcp/config-dialog.tsx @@ -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 = ({ onSave, onCancel }) => { - const [config, setConfig] = useState>(existingConfig?.config || {}); const [selectedTools, setSelectedTools] = useState>( new Set(existingConfig?.enabledTools || []) ); + const [selectedProfileId, setSelectedProfileId] = useState( + 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 = ({ setSelectedTools(newTools); }; + const handleProfileSelect = (profileId: string | null, profile: CredentialProfile | null) => { + setSelectedProfileId(profileId); + }; + return ( @@ -86,32 +99,32 @@ export const ConfigDialog: React.FC = ({
Connection Settings - {serverDetails?.connections?.[0]?.configSchema?.properties ? ( - -
- {Object.entries(serverDetails.connections[0].configSchema.properties).map(([key, schema]: [string, any]) => ( -
- - setConfig({ ...config, [key]: e.target.value })} - className="bg-background" - /> -
- ))} -
-
+ + {requiresConfig ? ( +
+ + + {!selectedProfileId && ( + + + + Please select or create a credential profile to configure this MCP server. + + + )} +
) : (
-

No configuration required

+ + +

No configuration required for this MCP server

+
+
)}
@@ -164,7 +177,11 @@ export const ConfigDialog: React.FC = ({ ) : (
-

No tools available

+ + +

No tools available for this MCP server

+
+
)} @@ -177,7 +194,7 @@ export const ConfigDialog: React.FC = ({ + + + + + ); +}; + export const ConfiguredMcpList: React.FC = ({ configuredMCPs, onEdit, @@ -21,37 +90,13 @@ export const ConfiguredMcpList: React.FC = ({ return (
{configuredMCPs.map((mcp, index) => ( - -
-
-
- -
-
-
{mcp.name}
-
- {mcp.enabledTools?.length || 0} tools enabled -
-
-
-
- - -
-
-
+ ))}
); diff --git a/frontend/src/app/(dashboard)/agents/_components/mcp/custom-mcp-dialog.tsx b/frontend/src/app/(dashboard)/agents/_components/mcp/custom-mcp-dialog.tsx index 361cd54c..cf9ee4c1 100644 --- a/frontend/src/app/(dashboard)/agents/_components/mcp/custom-mcp-dialog.tsx +++ b/frontend/src/app/(dashboard)/agents/_components/mcp/custom-mcp-dialog.tsx @@ -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 = ({ } }; + 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 = ({ 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 = ({ setSelectedTools(new Set()); setServerName(''); setProcessedConfig(null); + setValidationError(null); setStep('setup'); onOpenChange(false); @@ -165,7 +180,9 @@ export const CustomMCPDialog: React.FC = ({ }; const handleBack = () => { - setStep('setup'); + if (step === 'tools') { + setStep('setup'); + } setValidationError(null); }; @@ -176,6 +193,7 @@ export const CustomMCPDialog: React.FC = ({ setSelectedTools(new Set()); setServerName(''); setProcessedConfig(null); + setValidationError(null); setStep('setup'); }; @@ -325,7 +343,7 @@ export const CustomMCPDialog: React.FC = ({ )} - ) : ( + ) : step === 'tools' ? (
@@ -409,7 +427,7 @@ export const CustomMCPDialog: React.FC = ({ )}
- )} + ) : null} @@ -422,7 +440,7 @@ export const CustomMCPDialog: React.FC = ({ Cancel + )} + - )} + + ); } \ No newline at end of file diff --git a/frontend/src/app/(dashboard)/marketplace/page.tsx b/frontend/src/app/(dashboard)/marketplace/page.tsx index 2baa6ba8..372086eb 100644 --- a/frontend/src/app/(dashboard)/marketplace/page.tsx +++ b/frontend/src/app/(dashboard)/marketplace/page.tsx @@ -271,70 +271,32 @@ const InstallDialog: React.FC = ({ } }, [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 = ({ } 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 = ({ 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 = ({

Checking requirements...

- ) : missingProfiles.length > 0 ? ( -
- - - Missing Credential Profiles - - This agent requires profiles for the following services: - - - -
- {missingProfiles.map((profile) => ( - - -
-
- -
-
- {profile.display_name} -
- {profile.required_config.map((config) => ( - - {config} - - ))} -
-
-
- Missing -
-
- ))} -
-
) : setupSteps.length === 0 ? (
@@ -527,9 +453,9 @@ const InstallDialog: React.FC = ({ )}
-

+ {/*

{currentStepData.description} -

+

*/}
@@ -541,6 +467,9 @@ const InstallDialog: React.FC = ({ 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 = ({ - {missingProfiles.length > 0 ? ( -
- + )} + + {setupSteps.length === 0 ? ( -
- ) : ( -
- {currentStep > 0 && ( - - )} - - {setupSteps.length === 0 ? ( - - ) : currentStep < setupSteps.length ? ( - - ) : ( - - )} -
- )} + ) : currentStep < setupSteps.length ? ( + + ) : ( + + )} +
@@ -1099,32 +1011,26 @@ export default function MarketplacePage() {
-

Agents from Community

-

Templates created by the community

+

Agents from Community

{communityItems.map((item) => { const { avatar, color } = getItemStyling(item); - return (
handleItemClick(item)} > -
-
- {avatar} -
-
-
- - {item.download_count} +
+
+
+ {avatar}
-
+

{item.name} @@ -1153,19 +1059,24 @@ export default function MarketplacePage() { )}

)} -
-
- - By {item.creator_name} -
- {item.marketplace_published_at && ( +
+
- - {new Date(item.marketplace_published_at).toLocaleDateString()} + + By {item.creator_name}
- )} + {item.marketplace_published_at && ( +
+ + {new Date(item.marketplace_published_at).toLocaleDateString()} +
+ )} +
+
+ + {item.download_count} +
- diff --git a/frontend/src/app/(dashboard)/settings/credentials/_components/enhanced-add-credential-dialog.tsx b/frontend/src/app/(dashboard)/settings/credentials/_components/enhanced-add-credential-dialog.tsx index e1b2d680..6cf3dc6f 100644 --- a/frontend/src/app/(dashboard)/settings/credentials/_components/enhanced-add-credential-dialog.tsx +++ b/frontend/src/app/(dashboard)/settings/credentials/_components/enhanced-add-credential-dialog.tsx @@ -636,7 +636,7 @@ export const EnhancedAddCredentialDialog: React.FC - + Creating... ) : 'Create Profile'} diff --git a/frontend/src/components/maintenance/maintenance-page.tsx b/frontend/src/components/maintenance/maintenance-page.tsx index e92c9416..6623b08c 100644 --- a/frontend/src/components/maintenance/maintenance-page.tsx +++ b/frontend/src/components/maintenance/maintenance-page.tsx @@ -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 ( -
-
-
-
-
- -
+
+
+ + +
+
+
+
+ +
+
+
+
+ + + System Under Maintenance + +
+
+

+ We'll Be Right Back +

+

+ {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." + )} +

+
+
+ + +
+
+ Services Offline +
+

All agent executions are currently paused.

+
+
+
+
+ + {lastChecked && ( +
+ + Last checked: {lastChecked.toLocaleTimeString()} +
+ )} +
+
+

+ {isLocalMode() ? ( + "Need help? Check the documentation for setup instructions." + ) : ( + "For urgent matters, please contact our support team." + )} +

+
-
- -

- System Maintenance -

- -

- {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." - )} -

- - - Agent Executions Stopped - - {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." - )} - - -
- -
- - - {lastChecked && ( -

- Last checked: {lastChecked.toLocaleTimeString()} -

- )} -
+ +
); diff --git a/frontend/src/components/thread/content/ThreadContent.tsx b/frontend/src/components/thread/content/ThreadContent.tsx index 55b1f439..f287389f 100644 --- a/frontend/src/components/thread/content/ThreadContent.tsx +++ b/frontend/src/components/thread/content/ThreadContent.tsx @@ -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 = ({
- {extractToolNameFromStream(streamingTextContent) || 'Using Tool...'} + {(() => { + const extractedToolName = extractToolNameFromStream(streamingTextContent); + return extractedToolName ? getUserFriendlyToolName(extractedToolName) : 'Using Tool...'; + })()}
@@ -857,7 +877,13 @@ export const ThreadContent: React.FC = ({ > - {detectedTag === 'function_calls' ? (extractToolNameFromStream(streamingText) || 'Using Tool...') : detectedTag} + {detectedTag === 'function_calls' ? + (() => { + const extractedToolName = extractToolNameFromStream(streamingText); + return extractedToolName ? getUserFriendlyToolName(extractedToolName) : 'Using Tool...'; + })() : + getUserFriendlyToolName(detectedTag) + }
diff --git a/frontend/src/components/thread/tool-views/mcp-tool/McpToolView.tsx b/frontend/src/components/thread/tool-views/mcp-tool/McpToolView.tsx index 336f017d..c0e8d1aa 100644 --- a/frontend/src/components/thread/tool-views/mcp-tool/McpToolView.tsx +++ b/frontend/src/components/thread/tool-views/mcp-tool/McpToolView.tsx @@ -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); diff --git a/frontend/src/components/thread/tool-views/mcp-tool/_utils.ts b/frontend/src/components/thread/tool-views/mcp-tool/_utils.ts index f411e1e3..b8ad19d1 100644 --- a/frontend/src/components/thread/tool-views/mcp-tool/_utils.ts +++ b/frontend/src/components/thread/tool-views/mcp-tool/_utils.ts @@ -24,6 +24,84 @@ export interface ParsedMCPTool { arguments: Record; } +/** + * 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 = { + '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) { diff --git a/frontend/src/components/thread/tool-views/xml-parser.ts b/frontend/src/components/thread/tool-views/xml-parser.ts index ddd0668d..877acef8 100644 --- a/frontend/src/components/thread/tool-views/xml-parser.ts +++ b/frontend/src/components/thread/tool-views/xml-parser.ts @@ -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)) diff --git a/frontend/src/components/thread/utils.ts b/frontend/src/components/thread/utils.ts index ac7fdc72..a5462e28 100644 --- a/frontend/src/components/thread/utils.ts +++ b/frontend/src/components/thread/utils.ts @@ -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 = { + '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; } diff --git a/frontend/src/components/workflows/CredentialProfileSelector.tsx b/frontend/src/components/workflows/CredentialProfileSelector.tsx index 94398d11..26c8f971 100644 --- a/frontend/src/components/workflows/CredentialProfileSelector.tsx +++ b/frontend/src/components/workflows/CredentialProfileSelector.tsx @@ -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 = ({ + open, + onOpenChange, + mcpQualifiedName, + mcpDisplayName, + onSuccess +}) => { + const [formData, setFormData] = useState<{ + profile_name: string; + display_name: string; + config: Record; + 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 ( + + + + + + Create Credential Profile + + + Create a new credential profile for {mcpDisplayName} + + + + {isLoadingDetails ? ( +
+ + Loading server configuration... +
+ ) : ( +
+
+
+
+ + setFormData(prev => ({ ...prev, profile_name: e.target.value }))} + placeholder="Enter a name for this profile" + /> +

+ This helps you identify different configurations for the same MCP server +

+
+
+ + {hasConfigFields ? ( +
+

+ + Connection Settings +

+ {Object.entries(configProperties).map(([key, schema]: [string, any]) => ( +
+ + handleConfigChange(key, e.target.value)} + /> + {schema.description && ( +

{schema.description}

+ )} +
+ ))} +
+ ) : ( + + + + This MCP server doesn't require any API credentials to use. + + + )} + + + + + Your credentials will be encrypted and stored securely. You can create multiple profiles for the same MCP server to handle different use cases. + + +
+
+ )} + + + + + +
+
+ ); +}; + 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 ( -
-
- - -
- - {profiles.length === 0 ? ( - - - -

- No credential profiles found for {mcpDisplayName} -

- */} +
+ + {profiles.length === 0 ? ( + + + +

+ No credential profiles found for {mcpDisplayName} +

+ +
+
+ ) : ( +
+ { - const profile = profiles.find(p => p.profile_id === value); - onProfileSelect(value || null, profile || null); - }} - disabled={disabled} - > - - - - - {profiles.map((profile) => ( - -
- {profile.profile_name} - {profile.is_default && ( - - Default - - )} -
-
- ))} -
- - - {selectedProfile && ( - - -
-
+ + + + + {profiles.map((profile) => ( +
-

{selectedProfile.profile_name}

- {selectedProfile.is_default && ( + {profile.profile_name} + {profile.is_default && ( Default )}
-

- {selectedProfile.display_name} -

+
+ ))} +
+ + + {selectedProfile && ( + + +
+
+
+

{selectedProfile.profile_name}

+ {selectedProfile.is_default && ( + + Default + + )} +
+

+ {selectedProfile.display_name} +

+
+ {!selectedProfile.is_default && ( + + )}
- {!selectedProfile.is_default && ( - - )} -
- - - )} -
- )} -
+ + + )} +
+ )} +
+ + + ); } \ No newline at end of file