| |
| |
|
|
| interface ParseResult { |
| success: boolean | null; |
| toolName: string | null; |
| output: any; |
| error: string | null; |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function parseToolResult(raw: string): ParseResult { |
| const result: ParseResult = { |
| success: null, |
| toolName: null, |
| output: null, |
| error: null, |
| }; |
| |
| try { |
| |
| const successMatch = raw.match(/\bsuccess\s*=\s*(true|false)\b/i); |
| if (successMatch) { |
| result.success = successMatch[1].toLowerCase() === 'true'; |
| } |
| |
| |
| const outIdx = raw.search(/\boutput\s*=/i); |
| if (outIdx === -1) throw new Error('No output= found'); |
| |
| let i = raw.indexOf('=', outIdx) + 1; |
| |
| while (i < raw.length && /\s/.test(raw[i])) i++; |
| if (i >= raw.length) throw new Error('Output value missing'); |
| |
| const startChar = raw[i]; |
| let extracted: string; |
| |
| if (startChar === '"' || startChar === "'") { |
| |
| const quote = startChar; |
| i++; |
| let esc = false; |
| let buf = ''; |
| while (i < raw.length) { |
| const ch = raw[i]; |
| if (esc) { |
| buf += ch; |
| esc = false; |
| } else if (ch === '\\') { |
| esc = true; |
| } else if (ch === quote) { |
| break; |
| } else { |
| buf += ch; |
| } |
| i++; |
| } |
| extracted = buf; |
| } else if (startChar === '{' || startChar === '[') { |
| |
| const open = startChar; |
| const close = open === '{' ? '}' : ']'; |
| let depth = 0; |
| let buf = ''; |
| while (i < raw.length) { |
| const ch = raw[i]; |
| buf += ch; |
| if (ch === '"') { |
| |
| i++; |
| while (i < raw.length) { |
| const c2 = raw[i]; |
| buf += c2; |
| if (c2 === '\\') { |
| i += 2; |
| continue; |
| } |
| if (c2 === '"') break; |
| i++; |
| } |
| } else if (ch === open) { |
| depth++; |
| } else if (ch === close) { |
| depth--; |
| if (depth === 0) { |
| break; |
| } |
| } |
| i++; |
| } |
| extracted = buf; |
| } else { |
| |
| let buf = ''; |
| while (i < raw.length && !/[,)]/.test(raw[i])) { |
| buf += raw[i]; |
| i++; |
| } |
| extracted = buf.trim(); |
| } |
| |
| |
| try { |
| result.output = JSON.parse(extracted); |
| } catch (e) { |
| |
| result.error = `Output JSON.parse error: ${e instanceof Error ? e.message : String(e)}`; |
| result.output = extracted; |
| } |
| |
| |
| if (result.output && typeof result.output === 'object') { |
| const md = (result.output as any).mcp_metadata; |
| if (md) result.toolName = md.full_tool_name || md.tool_name || null; |
| } |
| } catch (err: any) { |
| result.error = err.message || String(err); |
| } |
| |
| return result; |
| } |
| |
|
|
|
|
| type TokenType = |
| | 'LBRACE' | 'RBRACE' |
| | 'LBRACK' | 'RBRACK' |
| | 'COLON' | 'COMMA' |
| | 'STRING' | 'NUMBER' |
| | 'IDENT' | 'EOF'; |
|
|
| interface Token { type: TokenType; value: string; line: number; col: number; } |
|
|
| export function cleanAndParse(messy: string): string { |
| |
| |
| |
| |
| messy = messy.replace(/\\b/g, '').replace(/\x08/g, ''); |
|
|
| |
| messy = messy.replace(/\/\/.*$/gm, '') |
| .replace(/\/\*[\s\S]*?\*\//g, ''); |
|
|
| |
| messy = messy.replace(/\\r\\n|\\n|\\r/g, '\\n') |
| .replace(/\\t/g, '\\t') |
| .replace(/\btruen\b/gi, 'true') |
| .replace(/,\s*([\]\}])/g, '$1'); |
|
|
| |
| |
| |
| const TOKS = [ |
| ['WHITESPACE', /\s+/y], |
| ['LBRACE' , /\{/y], |
| ['RBRACE' , /\}/y], |
| ['LBRACK' , /\[/y], |
| ['RBRACK' , /\]/y], |
| ['COLON' , /:/y], |
| ['COMMA' , /,/y], |
| |
| ['STRING' , /"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/y], |
| ['NUMBER' , /-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/y], |
| ['IDENT' , /[A-Za-z_]\w*/y], |
| ['MISMATCH' , /./y], |
| ] as const; |
|
|
| const tokens: Token[] = []; |
| let line = 1, col = 1, idx = 0; |
| while (idx < messy.length) { |
| let matched = false; |
| for (const [type, rx] of TOKS) { |
| rx.lastIndex = idx; |
| const m = rx.exec(messy); |
| if (!m) continue; |
| matched = true; |
| const text = m[0]; |
| if (type !== 'WHITESPACE' && type !== 'MISMATCH') { |
| tokens.push({ type: type as TokenType, value: text, line, col }); |
| } |
| |
| const nl = text.match(/\n/g); |
| if (nl) { |
| line += nl.length; |
| col = text.length - text.lastIndexOf('\n'); |
| } else { |
| col += text.length; |
| } |
| idx = rx.lastIndex; |
| break; |
| } |
| if (!matched) { |
| |
| idx++; |
| col++; |
| } |
| } |
| tokens.push({ type: 'EOF', value: '', line, col }); |
|
|
| |
| |
| |
| let p = 0; |
| function peek() { return tokens[p]?.type; } |
| function advance() { return tokens[p++]; } |
|
|
| function escapeControlChars(s: string) { |
| return s.replace(/[\u0000-\u001F\x7F]/g, c => |
| '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0') |
| ); |
| } |
|
|
| function parseValue(): any { |
| const tk = tokens[p]; |
| try { |
| switch (tk.type) { |
| case 'LBRACE': return parseObject(); |
| case 'LBRACK': return parseArray(); |
| case 'STRING': { |
| const raw = advance().value; |
| let inner = raw.slice(1, -1); |
| |
| inner = escapeControlChars(inner) |
| .replace(/\\/g, '\\\\') |
| .replace(/"/g, '\\"'); |
| return JSON.parse(`"${inner}"`); |
| } |
| case 'NUMBER': { |
| const v = advance().value; |
| return v.includes('.') || /[eE]/.test(v) |
| ? parseFloat(v) : parseInt(v, 10); |
| } |
| case 'IDENT': { |
| const lower = advance().value.toLowerCase(); |
| if (lower === 'true') return true; |
| if (lower === 'false') return false; |
| if (lower === 'null') return null; |
| return lower; |
| } |
| default: |
| throw new Error(`Unexpected ${tk.type}`); |
| } |
| } catch (e) { |
| console.warn(`Parse error at ${tk.line}:${tk.col}: ${e instanceof Error ? e.message : String(e)}`); |
| |
| while (p < tokens.length && |
| !['COMMA','RBRACE','RBRACK','EOF'].includes(peek()!)) { |
| advance(); |
| } |
| return null; |
| } |
| } |
|
|
| function parseObject(): any { |
| const obj: Record<string, any> = {}; |
| advance(); |
| while (peek() !== 'RBRACE' && peek() !== 'EOF') { |
| if (peek() === 'COMMA') { advance(); continue; } |
| |
| let key: string | null = null; |
| const tk = tokens[p]; |
| if (tk.type === 'STRING' || tk.type === 'IDENT') { |
| key = tk.type === 'STRING' |
| ? JSON.parse(advance().value.replace(/^['"]|['"]$/g, '"')) |
| : advance().value; |
| } else { |
| console.warn(`Expected key at ${tk.line}:${tk.col}`); |
| advance(); continue; |
| } |
| if (peek() === 'COLON') advance(); |
| else { console.warn(`Missing ':' after key at ${tk.line}:${tk.col}`); } |
|
|
| const val = parseValue(); |
| if (key !== null) { |
| obj[key] = val; |
| } |
| if (peek() === 'COMMA') advance(); |
| } |
| if (peek() === 'RBRACE') advance(); |
| return obj; |
| } |
|
|
| function parseArray(): any[] { |
| const arr: any[] = []; |
| advance(); |
| while (peek() !== 'RBRACK' && peek() !== 'EOF') { |
| if (peek() === 'COMMA') { advance(); continue; } |
| arr.push(parseValue()); |
| } |
| if (peek() === 'RBRACK') advance(); |
| return arr; |
| } |
|
|
| const result = parseValue(); |
| return JSON.stringify(result, null, 2); |
| } |
|
|