suna/frontend/src/components/thread/tool-views/mcp-tool/_utils.ts

354 lines
9.3 KiB
TypeScript

import { Search, Code, FileText, Network, Database, Server } from 'lucide-react';
export interface MCPResult {
success: boolean;
data: any;
isError?: boolean;
content?: string;
mcp_metadata?: {
server_name: string;
tool_name: string;
full_tool_name: string;
arguments_count: number;
is_mcp_tool: boolean;
};
error_type?: string;
raw_result?: any;
}
export interface ParsedMCPTool {
serverName: string;
toolName: string;
fullToolName: string;
displayName: string;
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;
if (typeof toolContent === 'string') {
parsed = JSON.parse(toolContent);
} else {
parsed = toolContent;
}
if (parsed && typeof parsed === 'object' && 'tool_execution' in parsed) {
const toolExecution = parsed.tool_execution;
if (toolExecution && typeof toolExecution === 'object' && 'result' in toolExecution) {
const result = toolExecution.result;
const success = result.success === true;
let output = result.output;
if (typeof output === 'string') {
try {
output = JSON.parse(output);
} catch (e) {
}
}
const args = toolExecution.arguments || {};
const toolName = args.tool_name || 'unknown_mcp_tool';
const toolArgs = args.arguments || {};
const parts = toolName.split('_');
const serverName = parts.length > 1 ? parts[1] : 'unknown';
const actualToolName = parts.length > 2 ? parts.slice(2).join('_') : toolName;
return {
success,
data: output,
isError: !success,
content: output,
mcp_metadata: {
server_name: serverName,
tool_name: actualToolName,
full_tool_name: toolName,
arguments_count: Object.keys(toolArgs).length,
is_mcp_tool: true
}
};
}
}
return null;
} catch (e) {
return null;
}
}
function extractFromLegacyFormat(toolContent: string): MCPResult | null {
try {
const toolResultMatch = toolContent.match(/ToolResult\(success=(\w+),\s*output='([\s\S]*)'\)/);
if (toolResultMatch) {
const isSuccess = toolResultMatch[1] === 'True';
let output = toolResultMatch[2];
output = output.replace(/\\n/g, '\n').replace(/\\'/g, "'").replace(/\\"/g, '"');
return {
success: isSuccess,
data: output,
isError: !isSuccess,
content: output
};
}
const xmlMatch = toolContent.match(/<tool_result>\s*<call-mcp-tool>\s*([\s\S]*?)\s*<\/call-mcp-tool>\s*<\/tool_result>/);
if (xmlMatch && xmlMatch[1]) {
const innerContent = xmlMatch[1].trim();
try {
const parsed = JSON.parse(innerContent);
if (parsed.mcp_metadata) {
let actualContent = parsed.content;
if (typeof actualContent === 'string') {
try {
actualContent = JSON.parse(actualContent);
} catch (e) {
}
}
return {
success: !parsed.isError,
data: actualContent,
isError: parsed.isError,
content: actualContent,
mcp_metadata: parsed.mcp_metadata,
error_type: parsed.error_type,
raw_result: parsed.raw_result
};
}
return {
success: !parsed.isError,
data: parsed.content || parsed,
isError: parsed.isError,
content: parsed.content
};
} catch (e) {
return {
success: true,
data: innerContent,
content: innerContent
};
}
}
const parsed = JSON.parse(toolContent);
if (parsed.mcp_metadata) {
let actualContent = parsed.content;
if (typeof actualContent === 'string') {
try {
actualContent = JSON.parse(actualContent);
} catch (e) {
}
}
return {
success: !parsed.isError,
data: actualContent,
isError: parsed.isError,
content: actualContent,
mcp_metadata: parsed.mcp_metadata,
error_type: parsed.error_type,
raw_result: parsed.raw_result
};
}
return {
success: !parsed.isError,
data: parsed.content || parsed,
isError: parsed.isError,
content: parsed.content
};
} catch (e) {
return null;
}
}
export function parseMCPResult(toolContent: string | object | undefined): MCPResult {
if (!toolContent) {
return {
success: true,
data: '',
content: ''
};
}
const newFormatResult = extractFromNewFormat(toolContent);
if (newFormatResult) {
return newFormatResult;
}
if (typeof toolContent === 'string') {
const legacyResult = extractFromLegacyFormat(toolContent);
if (legacyResult) {
return legacyResult;
}
}
const content = typeof toolContent === 'string' ? toolContent : JSON.stringify(toolContent);
return {
success: true,
data: content,
content: content
};
}
export function parseMCPToolCall(assistantContent: string): ParsedMCPTool {
try {
const toolNameMatch = assistantContent.match(/tool_name="([^"]+)"/);
const fullToolName = toolNameMatch ? toolNameMatch[1] : 'unknown_mcp_tool';
const contentMatch = assistantContent.match(/<call-mcp-tool[^>]*>([\s\S]*?)<\/call-mcp-tool>/);
let args = {};
if (contentMatch && contentMatch[1]) {
try {
args = JSON.parse(contentMatch[1].trim());
} catch (e) {
args = { raw: contentMatch[1].trim() };
}
}
const parts = fullToolName.split('_');
const serverName = parts.length > 1 ? parts[1] : 'unknown';
const toolName = parts.length > 2 ? parts.slice(2).join('_') : fullToolName;
// Use the enhanced formatting function
const displayName = formatMCPToolDisplayName(serverName, toolName);
return {
serverName,
toolName,
fullToolName,
displayName,
arguments: args
};
} catch (e) {
return {
serverName: 'unknown',
toolName: 'unknown',
fullToolName: 'unknown_mcp_tool',
displayName: 'MCP Tool',
arguments: {}
};
}
}
export function getMCPServerIcon(serverName: string) {
switch (serverName.toLowerCase()) {
case 'exa':
return Search;
case 'github':
return Code;
case 'notion':
return FileText;
case 'slack':
return Network;
case 'filesystem':
return Database;
default:
return Server;
}
}
export function getMCPServerColor(serverName: string) {
switch (serverName.toLowerCase()) {
case 'exa':
return 'from-blue-500/20 to-blue-600/10 border-blue-500/20';
case 'github':
return 'from-purple-500/20 to-purple-600/10 border-purple-500/20';
case 'notion':
return 'from-gray-500/20 to-gray-600/10 border-gray-500/20';
case 'slack':
return 'from-green-500/20 to-green-600/10 border-green-500/20';
case 'filesystem':
return 'from-orange-500/20 to-orange-600/10 border-orange-500/20';
default:
return 'from-indigo-500/20 to-indigo-600/10 border-indigo-500/20';
}
}