[eric] split: extract ToolCallBubble parsers + colorize + chrome

This commit is contained in:
ciregenz
2026-05-23 05:20:45 -07:00
parent 08063ed85d
commit 0731e2b293
5 changed files with 695 additions and 0 deletions
@@ -0,0 +1,65 @@
import { parseMcpToolName } from './mcpToolName';
export function isBrowserAgentTool(name: string): boolean {
if (name === 'CreateBrowserAgent' || name === 'BrowserAgent' || name === 'BrowserAgents') return true;
const mcp = parseMcpToolName(name);
return mcp.isMcp && mcp.serverSlug === 'openswarm-browser-agent';
}
export function isInvokeAgentTool(name: string): boolean {
if (name === 'InvokeAgent') return true;
const mcp = parseMcpToolName(name);
return mcp.isMcp && mcp.serverSlug === 'openswarm-invoke-agent';
}
export function isCreateAgentTool(name: string): boolean {
return name === 'Agent';
}
export function parseInvokedSessionId(rawText: string): string | null {
const match = rawText.match(/\(forked session:\s*([a-f0-9]+)\)/);
return match ? match[1] : null;
}
export interface InvokeAgentParsed {
agentName: string;
sessionId: string | null;
cost: string | null;
response: string;
}
export function parseCreateAgentResult(rawText: string): string {
if (!rawText) return '';
try {
const parsed = JSON.parse(rawText);
if (typeof parsed === 'string') return parsed;
if (typeof parsed === 'object' && parsed !== null) {
if (parsed.text) return parsed.text;
if (parsed.content) return typeof parsed.content === 'string' ? parsed.content : JSON.stringify(parsed.content);
if (parsed.result) return typeof parsed.result === 'string' ? parsed.result : JSON.stringify(parsed.result);
}
} catch {}
return rawText;
}
export function parseInvokeAgentResult(rawText: string): InvokeAgentParsed | null {
const headerMatch = rawText.match(
/\*\*Invoked Agent Result\*\*(?:\s*;\s*(.+?))?\s*\(forked session:\s*([a-f0-9]+)\)/,
);
if (!headerMatch) return null;
const agentName = headerMatch[1]?.trim() || 'Agent';
const sessionId = headerMatch[2];
const costMatch = rawText.match(/\*Cost:\s*\$([0-9.]+)\*/);
const cost = costMatch ? costMatch[1] : null;
const bodyStart = rawText.indexOf('\n\n');
let response = bodyStart >= 0 ? rawText.slice(bodyStart + 2).trim() : '';
if (response.startsWith('*Cost:')) {
const afterCost = response.indexOf('\n');
response = afterCost >= 0 ? response.slice(afterCost + 1).trim() : '';
}
return { agentName, sessionId, cost, response };
}
@@ -0,0 +1,66 @@
export interface McpToolInfo {
isMcp: boolean;
serverSlug: string;
action: string;
service: string;
displayName: string;
}
export function parseMcpToolName(rawName: string): McpToolInfo {
const m = rawName.match(/^mcp__([^_]+(?:-[^_]+)*)__(.+)$/);
if (!m) return { isMcp: false, serverSlug: '', action: '', service: '', displayName: rawName };
const serverSlug = m[1];
const action = m[2];
const spaced = action.replace(/_/g, ' ').toLowerCase();
const display = spaced.charAt(0).toUpperCase() + spaced.slice(1);
const lower = action.toLowerCase();
let service = '';
if (lower.includes('gmail') || lower.includes('email') || lower.includes('mail')) service = 'gmail';
else if (lower.includes('calendar') || lower.includes('event') || lower.includes('freebusy')) service = 'calendar';
else if (lower.includes('drive') || lower.includes('file')) service = 'drive';
else if (lower.includes('sheet') || lower.includes('spreadsheet')) service = 'sheets';
else if (lower.includes('doc') || lower.includes('paragraph')) service = 'docs';
else if (lower.includes('contact')) service = 'contacts';
return { isMcp: true, serverSlug, action, service, displayName: display };
}
export function getMcpInputSummary(input: any): string {
if (!input || typeof input !== 'object') return '';
const keys = Object.keys(input);
if (keys.length === 0) return '';
if (keys.length === 1) {
const v = input[keys[0]];
const s = typeof v === 'string' ? v : JSON.stringify(v);
return s.length > 60 ? s.slice(0, 60) + '…' : s;
}
return keys.slice(0, 3).map((k) => {
const v = input[k];
const s = typeof v === 'string' ? v : JSON.stringify(v);
return `${k}: ${s.length > 30 ? s.slice(0, 30) + '…' : s}`;
}).join(' ');
}
export function getMcpShortAction(mcpInfo: McpToolInfo): string {
const { action, service } = mcpInfo;
let short = action;
if (service && action.toLowerCase().startsWith(service.toLowerCase() + '_')) {
short = action.slice(service.length + 1);
}
const lower = short.replace(/_/g, ' ').toLowerCase();
return lower.charAt(0).toUpperCase() + lower.slice(1);
}
export function getGmailHeader(msg: any, name: string): string {
if (msg.payload?.headers && Array.isArray(msg.payload.headers)) {
const h = msg.payload.headers.find(
(hdr: any) => (hdr.name || '').toLowerCase() === name.toLowerCase()
);
if (h) return h.value || '';
}
if (msg.headers && typeof msg.headers === 'object' && !Array.isArray(msg.headers)) {
return msg.headers[name] || msg.headers[name.toLowerCase()] || '';
}
return '';
}
@@ -0,0 +1,75 @@
import React, { useState, useEffect } from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
let toolCallKeyframesInjected = false;
export function ensureToolCallKeyframes() {
if (toolCallKeyframesInjected) return;
toolCallKeyframesInjected = true;
const style = document.createElement('style');
style.setAttribute('data-tool-call-keyframes', '');
style.textContent = `
@keyframes tool-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
@keyframes border-glow {
0%, 100% { box-shadow: 0 0 0 0 rgba(var(--glow-rgb), 0); }
50% { box-shadow: 0 0 10px 2px rgba(var(--glow-rgb), 0.12); }
}
@keyframes blink-cursor {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
`;
document.head.appendChild(style);
}
export const ElapsedTimer: React.FC<{ startTime: string }> = ({ startTime }) => {
const c = useClaudeTokens();
const [elapsed, setElapsed] = useState(0);
useEffect(() => {
const start = new Date(startTime).getTime();
const tick = () => setElapsed(Math.floor((Date.now() - start) / 1000));
tick();
const interval = setInterval(tick, 1000);
return () => clearInterval(interval);
}, [startTime]);
const mins = Math.floor(elapsed / 60);
const secs = elapsed % 60;
const display = mins > 0 ? `${mins}m ${secs}s` : `${secs}s`;
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<Box
sx={{
width: 6,
height: 6,
borderRadius: '50%',
bgcolor: c.accent.primary,
animation: 'tool-pulse 1.5s ease-in-out infinite',
}}
/>
<Typography
sx={{
fontSize: '0.7rem',
fontFamily: c.font.mono,
color: c.text.tertiary,
minWidth: 28,
textAlign: 'right',
}}
>
{display}
</Typography>
</Box>
);
};
export function formatElapsed(ms: number): string {
if (ms >= 60000) return `${Math.floor(ms / 60000)}m ${Math.round((ms % 60000) / 1000)}s`;
if (ms >= 1000) return `${(ms / 1000).toFixed(1)}s`;
return `${ms}ms`;
}
@@ -0,0 +1,218 @@
import React from 'react';
import { useThemeMode } from '@/shared/styles/ThemeContext';
import { parseMcpToolName } from './mcpToolName';
import { isBashTool } from './toolResultParsing';
export interface TermColors {
TERM_BG: string;
TERM_BORDER: string;
PROMPT_COLOR: string;
CMD_COLOR: string;
OUTPUT_COLOR: string;
PATH_COLOR: string;
ADD_COLOR: string;
DEL_COLOR: string;
STDERR_COLOR: string;
WARN_COLOR: string;
NUM_COLOR: string;
DIM_COLOR: string;
DIFF_HEADER_COLOR: string;
SCROLLBAR_THUMB: string;
}
const darkTermColors: TermColors = {
TERM_BG: '#131520',
TERM_BORDER: '#1e2030',
PROMPT_COLOR: '#7ec699',
CMD_COLOR: '#e8ecf4',
OUTPUT_COLOR: '#a0aab8',
PATH_COLOR: '#82aaff',
ADD_COLOR: '#7ec699',
DEL_COLOR: '#ff8787',
STDERR_COLOR: '#ff8787',
WARN_COLOR: '#ffcb6b',
NUM_COLOR: '#f78c6c',
DIM_COLOR: '#555b6e',
DIFF_HEADER_COLOR: '#c792ea',
SCROLLBAR_THUMB: '#2a2d3e',
};
const lightTermColors: TermColors = {
TERM_BG: '#f4f3ee',
TERM_BORDER: '#e2e0d8',
PROMPT_COLOR: '#2d7a3e',
CMD_COLOR: '#2a2a28',
OUTPUT_COLOR: '#555550',
PATH_COLOR: '#3060a8',
ADD_COLOR: '#2d7a3e',
DEL_COLOR: '#c03030',
STDERR_COLOR: '#c03030',
WARN_COLOR: '#8a6518',
NUM_COLOR: '#c05020',
DIM_COLOR: '#9e9c95',
DIFF_HEADER_COLOR: '#7c4daa',
SCROLLBAR_THUMB: '#ccc9c0',
};
export function useTermColors(): TermColors {
const { mode } = useThemeMode();
return mode === 'dark' ? darkTermColors : lightTermColors;
}
export function colorizeInput(toolName: string, text: string, tc: TermColors): React.ReactNode {
const n = toolName.toLowerCase();
const mcp = parseMcpToolName(toolName);
if (mcp.isMcp) {
const lines = text.split('\n');
return (
<>
{lines.map((line, i) => {
const nl = i < lines.length - 1 ? '\n' : '';
const colonIdx = line.indexOf(':');
if (colonIdx > 0 && colonIdx < 30) {
return (
<span key={i}>
<span style={{ color: tc.DIM_COLOR }}>{line.slice(0, colonIdx + 1)}</span>
<span style={{ color: tc.CMD_COLOR }}>{line.slice(colonIdx + 1)}</span>
{nl}
</span>
);
}
return <span key={i} style={{ color: tc.CMD_COLOR }}>{line}{nl}</span>;
})}
</>
);
}
if (isBashTool(toolName)) return <span style={{ color: tc.CMD_COLOR }}>{text}</span>;
if (n === 'edit' || n === 'strreplace' || n === 'multiedit') {
const lines = text.split('\n');
return (
<>
{lines.map((line, i) => {
const nl = i < lines.length - 1 ? '\n' : '';
if (i === 0 && (line.startsWith('/') || line.includes('.')))
return <span key={i} style={{ color: tc.PATH_COLOR }}>{line}{nl}</span>;
if (line.startsWith('+ '))
return <span key={i} style={{ color: tc.ADD_COLOR }}>{line}{nl}</span>;
if (line.startsWith('- '))
return <span key={i} style={{ color: tc.DEL_COLOR }}>{line}{nl}</span>;
return <span key={i} style={{ color: tc.CMD_COLOR }}>{line}{nl}</span>;
})}
</>
);
}
if (n === 'write') {
const lines = text.split('\n');
return (
<>
{lines.map((line, i) => {
const nl = i < lines.length - 1 ? '\n' : '';
if (i === 0 && (line.startsWith('/') || line.includes('.')))
return <span key={i} style={{ color: tc.PATH_COLOR }}>{line}{nl}</span>;
return <span key={i} style={{ color: tc.CMD_COLOR, opacity: 0.7 }}>{line}{nl}</span>;
})}
</>
);
}
if (n === 'read' || n === 'glob' || n === 'webfetch') {
if (/^\//.test(text) || text.includes('/'))
return <span style={{ color: tc.PATH_COLOR }}>{text}</span>;
}
if (n === 'grep' || n === 'ripgrep') {
const lines = text.split('\n');
return (
<>
{lines.map((line, i) => {
const nl = i < lines.length - 1 ? '\n' : '';
if (line.startsWith('pattern:'))
return (
<span key={i}>
<span style={{ color: tc.DIM_COLOR }}>pattern: </span>
<span style={{ color: tc.WARN_COLOR }}>{line.slice(9)}</span>
{nl}
</span>
);
if (line.startsWith('path:'))
return (
<span key={i}>
<span style={{ color: tc.DIM_COLOR }}>path: </span>
<span style={{ color: tc.PATH_COLOR }}>{line.slice(6)}</span>
{nl}
</span>
);
return <span key={i} style={{ color: tc.CMD_COLOR }}>{line}{nl}</span>;
})}
</>
);
}
return <span style={{ color: tc.CMD_COLOR }}>{text}</span>;
}
export function colorizeOutput(toolName: string, text: string, tc: TermColors): React.ReactNode {
if (!text) return <span style={{ color: tc.DIM_COLOR, fontStyle: 'italic' }}>(empty)</span>;
const lines = text.split('\n');
const n = toolName.toLowerCase();
return (
<>
{lines.map((line, i) => {
const nl = i < lines.length - 1 ? '\n' : '';
const trimmed = line.trimStart();
if (/^\/\S+/.test(trimmed))
return <span key={i} style={{ color: tc.PATH_COLOR }}>{line}{nl}</span>;
if (n === 'grep' || n === 'ripgrep') {
const grepMatch = line.match(/^(\S+?:\d+[:-])/);
if (grepMatch) {
return (
<span key={i}>
<span style={{ color: tc.PATH_COLOR }}>{grepMatch[1]}</span>
<span style={{ color: tc.OUTPUT_COLOR }}>{line.slice(grepMatch[1].length)}</span>
{nl}
</span>
);
}
const fileHeader = line.match(/^(\S+\.\w+)$/);
if (fileHeader)
return <span key={i} style={{ color: tc.PATH_COLOR, fontWeight: 600 }}>{line}{nl}</span>;
}
if (line.startsWith('@@') && line.includes('@@'))
return <span key={i} style={{ color: tc.DIFF_HEADER_COLOR }}>{line}{nl}</span>;
if (line.startsWith('+'))
return <span key={i} style={{ color: tc.ADD_COLOR }}>{line}{nl}</span>;
if (line.startsWith('-'))
return <span key={i} style={{ color: tc.DEL_COLOR }}>{line}{nl}</span>;
if (/\b[Ee]rror\b/.test(line))
return <span key={i} style={{ color: tc.STDERR_COLOR }}>{line}{nl}</span>;
if (/\b[Ww]arning\b/.test(line))
return <span key={i} style={{ color: tc.WARN_COLOR }}>{line}{nl}</span>;
if (n === 'read') {
const lineNumMatch = line.match(/^(\s*\d+\s*[|:])/);
if (lineNumMatch) {
return (
<span key={i}>
<span style={{ color: tc.NUM_COLOR, opacity: 0.6 }}>{lineNumMatch[1]}</span>
<span style={{ color: tc.OUTPUT_COLOR }}>{line.slice(lineNumMatch[1].length)}</span>
{nl}
</span>
);
}
}
return <span key={i} style={{ color: tc.OUTPUT_COLOR }}>{line}{nl}</span>;
})}
</>
);
}
@@ -0,0 +1,271 @@
import { AgentMessage } from '@/shared/state/agentsSlice';
import { prettyPath, prettyUrl, quoteQuery, bashCommandDetail } from './toolLabels';
import { parseMcpToolName, getMcpInputSummary, getGmailHeader } from './mcpToolName';
export function getToolData(call: AgentMessage) {
const content = typeof call.content === 'object' ? call.content : {};
return {
toolName: content.tool || 'Unknown',
input: content.input || {},
isDenied: content.approved === false,
toolId: content.id,
};
}
export function isBashTool(name: string) {
return name === 'Bash' || name === 'bash';
}
export function getInputSummary(toolName: string, input: any): string {
try {
const mcp = parseMcpToolName(toolName);
if (mcp.isMcp) return getMcpInputSummary(input);
const n = toolName.toLowerCase();
if (isBashTool(toolName)) {
return bashCommandDetail(input.command || '');
}
if (n === 'read' || n === 'write' || n === 'edit' || n === 'multiedit' || n === 'strreplace')
return prettyPath(input.file_path || input.path || '');
if (n === 'glob') return input.pattern || input.glob || input.glob_pattern || '';
if (n === 'grep' || n === 'ripgrep') {
const pat = input.pattern || input.regex || '';
const path = input.path || input.directory || '';
const q = quoteQuery(pat);
return path ? `${q} in ${prettyPath(path)}` : q;
}
if (n === 'websearch') return quoteQuery(input.query || input.search_term || '');
if (n === 'webfetch') return prettyUrl(input.url || '');
if (n === 'todoread' || n === 'todowrite') return '';
if (n === 'ls') return prettyPath(input.path || '.');
if (n === 'mcpactivate') return '';
if (n === 'mcpsearch' || n === 'outputsearch') return quoteQuery(input.query || '');
if (n === 'outputactivate') return input.output_id || '';
if (n === 'renderoutput') return input.output_id || '';
return '';
} catch {
return '';
}
}
function formatMcpInputDisplay(input: any): string {
if (!input || typeof input !== 'object') return String(input ?? '');
return Object.entries(input)
.map(([k, v]) => {
const s = typeof v === 'string' ? v : JSON.stringify(v, null, 2);
return `${k}: ${s}`;
})
.join('\n');
}
export function formatInputDisplay(toolName: string, input: any): string {
try {
const mcp = parseMcpToolName(toolName);
if (mcp.isMcp) return formatMcpInputDisplay(input);
const n = toolName.toLowerCase();
if (isBashTool(toolName)) return input.command || '';
if (n === 'read') {
const p = input.file_path || input.path || '';
const parts = [p];
if (input.offset) parts.push(`offset: ${input.offset}`);
if (input.limit) parts.push(`limit: ${input.limit}`);
return parts.join(' ');
}
if (n === 'write') {
const p = input.file_path || input.path || '';
const content = input.content || '';
const preview = content.length > 300 ? content.slice(0, 300) + '\n…' : content;
return `${p}\n\n${preview}`;
}
if (n === 'edit' || n === 'strreplace') {
const p = input.file_path || input.path || '';
const old = input.old_string || input.old_text || '';
const nw = input.new_string || input.new_text || '';
const lines = [p, ''];
if (old) {
const oldPreview = old.length > 200 ? old.slice(0, 200) + '…' : old;
lines.push(`- ${oldPreview.split('\n').join('\n- ')}`);
}
if (nw) {
const nwPreview = nw.length > 200 ? nw.slice(0, 200) + '…' : nw;
lines.push(`+ ${nwPreview.split('\n').join('\n+ ')}`);
}
return lines.join('\n');
}
if (n === 'multiedit') {
const p = input.file_path || input.path || '';
const edits = input.edits || [];
const lines = [p];
for (const e of edits.slice(0, 3)) {
const old = e.old_string || e.old_text || '';
lines.push(` - ${old.split('\n')[0].slice(0, 60)}`);
}
if (edits.length > 3) lines.push(` … +${edits.length - 3} more edits`);
return lines.join('\n');
}
if (n === 'glob') return input.pattern || input.glob || input.glob_pattern || '';
if (n === 'grep' || n === 'ripgrep') {
const pat = input.pattern || input.regex || '';
const path = input.path || input.directory || '';
const parts = [`pattern: ${pat}`];
if (path) parts.push(`path: ${path}`);
if (input.include) parts.push(`include: ${input.include}`);
return parts.join('\n');
}
if (n === 'websearch') return input.query || input.search_term || '';
if (n === 'webfetch') return input.url || '';
} catch {}
if (typeof input === 'string') return input;
return JSON.stringify(input, null, 2);
}
export interface ParsedBashResult {
type: 'bash';
stdout: string;
stderr: string;
exitCode: number | null;
}
export interface ParsedTextResult {
type: 'text';
content: string;
isError?: boolean;
}
export interface ParsedMcpResult {
type: 'mcp';
service: string;
action: string;
data: Record<string, any>;
rawText: string;
}
export type ParsedResult = ParsedBashResult | ParsedTextResult | ParsedMcpResult;
export function parseToolResult(toolName: string, rawText: string): ParsedResult {
if (isBashTool(toolName)) {
try {
const parsed = JSON.parse(rawText);
if (typeof parsed === 'object' && parsed !== null && 'stdout' in parsed) {
const exitMatch = (parsed.stdout || '').match(/[Ee]xit code:\s*(\d+)/);
return {
type: 'bash',
stdout: parsed.stdout || '',
stderr: parsed.stderr || '',
exitCode: exitMatch ? parseInt(exitMatch[1], 10) : null,
};
}
} catch {}
}
const mcp = parseMcpToolName(toolName);
if (mcp.isMcp) {
try {
let parsed = JSON.parse(rawText);
if (Array.isArray(parsed) && parsed.some((b: any) => b?.type === 'text' && typeof b?.text === 'string')) {
const textContent = parsed
.filter((b: any) => b?.type === 'text')
.map((b: any) => b.text)
.join('\n');
try {
parsed = JSON.parse(textContent);
} catch {
return { type: 'mcp', service: mcp.service, action: mcp.action, data: {}, rawText: textContent };
}
}
if (typeof parsed === 'object' && parsed !== null) {
return { type: 'mcp', service: mcp.service, action: mcp.action, data: parsed, rawText };
}
} catch {}
return { type: 'mcp', service: mcp.service, action: mcp.action, data: {}, rawText };
}
try {
const parsed = JSON.parse(rawText);
if (typeof parsed === 'object' && parsed !== null) {
if ('stdout' in parsed) {
return { type: 'text', content: parsed.stdout || '' };
}
if ('content' in parsed && typeof parsed.content === 'string') {
return { type: 'text', content: parsed.content, isError: !!parsed.is_error };
}
if ('result' in parsed && typeof parsed.result === 'string') {
return { type: 'text', content: parsed.result };
}
if ('output' in parsed && typeof parsed.output === 'string') {
return { type: 'text', content: parsed.output };
}
const n = toolName.toLowerCase();
if (n === 'glob' && Array.isArray(parsed)) {
return { type: 'text', content: parsed.join('\n') };
}
}
} catch {}
return { type: 'text', content: rawText };
}
export function getResultSummary(toolName: string, rawText: string): string {
const parsed = parseToolResult(toolName, rawText);
if (parsed.type === 'bash') {
const lines = parsed.stdout.split('\n').filter((l) => l.trim()).length;
if (parsed.exitCode !== null && parsed.exitCode !== 0) return `exit ${parsed.exitCode}`;
if (parsed.stderr && !parsed.stdout) return 'stderr';
return `${lines} line${lines !== 1 ? 's' : ''}`;
}
if (parsed.type === 'mcp') {
const d = parsed.data;
if (parsed.service === 'gmail') {
const subj = d.subject || getGmailHeader(d, 'Subject');
if (subj) return subj;
if (Array.isArray(d.messages)) return `${d.messages.length} email${d.messages.length !== 1 ? 's' : ''}`;
if (d.id || d.messageId) return 'sent';
}
if (parsed.service === 'calendar') {
if (d.summary) return d.summary.slice(0, 40);
if (Array.isArray(d.items)) return `${d.items.length} event${d.items.length !== 1 ? 's' : ''}`;
}
if (parsed.service === 'drive') {
if (d.name) return d.name;
if (Array.isArray(d.files)) return `${d.files.length} file${d.files.length !== 1 ? 's' : ''}`;
}
if (d.error || d.is_error) return 'error';
return '';
}
const text = parsed.content;
const lines = text.split('\n');
const lineCount = lines.length;
const n = toolName.toLowerCase();
try {
if (n === 'glob') {
const fileCount = lines.filter((l) => l.trim()).length;
return `${fileCount} file${fileCount !== 1 ? 's' : ''}`;
}
if (n === 'grep' || n === 'ripgrep') {
const matchCount = lines.filter((l) => l.trim()).length;
return `${matchCount} match${matchCount !== 1 ? 'es' : ''}`;
}
if (n === 'read') return `${lineCount} lines`;
if (n === 'write') return '';
if (n === 'edit' || n === 'multiedit' || n === 'strreplace') return '';
if (n === 'websearch') return 'results';
if (n === 'webfetch') return `${lineCount} lines`;
if (parsed.isError) return 'error';
} catch {}
return `${lineCount} line${lineCount !== 1 ? 's' : ''}`;
}
export function getPromptPrefix(toolName: string): string {
if (isBashTool(toolName)) return '$ ';
const mcp = parseMcpToolName(toolName);
if (mcp.isMcp) return ` ${mcp.displayName} `;
return ` ${toolName} `;
}