From 92252189b1c2b70b71c104a9ce87d6b2fe293a7d Mon Sep 17 00:00:00 2001 From: "Kai (Tam Nhu) Tran" <61256810+kaitranntt@users.noreply.github.com> Date: Sat, 30 May 2026 14:56:49 -0400 Subject: [PATCH] fix(browser): handle DevTools close endpoint response --- lib/mcp/ccs-browser-server.cjs | 9 ++++- ...browser-mcp-session-and-intercepts.test.ts | 39 +++++++++++++++++++ tests/unit/hooks/browser-mcp-test-harness.ts | 13 +++++++ 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/lib/mcp/ccs-browser-server.cjs b/lib/mcp/ccs-browser-server.cjs index bfb0d2f2..3799c74b 100755 --- a/lib/mcp/ccs-browser-server.cjs +++ b/lib/mcp/ccs-browser-server.cjs @@ -1230,11 +1230,16 @@ function getTools() { ]; } -async function fetchJson(url, options = undefined) { +async function fetchOk(url, options = undefined) { const response = await fetch(url, options); if (!response.ok) { throw new Error(`HTTP ${response.status} for ${url}`); } + return response; +} + +async function fetchJson(url, options = undefined) { + const response = await fetchOk(url, options); return await response.json(); } @@ -5193,7 +5198,7 @@ async function handleClosePage(toolArgs) { activeRecordingSession = null; } - await fetchJson(`${getHttpUrl()}/json/close/${encodeURIComponent(page.id)}`); + await fetchOk(`${getHttpUrl()}/json/close/${encodeURIComponent(page.id)}`, { method: 'PUT' }); const interceptSession = interceptSessionsByPageId.get(page.id); if (interceptSession) { closeSocket(interceptSession.ws); diff --git a/tests/unit/hooks/browser-mcp-session-and-intercepts.test.ts b/tests/unit/hooks/browser-mcp-session-and-intercepts.test.ts index 5d324aa7..67ea287e 100644 --- a/tests/unit/hooks/browser-mcp-session-and-intercepts.test.ts +++ b/tests/unit/hooks/browser-mcp-session-and-intercepts.test.ts @@ -433,6 +433,45 @@ describe('ccs-browser MCP server - session and interception', () => { expect(listText).not.toContain('Docs'); }); + it('closes a page when Chrome DevTools requires PUT and returns text', async () => { + const responses = await runMcpRequests( + [ + { id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' }, + { id: 'page-2', title: 'Docs', currentUrl: 'https://example.com/docs' }, + ], + [ + { + jsonrpc: '2.0', + id: 8331, + method: 'tools/call', + params: { name: 'browser_select_page', arguments: { pageIndex: 1 } }, + }, + { + jsonrpc: '2.0', + id: 8332, + method: 'tools/call', + params: { name: 'browser_close_page', arguments: {} }, + }, + { + jsonrpc: '2.0', + id: 8333, + method: 'tools/call', + params: { name: 'browser_get_session_info', arguments: {} }, + }, + ], + { requirePutForClosePage: true, closePageRespondsWithText: true } + ); + + const closeText = getResponseText(responses.find((message) => message.id === 8332)); + expect(closeText).toContain('status: closed'); + expect(closeText).toContain('selectedPageId: page-1'); + + const listText = getResponseText(responses.find((message) => message.id === 8333)); + expect(listText).toContain('0. Home'); + expect(listText).toContain('selected: true'); + expect(listText).not.toContain('Docs'); + }); + it('keeps the selected page when closing a different page', async () => { const responses = await runMcpRequests( [ diff --git a/tests/unit/hooks/browser-mcp-test-harness.ts b/tests/unit/hooks/browser-mcp-test-harness.ts index 52dd8cea..087f1956 100644 --- a/tests/unit/hooks/browser-mcp-test-harness.ts +++ b/tests/unit/hooks/browser-mcp-test-harness.ts @@ -350,6 +350,8 @@ type RunMcpRequestsOptions = { childEnv?: NodeJS.ProcessEnv; responseTimeoutMs?: number; requirePutForNewPage?: boolean; + requirePutForClosePage?: boolean; + closePageRespondsWithText?: boolean; }; function encodeMessage(message: unknown): string { @@ -746,6 +748,12 @@ function createMockBrowser(pagesInput: MockPageState[]) { } if (req.url?.startsWith('/json/close/')) { + if (options.requirePutForClosePage && req.method !== 'PUT') { + res.writeHead(405, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'method not allowed' })); + return; + } + const targetId = decodeURIComponent(req.url.slice('/json/close/'.length)); const entry = Array.from(pageStates.entries()).find(([, page]) => page.id === targetId); if (!entry) { @@ -754,6 +762,11 @@ function createMockBrowser(pagesInput: MockPageState[]) { return; } pageStates.delete(entry[0]); + if (options.closePageRespondsWithText) { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('Target is closing'); + return; + } res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ id: targetId })); return;