/** * Shared formatting functions for MCP traffic events. * * Extracted from inspect-app.tsx so they can be reused by * the unified timeline, action area, and provenance views. */ import type { TrafficEventType } from './unified-types.js'; /** Safely dig into unknown objects */ export function dig(obj: unknown, ...keys: string[]): unknown { let cur = obj; for (const k of keys) { if (cur === null || cur === undefined || typeof cur !== 'object') return undefined; cur = (cur as Record)[k]; } return cur; } export function trunc(s: string, maxLen: number): string { return s.length > maxLen ? s.slice(0, maxLen - 1) + '\u2026' : s; } export function nameList(items: unknown[], key: string, max: number): string { if (items.length === 0) return '(none)'; const names = items.map((it) => dig(it, key) as string).filter(Boolean); const shown = names.slice(0, max); const rest = names.length - shown.length; return shown.join(', ') + (rest > 0 ? ` +${rest} more` : ''); } export function formatTime(ts: Date | string): string { try { const d = typeof ts === 'string' ? new Date(ts) : ts; return d.toLocaleTimeString('en-GB', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }); } catch { return '??:??:??'; } } /** Extract meaningful summary from request params (strips jsonrpc/id boilerplate) */ export function summarizeRequest(method: string, body: unknown): string { const params = dig(body, 'params') as Record | undefined; switch (method) { case 'initialize': { const name = dig(params, 'clientInfo', 'name') ?? '?'; const ver = dig(params, 'clientInfo', 'version') ?? ''; const proto = dig(params, 'protocolVersion') ?? ''; return `client=${name}${ver ? ` v${ver}` : ''} proto=${proto}`; } case 'tools/call': { const toolName = dig(params, 'name') as string ?? '?'; const args = dig(params, 'arguments') as Record | undefined; if (!args || Object.keys(args).length === 0) return `${toolName}()`; const pairs = Object.entries(args).map(([k, v]) => { const vs = typeof v === 'string' ? v : JSON.stringify(v); return `${k}: ${trunc(vs, 40)}`; }); return `${toolName}(${trunc(pairs.join(', '), 80)})`; } case 'resources/read': { const uri = dig(params, 'uri') as string ?? ''; return uri; } case 'prompts/get': { const name = dig(params, 'name') as string ?? ''; return name; } case 'tools/list': case 'resources/list': case 'prompts/list': case 'notifications/initialized': return ''; default: { if (!params || Object.keys(params).length === 0) return ''; const s = JSON.stringify(params); return trunc(s, 80); } } } /** Extract meaningful summary from response result */ export function summarizeResponse(method: string, body: unknown, durationMs?: number): string { const error = dig(body, 'error') as { message?: string; code?: number } | undefined; if (error) { return `ERROR ${error.code ?? ''}: ${error.message ?? 'unknown'}`; } const result = dig(body, 'result') as Record | undefined; if (!result) return ''; let summary: string; switch (method) { case 'initialize': { const name = dig(result, 'serverInfo', 'name') ?? '?'; const ver = dig(result, 'serverInfo', 'version') ?? ''; const caps = dig(result, 'capabilities') as Record | undefined; const capList = caps ? Object.keys(caps).filter((k) => caps[k] && Object.keys(caps[k] as object).length > 0) : []; summary = `server=${name}${ver ? ` v${ver}` : ''}${capList.length ? ` caps=[${capList.join(',')}]` : ''}`; break; } case 'tools/list': { const tools = (result.tools ?? []) as unknown[]; summary = `${tools.length} tools: ${nameList(tools, 'name', 6)}`; break; } case 'resources/list': { const resources = (result.resources ?? []) as unknown[]; summary = `${resources.length} resources: ${nameList(resources, 'name', 6)}`; break; } case 'prompts/list': { const prompts = (result.prompts ?? []) as unknown[]; if (prompts.length === 0) { summary = '0 prompts'; break; } summary = `${prompts.length} prompts: ${nameList(prompts, 'name', 6)}`; break; } case 'tools/call': { const content = (result.content ?? []) as unknown[]; const isError = result.isError; const first = content[0]; const text = (dig(first, 'text') as string) ?? ''; const prefix = isError ? 'ERROR: ' : ''; if (text) { summary = prefix + trunc(text.replace(/\n/g, ' '), 100); break; } summary = prefix + `${content.length} content block(s)`; break; } case 'resources/read': { const contents = (result.contents ?? []) as unknown[]; const first = contents[0]; const text = (dig(first, 'text') as string) ?? ''; if (text) { summary = trunc(text.replace(/\n/g, ' '), 80); break; } summary = `${contents.length} content block(s)`; break; } case 'notifications/initialized': summary = 'ok'; break; default: { if (Object.keys(result).length === 0) { summary = 'ok'; break; } const s = JSON.stringify(result); summary = trunc(s, 80); break; } } if (durationMs !== undefined) { return `[${durationMs}ms] ${summary}`; } return summary; } /** Format full event body for expanded detail view (multi-line, readable) */ export function formatBodyDetail(eventType: string, method: string, body: unknown): string[] { const bodyObj = body as Record | null; if (!bodyObj) return ['(no body)']; const lines: string[] = []; if (eventType.includes('request') || eventType === 'client_notification') { const params = bodyObj['params'] as Record | undefined; if (method === 'tools/call' && params) { lines.push(`Tool: ${params['name'] as string}`); const args = params['arguments'] as Record | undefined; if (args && Object.keys(args).length > 0) { lines.push('Arguments:'); for (const [k, v] of Object.entries(args)) { const vs = typeof v === 'string' ? v : JSON.stringify(v, null, 2); for (const vl of vs.split('\n')) { lines.push(` ${k}: ${vl}`); } } } } else if (method === 'initialize' && params) { const ci = params['clientInfo'] as Record | undefined; lines.push(`Client: ${ci?.['name'] ?? '?'} v${ci?.['version'] ?? '?'}`); lines.push(`Protocol: ${params['protocolVersion'] ?? '?'}`); const caps = params['capabilities'] as Record | undefined; if (caps) lines.push(`Capabilities: ${JSON.stringify(caps)}`); } else if (params && Object.keys(params).length > 0) { for (const l of JSON.stringify(params, null, 2).split('\n')) { lines.push(l); } } else { lines.push('(empty params)'); } } else if (eventType.includes('response')) { const error = bodyObj['error'] as Record | undefined; if (error) { lines.push(`Error ${error['code']}: ${error['message']}`); if (error['data']) { for (const l of JSON.stringify(error['data'], null, 2).split('\n')) { lines.push(` ${l}`); } } } else { const result = bodyObj['result'] as Record | undefined; if (!result) { lines.push('(empty result)'); } else if (method === 'tools/list') { const tools = (result['tools'] ?? []) as Array<{ name: string; description?: string }>; lines.push(`${tools.length} tools:`); for (const t of tools) { lines.push(` ${t.name}${t.description ? ` \u2014 ${trunc(t.description, 60)}` : ''}`); } } else if (method === 'resources/list') { const resources = (result['resources'] ?? []) as Array<{ name: string; uri?: string; description?: string }>; lines.push(`${resources.length} resources:`); for (const r of resources) { lines.push(` ${r.name}${r.uri ? ` (${r.uri})` : ''}${r.description ? ` \u2014 ${trunc(r.description, 50)}` : ''}`); } } else if (method === 'prompts/list') { const prompts = (result['prompts'] ?? []) as Array<{ name: string; description?: string }>; lines.push(`${prompts.length} prompts:`); for (const p of prompts) { lines.push(` ${p.name}${p.description ? ` \u2014 ${trunc(p.description, 60)}` : ''}`); } } else if (method === 'tools/call') { const isErr = result['isError']; const content = (result['content'] ?? []) as Array<{ type?: string; text?: string }>; if (isErr) lines.push('(error response)'); for (const c of content) { if (c.text) { for (const l of c.text.split('\n')) { lines.push(l); } } else { lines.push(`[${c.type ?? 'unknown'} content]`); } } } else if (method === 'initialize') { const si = result['serverInfo'] as Record | undefined; lines.push(`Server: ${si?.['name'] ?? '?'} v${si?.['version'] ?? '?'}`); lines.push(`Protocol: ${result['protocolVersion'] ?? '?'}`); const caps = result['capabilities'] as Record | undefined; if (caps) { lines.push('Capabilities:'); for (const [k, v] of Object.entries(caps)) { if (v && typeof v === 'object' && Object.keys(v).length > 0) { lines.push(` ${k}: ${JSON.stringify(v)}`); } } } const instructions = result['instructions'] as string | undefined; if (instructions) { lines.push(''); lines.push('Instructions:'); for (const l of instructions.split('\n')) { lines.push(` ${l}`); } } } else { for (const l of JSON.stringify(result, null, 2).split('\n')) { lines.push(l); } } } } else { // Lifecycle events for (const l of JSON.stringify(bodyObj, null, 2).split('\n')) { lines.push(l); } } return lines; } export interface FormattedEvent { arrow: string; color: string; label: string; detail: string; detailColor?: string | undefined; } export function formatEventSummary( eventType: TrafficEventType, method: string | undefined, body: unknown, upstreamName?: string, durationMs?: number, ): FormattedEvent { const m = method ?? ''; switch (eventType) { case 'client_request': return { arrow: '\u2192', color: 'green', label: m, detail: summarizeRequest(m, body) }; case 'client_response': { const detail = summarizeResponse(m, body, durationMs); const hasError = detail.startsWith('ERROR'); return { arrow: '\u2190', color: 'blue', label: m, detail, detailColor: hasError ? 'red' : undefined }; } case 'client_notification': return { arrow: '\u25C2', color: 'magenta', label: m, detail: summarizeRequest(m, body) }; case 'upstream_request': return { arrow: ' \u21E2', color: 'yellowBright', label: `${upstreamName ?? '?'}/${m}`, detail: summarizeRequest(m, body) }; case 'upstream_response': { const detail = summarizeResponse(m, body, durationMs); const hasError = detail.startsWith('ERROR'); return { arrow: ' \u21E0', color: 'yellowBright', label: `${upstreamName ?? '?'}/${m}`, detail, detailColor: hasError ? 'red' : undefined }; } case 'session_created': return { arrow: '\u25CF', color: 'cyan', label: 'session', detail: '' }; case 'session_closed': return { arrow: '\u25CB', color: 'red', label: 'session', detail: 'closed' }; default: return { arrow: '?', color: 'white', label: eventType, detail: '' }; } }