From 93f4753d1ce9c615fe6d4182c9fb725894748185 Mon Sep 17 00:00:00 2001 From: jackwener Date: Sun, 15 Mar 2026 07:14:07 +0800 Subject: [PATCH] fix: critical MCP result parsing + V2EX strategy + extension tab cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - browser.ts: strip '### Ran Playwright code' suffix from MCP evaluate results to correctly parse JSON data (was causing all evaluate-based commands to fail) - browser.ts: auto-close chrome-extension:// tabs on session close - pipeline.ts: add JSON string auto-parse fallback for evaluate step - v2ex: add strategy:public + browser:false (public API, no browser needed) - v2ex: simplify to use fetch step instead of evaluate Tested: bilibili hot/search ✅, zhihu hot ✅, v2ex hot ✅, hackernews top ✅, github search ✅ --- src/browser.ts | 34 +++++++++++++++++++++++++++++++++- src/clis/v2ex/hot.yaml | 14 +++++--------- src/clis/v2ex/latest.yaml | 13 +++++-------- src/pipeline.ts | 10 +++++++++- 4 files changed, 52 insertions(+), 19 deletions(-) diff --git a/src/browser.ts b/src/browser.ts index c6f02af..9c0155a 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -36,7 +36,18 @@ export class Page { if (result?.content) { const textParts = result.content.filter((c: any) => c.type === 'text'); if (textParts.length === 1) { - const text = textParts[0].text; + let text = textParts[0].text; + // MCP browser_evaluate returns: "[JSON]\n### Ran Playwright code\n```js\n...\n```" + // Strip the "### Ran Playwright code" suffix to get clean JSON + const codeMarker = text.indexOf('### Ran Playwright code'); + if (codeMarker !== -1) { + text = text.slice(0, codeMarker).trim(); + } + // Also handle "### Result\n[JSON]" format (some MCP versions) + const resultMarker = text.indexOf('### Result\n'); + if (resultMarker !== -1) { + text = text.slice(resultMarker + '### Result\n'.length).trim(); + } try { return JSON.parse(text); } catch { return text; } } } @@ -115,6 +126,8 @@ export class PlaywrightMCP { private _lockAcquired = false; private _initialTabCount = 0; + private _page: Page | null = null; + async connect(opts: { timeout?: number } = {}): Promise { await this._acquireLock(); const timeout = opts.timeout ?? CONNECT_TIMEOUT; @@ -137,6 +150,7 @@ export class PlaywrightMCP { (msg) => { if (this._proc?.stdin?.writable) this._proc.stdin.write(msg); }, () => new Promise((res) => { this._waiters.push(res); }), ); + this._page = page; this._proc.stdout?.on('data', (chunk: Buffer) => { this._buffer += chunk.toString(); @@ -185,11 +199,29 @@ export class PlaywrightMCP { async close(): Promise { try { + // Close extension tab(s) opened during this session + if (this._page && this._proc && !this._proc.killed) { + try { + const tabs = await this._page.tabs(); + const tabStr = typeof tabs === 'string' ? tabs : JSON.stringify(tabs); + // Find and close chrome-extension:// tabs (opened by MCP Bridge) + const tabMatches = tabStr.match(/Tab (\d+).*?chrome-extension:\/\//g); + if (tabMatches) { + for (const match of tabMatches) { + const idx = match.match(/Tab (\d+)/)?.[1]; + if (idx) { + try { await this._page.closeTab(parseInt(idx)); } catch {} + } + } + } + } catch {} + } if (this._proc && !this._proc.killed) { this._proc.kill('SIGTERM'); await new Promise((res) => { this._proc?.on('exit', () => res()); setTimeout(res, 3000); }); } } finally { + this._page = null; this._releaseLock(); } } diff --git a/src/clis/v2ex/hot.yaml b/src/clis/v2ex/hot.yaml index 4095bd6..f99e29d 100644 --- a/src/clis/v2ex/hot.yaml +++ b/src/clis/v2ex/hot.yaml @@ -2,6 +2,8 @@ site: v2ex name: hot description: V2EX 热门话题 domain: www.v2ex.com +strategy: public +browser: false args: limit: @@ -10,20 +12,14 @@ args: description: Number of topics pipeline: - - evaluate: | - (async () => { - const res = await fetch('https://www.v2ex.com/api/topics/hot.json'); - return await res.json(); - })() + - fetch: + url: https://www.v2ex.com/api/topics/hot.json - map: rank: ${{ index + 1 }} title: ${{ item.title }} - node: ${{ item.node?.title }} - author: ${{ item.member?.username }} replies: ${{ item.replies }} - url: ${{ item.url }} - limit: ${{ args.limit }} -columns: [rank, title, node, author, replies] +columns: [rank, title, replies] diff --git a/src/clis/v2ex/latest.yaml b/src/clis/v2ex/latest.yaml index e35c16a..61ed7fc 100644 --- a/src/clis/v2ex/latest.yaml +++ b/src/clis/v2ex/latest.yaml @@ -2,6 +2,8 @@ site: v2ex name: latest description: V2EX 最新话题 domain: www.v2ex.com +strategy: public +browser: false args: limit: @@ -10,19 +12,14 @@ args: description: Number of topics pipeline: - - evaluate: | - (async () => { - const res = await fetch('https://www.v2ex.com/api/topics/latest.json'); - return await res.json(); - })() + - fetch: + url: https://www.v2ex.com/api/topics/latest.json - map: rank: ${{ index + 1 }} title: ${{ item.title }} - node: ${{ item.node?.title }} - author: ${{ item.member?.username }} replies: ${{ item.replies }} - limit: ${{ args.limit }} -columns: [rank, title, node, author, replies] +columns: [rank, title, replies] diff --git a/src/pipeline.ts b/src/pipeline.ts index 810570d..0e7c1b2 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -143,7 +143,15 @@ async function executeStep(page: any, op: string, params: any, data: any, args: } case 'evaluate': { const js = String(render(params, { args, data })); - return page.evaluate(normalizeEvaluateSource(js)); + let result = await page.evaluate(normalizeEvaluateSource(js)); + // MCP may return JSON as a string — auto-parse it + if (typeof result === 'string') { + const trimmed = result.trim(); + if ((trimmed.startsWith('[') && trimmed.endsWith(']')) || (trimmed.startsWith('{') && trimmed.endsWith('}'))) { + try { result = JSON.parse(trimmed); } catch {} + } + } + return result; } case 'snapshot': { const opts = (typeof params === 'object' && params) ? params : {};