Files
openswarm/frontend/src/shared/mcpToolMeta.ts
T

228 lines
10 KiB
TypeScript

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 };
}
function formatTime(hour: unknown, minute: unknown): string {
const h = typeof hour === 'number' ? hour : Number(hour);
const m = typeof minute === 'number' ? minute : Number(minute || 0);
if (!Number.isFinite(h)) return '';
const h12 = ((h + 11) % 12) + 1;
const suffix = h < 12 ? 'am' : 'pm';
return Number.isFinite(m) && m > 0 ? `${h12}:${String(m).padStart(2, '0')}${suffix}` : `${h12}${suffix}`;
}
function weekdayLabel(day: number): string {
return ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][day] || '';
}
function compactWorkflowSchedule(input: any): string {
if (!input || typeof input !== 'object') return '';
const enabled = input.schedule_enabled ?? input.enabled;
if (enabled === false) return 'Turn schedule off';
const unit = input.repeat_unit || input.unit;
const every = Math.max(1, Number(input.repeat_every || 1));
const time = formatTime(input.hour, input.minute);
const days = Array.isArray(input.on_days) ? input.on_days.filter((d: any) => Number.isInteger(d) && d >= 0 && d <= 6) : [];
if (unit === 'minute') return `Run every ${Math.max(15, Number(input.repeat_every || 15))} minutes`;
if (unit === 'hour') {
const minute = Number(input.minute || 0);
const suffix = minute > 0 ? ` at :${String(minute).padStart(2, '0')}` : '';
return every === 1 ? `Run every hour${suffix}` : `Run every ${every} hours${suffix}`;
}
if (unit === 'day') return `Run ${every === 1 ? 'daily' : `every ${every} days`}${time ? ` at ${time}` : ''}`;
if (unit === 'month') return `Run ${every === 1 ? 'monthly' : `every ${every} months`}${time ? ` at ${time}` : ''}`;
if (unit === 'week') {
const dayText = days.length === 1
? weekdayLabel(days[0])
: days.length === 5 && [1, 2, 3, 4, 5].every((d) => days.includes(d))
? 'weekdays'
: days.length > 1
? days.map(weekdayLabel).filter(Boolean).join(', ')
: '';
if (!dayText) return time ? `Choose weekly days at ${time}` : 'Choose weekly days';
return `Run ${every === 1 ? `every ${dayText}` : `every ${every} weeks on ${dayText}`}${time ? ` at ${time}` : ''}`;
}
if (input.title) return `Update "${input.title}"`;
return '';
}
function firstText(input: any, keys: string[]): string {
for (const key of keys) {
const value = input?.[key];
if (typeof value === 'string' && value.trim()) return value.trim();
}
return '';
}
function stepNumber(input: any): string {
const raw = input?.step_idx ?? input?.step_index ?? input?.index;
const idx = typeof raw === 'number' ? raw : Number(raw);
return Number.isInteger(idx) && idx >= 0 ? String(idx + 1) : '';
}
function compactWorkflowAction(input: any, action: string): string {
const lower = action.toLowerCase();
const step = stepNumber(input);
const text = firstText(input, ['new_text', 'text', 'prompt', 'description']);
const label = firstText(input, ['new_label', 'label', 'title', 'name']);
const preview = text || label;
const shortPreview = preview.length > 80 ? preview.slice(0, 77) + '...' : preview;
if (lower === 'addworkflowstep') return shortPreview ? `Add step: ${shortPreview}` : 'Add a workflow step';
if (lower === 'editworkflowstep') return step ? `Edit step ${step}` : 'Edit workflow step';
if (lower === 'deleteworkflowstep') return step ? `Delete step ${step}` : 'Delete workflow step';
if (lower === 'runworkflow') return 'Run this workflow now';
if (lower === 'testworkflow') return 'Test this workflow';
if (lower === 'readtesttranscript') return 'Read latest test results';
if (lower === 'deletescheduledworkflow') return 'Delete this workflow';
if (lower === 'pauseallworkflows') return 'Pause all scheduled workflows';
if (lower === 'resumeallworkflows') return 'Resume scheduled workflows';
if (lower === 'listworkflows') return 'Show workflows';
return '';
}
export function getWorkflowToolLabel(action: string): string | null {
const lower = action.toLowerCase();
if (lower === 'scheduleworkflow') return 'Schedule workflow';
if (lower === 'updatescheduledworkflow') return 'Update schedule';
if (lower === 'deletescheduledworkflow') return 'Delete workflow';
if (lower === 'pauseallworkflows') return 'Pause workflows';
if (lower === 'resumeallworkflows') return 'Resume workflows';
if (lower === 'runworkflow') return 'Run workflow';
if (lower === 'editworkflowstep') return 'Edit workflow step';
if (lower === 'addworkflowstep') return 'Add workflow step';
if (lower === 'deleteworkflowstep') return 'Delete workflow step';
if (lower === 'testworkflow') return 'Test workflow';
if (lower === 'readtesttranscript') return 'Read test results';
if (lower === 'listworkflows') return 'List workflows';
if (lower === 'suggestconverttoworkflow') return 'Suggest workflow';
return null;
}
export function getWorkflowToolInputDisplay(input: any, action?: string, serverSlug?: string): string {
if (!input || typeof input !== 'object') return '';
const isWorkflowTool = serverSlug === 'openswarm-schedule' || (action && getWorkflowToolLabel(action));
if (!isWorkflowTool || !action) return '';
const lower = action.toLowerCase();
const lines: string[] = [];
const schedule = compactWorkflowSchedule(input);
const step = stepNumber(input);
const text = firstText(input, ['new_text', 'text', 'prompt', 'description']);
const label = firstText(input, ['new_label', 'label', 'title', 'name']);
if (lower === 'scheduleworkflow' || lower === 'updatescheduledworkflow') {
if (schedule) lines.push(`Schedule: ${schedule}`);
if (label) lines.push(`Name: ${label}`);
return lines.join('\n') || getWorkflowToolLabel(action) || 'Workflow action';
}
if (lower === 'addworkflowstep') {
if (text) lines.push(`Step: ${text}`);
if (label && label !== text) lines.push(`Label: ${label}`);
return lines.join('\n') || compactWorkflowAction(input, action);
}
if (lower === 'editworkflowstep') {
if (step) lines.push(`Step: ${step}`);
if (text) lines.push(`Prompt: ${text}`);
if (label && label !== text) lines.push(`Label: ${label}`);
return lines.join('\n') || compactWorkflowAction(input, action);
}
return compactWorkflowAction(input, action) || getWorkflowToolLabel(action) || 'Workflow action';
}
// Drop the "--- Selected UI Elements: ..." block the select feature appends to a prompt/task, and collapse newlines so a multi-line task reads as one clean summary line.
function p_cleanSummaryText(s: string): string {
return s.split(/\n*---\nSelected UI Elements:/)[0].replace(/\s+/g, ' ').trim();
}
export function getMcpInputSummary(input: any, action?: string, serverSlug?: string): string {
if (!input || typeof input !== 'object') return '';
// Web tools read as SOURCES, not payload dumps: a quoted query for a search, a bare domain for a
// page read. Lives here so every caller (compact rows, labels, approvals) gets the clean form.
const p_act = (action || '').toLowerCase();
if (p_act === 'websearch') {
const q = input.query ?? input.search_term;
if (typeof q === 'string' && q.trim()) return `"${p_cleanSummaryText(q.trim())}"`;
}
if (p_act === 'webfetch' && typeof input.url === 'string' && input.url) {
try {
return new URL(input.url).host.replace(/^www\./, '');
} catch {
return input.url.replace(/^https?:\/\//, '').split(/[/?#]/)[0];
}
}
if (serverSlug === 'openswarm-schedule' || (action && getWorkflowToolLabel(action))) {
const workflowSummary = compactWorkflowSchedule(input);
if (workflowSummary) return workflowSummary;
if (action) {
const actionSummary = compactWorkflowAction(input, action);
if (actionSummary) return actionSummary;
}
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' ? p_cleanSummaryText(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' ? p_cleanSummaryText(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 '';
}