From 856c4f35762cdaf85195409fa8c51667a7964650 Mon Sep 17 00:00:00 2001 From: Alex Rudenko Date: Wed, 15 Oct 2025 14:10:11 +0200 Subject: [PATCH 1/2] feat: support verbose a11y snapshot --- docs/tool-reference.md | 6 ++++-- src/McpContext.ts | 3 ++- src/McpResponse.ts | 10 ++++++++-- src/formatters/snapshotFormatter.ts | 17 ++++++++++++----- src/tools/ToolDefinition.ts | 2 +- src/tools/snapshot.ts | 15 +++++++++++---- tests/McpResponse.test.ts | 26 ++++++++++++++++++++++++-- 7 files changed, 62 insertions(+), 17 deletions(-) diff --git a/docs/tool-reference.md b/docs/tool-reference.md index 216a37fa..1836c924 100644 --- a/docs/tool-reference.md +++ b/docs/tool-reference.md @@ -320,9 +320,11 @@ so returned values have to JSON-serializable. ### `take_snapshot` -**Description:** Take a text snapshot of the currently selected page. The snapshot lists page elements along with a unique +**Description:** Take a text snapshot of the currently selected page based on the a11y tree. The snapshot lists page elements along with a unique identifier (uid). Always use the latest snapshot. Prefer taking a snapshot over taking a screenshot. -**Parameters:** None +**Parameters:** + +- **verbose** (boolean) _(optional)_: Whether to include all possible information available in the full a11y tree. Default is false. --- diff --git a/src/McpContext.ts b/src/McpContext.ts index 99b82696..771da7dc 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -304,10 +304,11 @@ export class McpContext implements Context { /** * Creates a text snapshot of a page. */ - async createTextSnapshot(): Promise { + async createTextSnapshot(verbose = false): Promise { const page = this.getSelectedPage(); const rootNode = await page.accessibility.snapshot({ includeIframes: true, + interestingOnly: !verbose, }); if (!rootNode) { return; diff --git a/src/McpResponse.ts b/src/McpResponse.ts index 98a979db..8fa0b0d2 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -32,6 +32,7 @@ interface NetworkRequestData { export class McpResponse implements Response { #includePages = false; #includeSnapshot = false; + #includeVerboseSnapshot = false; #attachedNetworkRequestData?: NetworkRequestData; #includeConsoleData = false; #textResponseLines: string[] = []; @@ -47,8 +48,9 @@ export class McpResponse implements Response { this.#includePages = value; } - setIncludeSnapshot(value: boolean): void { + setIncludeSnapshot(value: boolean, verbose = false): void { this.#includeSnapshot = value; + this.#includeVerboseSnapshot = verbose; } setIncludeNetworkRequests( @@ -125,6 +127,10 @@ export class McpResponse implements Response { return this.#includeSnapshot; } + get includeVersboseSnapshot(): boolean { + return this.#includeVerboseSnapshot; + } + async handle( toolName: string, context: McpContext, @@ -133,7 +139,7 @@ export class McpResponse implements Response { await context.createPagesSnapshot(); } if (this.#includeSnapshot) { - await context.createTextSnapshot(); + await context.createTextSnapshot(this.#includeVerboseSnapshot); } let formattedConsoleMessages: string[]; diff --git a/src/formatters/snapshotFormatter.ts b/src/formatters/snapshotFormatter.ts index 68885edb..00435937 100644 --- a/src/formatters/snapshotFormatter.ts +++ b/src/formatters/snapshotFormatter.ts @@ -22,11 +22,18 @@ export function formatA11ySnapshot( } function getAttributes(serializedAXNodeRoot: TextSnapshotNode): string[] { - const attributes = [ - `uid=${serializedAXNodeRoot.id}`, - serializedAXNodeRoot.role, - `"${serializedAXNodeRoot.name || ''}"`, // Corrected: Added quotes around name - ]; + const attributes = [`uid=${serializedAXNodeRoot.id}`]; + if (serializedAXNodeRoot.role) { + // To match representation in DevTools. + attributes.push( + serializedAXNodeRoot.role === 'none' + ? 'ignored' + : serializedAXNodeRoot.role, + ); + } + if (serializedAXNodeRoot.name) { + attributes.push(`"${serializedAXNodeRoot.name}"`); + } const excluded = new Set(['id', 'role', 'name', 'elementHandle', 'children']); diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index 6b1c92ae..9ecf49a8 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -48,7 +48,7 @@ export interface Response { options?: {pageSize?: number; pageIdx?: number; resourceTypes?: string[]}, ): void; setIncludeConsoleData(value: boolean): void; - setIncludeSnapshot(value: boolean): void; + setIncludeSnapshot(value: boolean, verbose?: boolean): void; attachImage(value: ImageContentData): void; attachNetworkRequest(reqid: number): void; } diff --git a/src/tools/snapshot.ts b/src/tools/snapshot.ts index 427e4f79..75ab1d0e 100644 --- a/src/tools/snapshot.ts +++ b/src/tools/snapshot.ts @@ -12,15 +12,22 @@ import {defineTool, timeoutSchema} from './ToolDefinition.js'; export const takeSnapshot = defineTool({ name: 'take_snapshot', - description: `Take a text snapshot of the currently selected page. The snapshot lists page elements along with a unique + description: `Take a text snapshot of the currently selected page based on the a11y tree. The snapshot lists page elements along with a unique identifier (uid). Always use the latest snapshot. Prefer taking a snapshot over taking a screenshot.`, annotations: { category: ToolCategories.DEBUGGING, readOnlyHint: true, }, - schema: {}, - handler: async (_request, response) => { - response.setIncludeSnapshot(true); + schema: { + verbose: z + .boolean() + .optional() + .describe( + 'Whether to include all possible information available in the full a11y tree. Default is false.', + ), + }, + handler: async (request, response) => { + response.setIncludeSnapshot(true, request.params.verbose ?? false); }, }); diff --git a/tests/McpResponse.test.ts b/tests/McpResponse.test.ts index 30fdf2a2..30af4671 100644 --- a/tests/McpResponse.test.ts +++ b/tests/McpResponse.test.ts @@ -61,9 +61,9 @@ Testing 2`, result[0].text, `# test response ## Page content -uid=1_0 RootWebArea "" +uid=1_0 RootWebArea uid=1_1 button "Click me" focusable focused - uid=1_2 textbox "" value="Input" + uid=1_2 textbox value="Input" `, ); }); @@ -95,6 +95,28 @@ uid=1_0 RootWebArea "My test page" }); }); + it.only('returns verbose snapshot', async () => { + await withBrowser(async (response, context) => { + const page = context.getSelectedPage(); + await page.setContent(html``); + response.setIncludeSnapshot(true, true); + const result = await response.handle('test', context); + assert.equal(result[0].type, 'text'); + assert.strictEqual( + result[0].text, + `# test response +## Page content +uid=1_0 RootWebArea "My test page" + uid=1_1 ignored + uid=1_2 ignored + uid=1_3 complementary + uid=1_4 StaticText "test" + uid=1_5 InlineTextBox "test" +`, + ); + }); + }); + it('adds throttling setting when it is not null', async () => { await withBrowser(async (response, context) => { context.setNetworkConditions('Slow 3G'); From 588892a2d0b8f8d282b2a43db81fe598c08e8f9c Mon Sep 17 00:00:00 2001 From: Alex Rudenko Date: Wed, 15 Oct 2025 14:14:00 +0200 Subject: [PATCH 2/2] Update tests/McpResponse.test.ts Co-authored-by: Nikolay Vitkov <34244704+Lightning00Blade@users.noreply.github.com> --- tests/McpResponse.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/McpResponse.test.ts b/tests/McpResponse.test.ts index 30af4671..62c179d1 100644 --- a/tests/McpResponse.test.ts +++ b/tests/McpResponse.test.ts @@ -95,7 +95,7 @@ uid=1_0 RootWebArea "My test page" }); }); - it.only('returns verbose snapshot', async () => { + it('returns verbose snapshot', async () => { await withBrowser(async (response, context) => { const page = context.getSelectedPage(); await page.setContent(html``);