|
|
|
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
|