Skip to content

Commit d22a5f0

Browse files
author
Tomohiko Hiraki
committed
feat(tools): integrate expectation parameter into main tools
- Add expectation parameter to navigate, click, type, and snapshot tools - Configure tool-specific default expectations for optimal token usage - Update browserServerBackend to pass expectation to Response class - Remove legacy setIncludeSnapshot/setIncludeTabs methods - Ensure complete backward compatibility This completes PR #3 of the token optimization implementation, enabling fine-grained control over response content per tool.
1 parent 08aee4b commit d22a5f0

File tree

8 files changed

+495
-26
lines changed

8 files changed

+495
-26
lines changed

src/browserServerBackend.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export class BrowserServerBackend implements ServerBackend {
7777

7878
async callTool(schema: mcpServer.ToolSchema<any>, parsedArguments: any) {
7979
const context = this._context!;
80-
const response = new Response(context, schema.name, parsedArguments);
80+
const response = new Response(context, schema.name, parsedArguments, parsedArguments.expectation);
8181
const tool = this._tools.find(tool => tool.schema.name === schema.name)!;
8282
context.setRunningTool(true);
8383
try {

src/response.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,9 @@ export class Response {
8787
async finish() {
8888
// All the async snapshotting post-action is happening here.
8989
// Everything below should race against modal states.
90-
if ((this._includeSnapshot || this._expectation.includeSnapshot) && this._context.currentTab()) {
90+
// Expectation settings take priority over legacy setIncludeSnapshot calls
91+
const shouldIncludeSnapshot = this._expectation.includeSnapshot;
92+
if (shouldIncludeSnapshot && this._context.currentTab()) {
9193
const options = this._expectation.snapshotOptions;
9294
if (options?.selector) {
9395
// TODO: Implement partial snapshot capture based on selector
@@ -125,10 +127,10 @@ ${this._code.join('\n')}
125127
}
126128

127129
// List browser tabs based on expectation.
128-
const shouldIncludeTabs = this._expectation.includeTabs || this._includeTabs;
129-
const shouldIncludeSnapshot = this._expectation.includeSnapshot || this._includeSnapshot;
130+
const shouldIncludeTabs = this._expectation.includeTabs;
131+
const shouldIncludeSnapshot = this._expectation.includeSnapshot;
130132

131-
if (shouldIncludeSnapshot || shouldIncludeTabs)
133+
if (shouldIncludeTabs)
132134
response.push(...renderTabsMarkdown(this._context.tabs(), shouldIncludeTabs));
133135

134136
// Add snapshot if provided and expectation allows it.

src/schemas/expectation.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export type ExpectationOptions = z.infer<typeof expectationSchema>;
5252
*/
5353
const TOOL_DEFAULTS: Record<string, Required<Omit<NonNullable<ExpectationOptions>, 'snapshotOptions' | 'consoleOptions' | 'imageOptions'>>> = {
5454
// Navigation tools need full context for verification
55-
navigate: {
55+
browser_navigate: {
5656
includeSnapshot: true,
5757
includeConsole: true,
5858
includeDownloads: true,
@@ -61,15 +61,15 @@ const TOOL_DEFAULTS: Record<string, Required<Omit<NonNullable<ExpectationOptions
6161
},
6262

6363
// Interactive tools need snapshot for feedback but less verbose logging
64-
click: {
64+
browser_click: {
6565
includeSnapshot: true,
6666
includeConsole: false,
6767
includeDownloads: false,
6868
includeTabs: false,
6969
includeCode: true
7070
},
7171

72-
fill: {
72+
browser_type: {
7373
includeSnapshot: true,
7474
includeConsole: false,
7575
includeDownloads: false,
@@ -86,6 +86,15 @@ const TOOL_DEFAULTS: Record<string, Required<Omit<NonNullable<ExpectationOptions
8686
includeCode: false
8787
},
8888

89+
// Snapshot tool should capture snapshot but minimal other context
90+
browser_snapshot: {
91+
includeSnapshot: true,
92+
includeConsole: false,
93+
includeDownloads: false,
94+
includeTabs: false,
95+
includeCode: false
96+
},
97+
8998
// Code evaluation needs console output but minimal other info
9099
evaluate: {
91100
includeSnapshot: false,

src/tools/keyboard.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { defineTabTool } from './tool.js';
2020
import { elementSchema } from './snapshot.js';
2121
import { generateLocator } from './utils.js';
2222
import * as javascript from '../javascript.js';
23+
import { expectationSchema } from '../schemas/expectation.js';
2324

2425
const pressKey = defineTabTool({
2526
capability: 'core',
@@ -30,12 +31,12 @@ const pressKey = defineTabTool({
3031
description: 'Press a key on the keyboard',
3132
inputSchema: z.object({
3233
key: z.string().describe('Name of the key to press or a character to generate, such as `ArrowLeft` or `a`'),
34+
expectation: expectationSchema
3335
}),
3436
type: 'destructive',
3537
},
3638

3739
handle: async (tab, params, response) => {
38-
response.setIncludeSnapshot();
3940
response.addCode(`// Press ${params.key}`);
4041
response.addCode(`await page.keyboard.press('${params.key}');`);
4142

@@ -49,6 +50,7 @@ const typeSchema = elementSchema.extend({
4950
text: z.string().describe('Text to type into the element'),
5051
submit: z.boolean().optional().describe('Whether to submit entered text (press Enter after)'),
5152
slowly: z.boolean().optional().describe('Whether to type one character at a time. Useful for triggering key handlers in the page. By default entire text is filled in at once.'),
53+
expectation: expectationSchema
5254
});
5355

5456
const type = defineTabTool({
@@ -66,7 +68,6 @@ const type = defineTabTool({
6668

6769
await tab.waitForCompletion(async () => {
6870
if (params.slowly) {
69-
response.setIncludeSnapshot();
7071
response.addCode(`await page.${await generateLocator(locator)}.pressSequentially(${javascript.quote(params.text)});`);
7172
await locator.pressSequentially(params.text);
7273
} else {
@@ -75,7 +76,6 @@ const type = defineTabTool({
7576
}
7677

7778
if (params.submit) {
78-
response.setIncludeSnapshot();
7979
response.addCode(`await page.${await generateLocator(locator)}.press('Enter');`);
8080
await locator.press('Enter');
8181
}

src/tools/navigate.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import { z } from 'zod';
1818
import { defineTool, defineTabTool } from './tool.js';
19+
import { expectationSchema } from '../schemas/expectation.js';
1920

2021
const navigate = defineTool({
2122
capability: 'core',
@@ -26,6 +27,7 @@ const navigate = defineTool({
2627
description: 'Navigate to a URL',
2728
inputSchema: z.object({
2829
url: z.string().describe('The URL to navigate to'),
30+
expectation: expectationSchema
2931
}),
3032
type: 'destructive',
3133
},
@@ -34,7 +36,6 @@ const navigate = defineTool({
3436
const tab = await context.ensureTab();
3537
await tab.navigate(params.url);
3638

37-
response.setIncludeSnapshot();
3839
response.addCode(`await page.goto('${params.url}');`);
3940
},
4041
});
@@ -51,7 +52,6 @@ const goBack = defineTabTool({
5152

5253
handle: async (tab, params, response) => {
5354
await tab.page.goBack();
54-
response.setIncludeSnapshot();
5555
response.addCode(`await page.goBack();`);
5656
},
5757
});
@@ -67,7 +67,6 @@ const goForward = defineTabTool({
6767
},
6868
handle: async (tab, params, response) => {
6969
await tab.page.goForward();
70-
response.setIncludeSnapshot();
7170
response.addCode(`await page.goForward();`);
7271
},
7372
});

src/tools/snapshot.ts

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,23 @@ import { z } from 'zod';
1919
import { defineTabTool, defineTool } from './tool.js';
2020
import * as javascript from '../javascript.js';
2121
import { generateLocator } from './utils.js';
22+
import { expectationSchema } from '../schemas/expectation.js';
2223

2324
const snapshot = defineTool({
2425
capability: 'core',
2526
schema: {
2627
name: 'browser_snapshot',
2728
title: 'Page snapshot',
2829
description: 'Capture accessibility snapshot of the current page, this is better than screenshot',
29-
inputSchema: z.object({}),
30+
inputSchema: z.object({
31+
expectation: expectationSchema
32+
}),
3033
type: 'readOnly',
3134
},
3235

3336
handle: async (context, params, response) => {
3437
await context.ensureTab();
35-
response.setIncludeSnapshot();
38+
// Snapshot will be handled by expectation settings
3639
},
3740
});
3841

@@ -44,6 +47,7 @@ export const elementSchema = z.object({
4447
const clickSchema = elementSchema.extend({
4548
doubleClick: z.boolean().optional().describe('Whether to perform a double click instead of a single click'),
4649
button: z.enum(['left', 'right', 'middle']).optional().describe('Button to click, defaults to left'),
50+
expectation: expectationSchema
4751
});
4852

4953
const click = defineTabTool({
@@ -57,8 +61,6 @@ const click = defineTabTool({
5761
},
5862

5963
handle: async (tab, params, response) => {
60-
response.setIncludeSnapshot();
61-
6264
const locator = await tab.refLocator(params);
6365
const button = params.button;
6466
const buttonAttr = button ? `{ button: '${button}' }` : '';
@@ -68,7 +70,6 @@ const click = defineTabTool({
6870
else
6971
response.addCode(`await page.${await generateLocator(locator)}.click(${buttonAttr});`);
7072

71-
7273
await tab.waitForCompletion(async () => {
7374
if (params.doubleClick)
7475
await locator.dblclick({ button });
@@ -89,13 +90,12 @@ const drag = defineTabTool({
8990
startRef: z.string().describe('Exact source element reference from the page snapshot'),
9091
endElement: z.string().describe('Human-readable target element description used to obtain the permission to interact with the element'),
9192
endRef: z.string().describe('Exact target element reference from the page snapshot'),
93+
expectation: expectationSchema
9294
}),
9395
type: 'destructive',
9496
},
9597

9698
handle: async (tab, params, response) => {
97-
response.setIncludeSnapshot();
98-
9999
const [startLocator, endLocator] = await tab.refLocators([
100100
{ ref: params.startRef, element: params.startElement },
101101
{ ref: params.endRef, element: params.endElement },
@@ -115,13 +115,13 @@ const hover = defineTabTool({
115115
name: 'browser_hover',
116116
title: 'Hover mouse',
117117
description: 'Hover over element on page',
118-
inputSchema: elementSchema,
118+
inputSchema: elementSchema.extend({
119+
expectation: expectationSchema
120+
}),
119121
type: 'readOnly',
120122
},
121123

122124
handle: async (tab, params, response) => {
123-
response.setIncludeSnapshot();
124-
125125
const locator = await tab.refLocator(params);
126126
response.addCode(`await page.${await generateLocator(locator)}.hover();`);
127127

@@ -133,6 +133,7 @@ const hover = defineTabTool({
133133

134134
const selectOptionSchema = elementSchema.extend({
135135
values: z.array(z.string()).describe('Array of values to select in the dropdown. This can be a single value or multiple values.'),
136+
expectation: expectationSchema
136137
});
137138

138139
const selectOption = defineTabTool({
@@ -146,8 +147,6 @@ const selectOption = defineTabTool({
146147
},
147148

148149
handle: async (tab, params, response) => {
149-
response.setIncludeSnapshot();
150-
151150
const locator = await tab.refLocator(params);
152151
response.addCode(`await page.${await generateLocator(locator)}.selectOption(${javascript.formatObject(params.values)});`);
153152

tests/expectation-unit.spec.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { test, expect } from './fixtures.js';
18+
import { mergeExpectations } from '../src/schemas/expectation.js';
19+
20+
test.describe('Expectation Unit Tests', () => {
21+
test('mergeExpectations should respect user false values', () => {
22+
const userExpectation = {
23+
includeSnapshot: false,
24+
includeConsole: false,
25+
includeDownloads: false,
26+
includeTabs: false,
27+
includeCode: true
28+
};
29+
30+
const merged = mergeExpectations('browser_navigate', userExpectation);
31+
32+
expect(merged.includeSnapshot).toBe(false);
33+
expect(merged.includeConsole).toBe(false);
34+
expect(merged.includeDownloads).toBe(false);
35+
expect(merged.includeTabs).toBe(false);
36+
expect(merged.includeCode).toBe(true);
37+
});
38+
39+
test('mergeExpectations should use tool defaults when user expectation is undefined', () => {
40+
const merged = mergeExpectations('browser_navigate', undefined);
41+
42+
// Navigate tool defaults should be all true
43+
expect(merged.includeSnapshot).toBe(true);
44+
expect(merged.includeConsole).toBe(true);
45+
expect(merged.includeDownloads).toBe(true);
46+
expect(merged.includeTabs).toBe(true);
47+
expect(merged.includeCode).toBe(true);
48+
});
49+
50+
test('mergeExpectations should use tool defaults for click tool', () => {
51+
const merged = mergeExpectations('browser_click', undefined);
52+
53+
// Click tool defaults
54+
expect(merged.includeSnapshot).toBe(true);
55+
expect(merged.includeConsole).toBe(false);
56+
expect(merged.includeDownloads).toBe(false);
57+
expect(merged.includeTabs).toBe(false);
58+
expect(merged.includeCode).toBe(true);
59+
});
60+
61+
test('mergeExpectations should partially override tool defaults', () => {
62+
const userExpectation = {
63+
includeSnapshot: false
64+
// Other values should use tool defaults
65+
};
66+
67+
const merged = mergeExpectations('browser_click', userExpectation);
68+
69+
expect(merged.includeSnapshot).toBe(false); // User override
70+
expect(merged.includeConsole).toBe(false); // Tool default
71+
expect(merged.includeDownloads).toBe(false); // Tool default
72+
expect(merged.includeTabs).toBe(false); // Tool default
73+
expect(merged.includeCode).toBe(true); // Tool default
74+
});
75+
});

0 commit comments

Comments
 (0)