From c4111ce42945938c0982d54b0d74b45eb8cd0f7a Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 28 Sep 2026 09:00:17 -0700 Subject: [PATCH 1/2] feat(mcp): allow saving browser_find results to a file References: https://github.com/microsoft/playwright/issues/42077 --- .../playwright-core/src/tools/backend/find.ts | 5 +++-- .../src/tools/cli-daemon/commands.ts | 3 ++- .../src/tools/skills/playwright-cli/SKILL.md | 2 ++ tests/mcp/cli-find.spec.ts | 15 +++++++++++++++ tests/mcp/find.spec.ts | 18 ++++++++++++++++++ 5 files changed, 40 insertions(+), 3 deletions(-) diff --git a/packages/playwright-core/src/tools/backend/find.ts b/packages/playwright-core/src/tools/backend/find.ts index c29b01dfbf9f6..f50b836f152b8 100644 --- a/packages/playwright-core/src/tools/backend/find.ts +++ b/packages/playwright-core/src/tools/backend/find.ts @@ -30,6 +30,7 @@ const find = defineTabTool({ inputSchema: z.object({ text: z.string().optional().describe('Plain text to search for in the page snapshot (case-insensitive substring match). Provide either text or regex, not both.'), regex: z.string().optional().refine(v => !v || isValidRegex(v), { message: 'Invalid regular expression' }).describe('Regular expression to search for in the page snapshot. Matching is case-sensitive by default; wrap the pattern in slashes to add flags, e.g. "/error/i" for case-insensitive. Provide either text or regex, not both.'), + filename: z.string().optional().describe('File name to save the results to. Relative file names are resolved against the workspace root. If not provided, results are returned as text.'), }), type: 'readOnly', }, @@ -69,7 +70,7 @@ const find = defineTabTool({ } if (!matchedLines.length) { - response.addTextResult(`No matches found for ${query}.`); + await response.addResult('Find results', `No matches found for ${query}.`, { prefix: 'find', ext: 'md', suggestedFilename: params.filename }); return; } @@ -106,7 +107,7 @@ const find = defineTabTool({ return out.join('\n'); }); const matchWord = matchedLines.length === 1 ? 'match' : 'matches'; - response.addTextResult(`Found ${matchedLines.length} ${matchWord} for ${query}:\n\n${snippets.join('\n\n----\n\n')}`); + await response.addResult('Find results', `Found ${matchedLines.length} ${matchWord} for ${query}:\n\n${snippets.join('\n\n----\n\n')}`, { prefix: 'find', ext: 'md', suggestedFilename: params.filename }); }, }); diff --git a/packages/playwright-core/src/tools/cli-daemon/commands.ts b/packages/playwright-core/src/tools/cli-daemon/commands.ts index 81cc77c7a7d3d..530c0c180360d 100644 --- a/packages/playwright-core/src/tools/cli-daemon/commands.ts +++ b/packages/playwright-core/src/tools/cli-daemon/commands.ts @@ -398,9 +398,10 @@ const find = declareCommand({ }), options: z.object({ regex: z.string().optional().describe('Regular expression to search for in the page snapshot. Provide either a text argument or --regex, not both.'), + filename: z.string().optional().describe('Save results to a file instead of returning them in the response.'), }), toolName: 'browser_find', - toolParams: ({ text, regex }) => ({ text, regex }), + toolParams: ({ text, regex, filename }) => ({ text, regex, filename }), }); const generateLocator = declareCommand({ diff --git a/packages/playwright-core/src/tools/skills/playwright-cli/SKILL.md b/packages/playwright-core/src/tools/skills/playwright-cli/SKILL.md index 64a308a818ac2..0a75c909eb7c8 100644 --- a/packages/playwright-core/src/tools/skills/playwright-cli/SKILL.md +++ b/packages/playwright-core/src/tools/skills/playwright-cli/SKILL.md @@ -52,6 +52,8 @@ playwright-cli find "Sign in" playwright-cli find --regex "Sign (in|up)" # wrap the regexp in slashes to add flags, e.g. /i for case-insensitive playwright-cli find --regex "/sign (in|up)/i" +# save results to a file when a query produces too many matches +playwright-cli find "Add" --filename=results.md playwright-cli eval "document.title" playwright-cli eval "el => el.textContent" e5 # get element id, class, or any attribute not visible in the snapshot diff --git a/tests/mcp/cli-find.spec.ts b/tests/mcp/cli-find.spec.ts index dafacf47aefb9..5c6a93c0ac4a7 100644 --- a/tests/mcp/cli-find.spec.ts +++ b/tests/mcp/cli-find.spec.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import fs from 'fs'; + import { test, expect } from './cli-fixtures'; const listPage = ` @@ -58,3 +60,16 @@ test('find reports no matches', async ({ cli, server }) => { const { output } = await cli('find', 'Pineapples'); expect(output).toContain('No matches found for "Pineapples".'); }); + +test('find --filename', async ({ cli, server }, testInfo) => { + server.setContent('/', listPage, 'text/html'); + await cli('open', server.PREFIX); + + const { output } = await cli('find', 'Bananas', '--filename=find.md'); + expect(output).toContain('[Find results](./find.md)'); + expect(output).not.toContain('Apples'); + + const content = fs.readFileSync(testInfo.outputPath('find.md'), 'utf-8'); + expect(content).toContain('Found 1 match for "Bananas":'); + expect(content).toContain('Apples'); +}); diff --git a/tests/mcp/find.spec.ts b/tests/mcp/find.spec.ts index 1b4ee9f93bc50..3252fc8658e43 100644 --- a/tests/mcp/find.spec.ts +++ b/tests/mcp/find.spec.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import fs from 'fs'; + import { test, expect } from './fixtures'; const listPage = ` @@ -229,3 +231,19 @@ test('browser_find rejects invalid regex', async ({ client, server }) => { isError: true, }); }); + +test('browser_find saves results to file', async ({ client, server }, testInfo) => { + server.setContent('/', listPage, 'text/html'); + await client.callTool({ name: 'browser_navigate', arguments: { url: server.PREFIX } }); + + const response = await client.callTool({ + name: 'browser_find', + arguments: { text: 'Bananas', filename: 'find.md' }, + }); + expect(response).toHaveTextResponse(expect.stringContaining('find.md')); + expect(response).toHaveTextResponse(expect.not.stringContaining('Apples')); + + const content = await fs.promises.readFile(testInfo.outputPath('find.md'), 'utf8'); + expect(content).toContain('Found 1 match for "Bananas":'); + expect(content).toContain('Apples'); +}); From a892b9b221f520653e03ee6072c55164ccb914dc Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 28 Sep 2026 09:02:41 -0700 Subject: [PATCH 2/2] chore: shorten browser_find filename description --- packages/playwright-core/src/tools/backend/find.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playwright-core/src/tools/backend/find.ts b/packages/playwright-core/src/tools/backend/find.ts index f50b836f152b8..3251df89b7929 100644 --- a/packages/playwright-core/src/tools/backend/find.ts +++ b/packages/playwright-core/src/tools/backend/find.ts @@ -30,7 +30,7 @@ const find = defineTabTool({ inputSchema: z.object({ text: z.string().optional().describe('Plain text to search for in the page snapshot (case-insensitive substring match). Provide either text or regex, not both.'), regex: z.string().optional().refine(v => !v || isValidRegex(v), { message: 'Invalid regular expression' }).describe('Regular expression to search for in the page snapshot. Matching is case-sensitive by default; wrap the pattern in slashes to add flags, e.g. "/error/i" for case-insensitive. Provide either text or regex, not both.'), - filename: z.string().optional().describe('File name to save the results to. Relative file names are resolved against the workspace root. If not provided, results are returned as text.'), + filename: z.string().optional().describe('Save results to a file instead of returning them in the response. Relative file names are resolved against the workspace root.'), }), type: 'readOnly', },