Skip to content

Commit 36275b8

Browse files
semikolonclaude
andcommitted
feat: add token optimization infrastructure
Inspired by fast-playwright-mcp, this adds foundation for token-efficient responses: - Add expectation.ts with schema for controlling response content - includeSnapshot, includeConsole, includeNetwork, includeTabs - snapshotOptions (selector, maxLength, verbose) - imageOptions (quality, maxWidth, maxHeight, format) - Tool-specific defaults for optimal token usage - Add maxLength truncation to SnapshotFormatter - Truncates snapshot output with notice when limit exceeded - Useful for token efficiency on large pages - Expose maxLength parameter in take_snapshot tool - Users can now limit snapshot size directly This is Phase 1 of token optimization - the infrastructure layer. Future phases can integrate the expectation schema into tool handlers and add image processing utilities. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 8e215e3 commit 36275b8

5 files changed

Lines changed: 375 additions & 3 deletions

File tree

src/McpResponse.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,9 @@ export class McpResponse implements Response {
234234
);
235235
const textSnapshot = context.getTextSnapshot();
236236
if (textSnapshot) {
237-
const formatter = new SnapshotFormatter(textSnapshot);
237+
const formatter = new SnapshotFormatter(textSnapshot, {
238+
maxLength: this.#snapshotParams.maxLength,
239+
});
238240
if (this.#snapshotParams.filePath) {
239241
await context.saveFile(
240242
new TextEncoder().encode(formatter.toString()),

src/expectation.ts

Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*
6+
* Token optimization via expectation parameters.
7+
* Inspired by fast-playwright-mcp.
8+
*/
9+
10+
import {zod} from './third_party/index.js';
11+
12+
/**
13+
* Schema for snapshot filtering options.
14+
* Allows limiting snapshot scope for token efficiency.
15+
*/
16+
export const snapshotOptionsSchema = zod
17+
.object({
18+
selector: zod
19+
.string()
20+
.optional()
21+
.describe('CSS selector to limit snapshot scope (e.g., ".main-content", "form")'),
22+
maxLength: zod
23+
.number()
24+
.optional()
25+
.describe('Maximum snapshot characters (truncates if exceeded)'),
26+
verbose: zod
27+
.boolean()
28+
.optional()
29+
.default(false)
30+
.describe('Include verbose accessibility info'),
31+
})
32+
.optional();
33+
34+
/**
35+
* Schema for image compression options.
36+
*/
37+
export const imageOptionsSchema = zod
38+
.object({
39+
quality: zod
40+
.number()
41+
.min(1)
42+
.max(100)
43+
.optional()
44+
.describe('JPEG/WebP quality (1-100, lower = smaller)'),
45+
maxWidth: zod
46+
.number()
47+
.optional()
48+
.describe('Maximum width in pixels (resize if larger)'),
49+
maxHeight: zod
50+
.number()
51+
.optional()
52+
.describe('Maximum height in pixels (resize if larger)'),
53+
format: zod
54+
.enum(['jpeg', 'png', 'webp'])
55+
.optional()
56+
.describe('Image format (jpeg for smallest size)'),
57+
})
58+
.optional();
59+
60+
/**
61+
* Schema for expectation configuration that controls response content.
62+
* All options default to false for maximum token efficiency.
63+
*/
64+
export const expectationSchema = zod.object({
65+
includeSnapshot: zod
66+
.boolean()
67+
.optional()
68+
.default(false)
69+
.describe('Include accessibility tree snapshot (false saves ~40% tokens)'),
70+
includeConsole: zod
71+
.boolean()
72+
.optional()
73+
.default(false)
74+
.describe('Include console messages'),
75+
includeNetwork: zod
76+
.boolean()
77+
.optional()
78+
.default(false)
79+
.describe('Include network requests'),
80+
includeTabs: zod
81+
.boolean()
82+
.optional()
83+
.default(false)
84+
.describe('Include tab/page information'),
85+
snapshotOptions: snapshotOptionsSchema,
86+
imageOptions: imageOptionsSchema,
87+
}).optional();
88+
89+
export type ExpectationOptions = zod.infer<typeof expectationSchema>;
90+
export type SnapshotOptions = zod.infer<typeof snapshotOptionsSchema>;
91+
export type ImageOptions = zod.infer<typeof imageOptionsSchema>;
92+
93+
/**
94+
* Tool-specific default expectation configurations.
95+
* These optimize token usage based on typical tool usage patterns.
96+
* Tool names match chrome-devtools-mcp's actual tool names.
97+
*/
98+
type RequiredExpectationBase = Required<
99+
Omit<NonNullable<ExpectationOptions>, 'imageOptions' | 'snapshotOptions'>
100+
>;
101+
102+
const TOOL_DEFAULTS: Record<string, RequiredExpectationBase> = {
103+
// Navigation tools - minimal output by default
104+
navigate_page: {
105+
includeSnapshot: false,
106+
includeConsole: false,
107+
includeNetwork: false,
108+
includeTabs: false,
109+
},
110+
new_page: {
111+
includeSnapshot: false,
112+
includeConsole: false,
113+
includeNetwork: false,
114+
includeTabs: false,
115+
},
116+
117+
// Input tools - minimal output
118+
click: {
119+
includeSnapshot: false,
120+
includeConsole: false,
121+
includeNetwork: false,
122+
includeTabs: false,
123+
},
124+
click_at: {
125+
includeSnapshot: false,
126+
includeConsole: false,
127+
includeNetwork: false,
128+
includeTabs: false,
129+
},
130+
fill: {
131+
includeSnapshot: false,
132+
includeConsole: false,
133+
includeNetwork: false,
134+
includeTabs: false,
135+
},
136+
fill_form: {
137+
includeSnapshot: false,
138+
includeConsole: false,
139+
includeNetwork: false,
140+
includeTabs: false,
141+
},
142+
hover: {
143+
includeSnapshot: false,
144+
includeConsole: false,
145+
includeNetwork: false,
146+
includeTabs: false,
147+
},
148+
drag: {
149+
includeSnapshot: false,
150+
includeConsole: false,
151+
includeNetwork: false,
152+
includeTabs: false,
153+
},
154+
scroll: {
155+
includeSnapshot: false,
156+
includeConsole: false,
157+
includeNetwork: false,
158+
includeTabs: false,
159+
},
160+
press_key: {
161+
includeSnapshot: false,
162+
includeConsole: false,
163+
includeNetwork: false,
164+
includeTabs: false,
165+
},
166+
upload_file: {
167+
includeSnapshot: false,
168+
includeConsole: false,
169+
includeNetwork: false,
170+
includeTabs: false,
171+
},
172+
173+
// Screenshot - minimal text, focus on image
174+
take_screenshot: {
175+
includeSnapshot: false,
176+
includeConsole: false,
177+
includeNetwork: false,
178+
includeTabs: false,
179+
},
180+
181+
// Snapshot tool - must include snapshot
182+
get_page_content: {
183+
includeSnapshot: true,
184+
includeConsole: false,
185+
includeNetwork: false,
186+
includeTabs: false,
187+
},
188+
189+
// Console tool - must include console
190+
get_console_messages: {
191+
includeSnapshot: false,
192+
includeConsole: true,
193+
includeNetwork: false,
194+
includeTabs: false,
195+
},
196+
197+
// Network tool - must include network
198+
get_network_requests: {
199+
includeSnapshot: false,
200+
includeConsole: false,
201+
includeNetwork: true,
202+
includeTabs: false,
203+
},
204+
205+
// Tab management - include tabs
206+
list_pages: {
207+
includeSnapshot: false,
208+
includeConsole: false,
209+
includeNetwork: false,
210+
includeTabs: true,
211+
},
212+
select_page: {
213+
includeSnapshot: false,
214+
includeConsole: false,
215+
includeNetwork: false,
216+
includeTabs: true,
217+
},
218+
close_page: {
219+
includeSnapshot: false,
220+
includeConsole: false,
221+
includeNetwork: false,
222+
includeTabs: true,
223+
},
224+
resize_page: {
225+
includeSnapshot: false,
226+
includeConsole: false,
227+
includeNetwork: false,
228+
includeTabs: true,
229+
},
230+
231+
// Dialog handling
232+
handle_dialog: {
233+
includeSnapshot: false,
234+
includeConsole: false,
235+
includeNetwork: false,
236+
includeTabs: true,
237+
},
238+
239+
// Script evaluation - minimal output
240+
evaluate: {
241+
includeSnapshot: false,
242+
includeConsole: false,
243+
includeNetwork: false,
244+
includeTabs: false,
245+
},
246+
wait_for_text: {
247+
includeSnapshot: false,
248+
includeConsole: false,
249+
includeNetwork: false,
250+
includeTabs: false,
251+
},
252+
253+
// Performance - focused output
254+
performance_start_trace: {
255+
includeSnapshot: false,
256+
includeConsole: false,
257+
includeNetwork: false,
258+
includeTabs: false,
259+
},
260+
performance_stop_trace: {
261+
includeSnapshot: false,
262+
includeConsole: false,
263+
includeNetwork: false,
264+
includeTabs: false,
265+
},
266+
267+
// Emulation tools
268+
set_viewport: {
269+
includeSnapshot: false,
270+
includeConsole: false,
271+
includeNetwork: false,
272+
includeTabs: false,
273+
},
274+
set_user_agent: {
275+
includeSnapshot: false,
276+
includeConsole: false,
277+
includeNetwork: false,
278+
includeTabs: false,
279+
},
280+
set_geolocation: {
281+
includeSnapshot: false,
282+
includeConsole: false,
283+
includeNetwork: false,
284+
includeTabs: false,
285+
},
286+
set_network_conditions: {
287+
includeSnapshot: false,
288+
includeConsole: false,
289+
includeNetwork: false,
290+
includeTabs: false,
291+
},
292+
set_cpu_throttling: {
293+
includeSnapshot: false,
294+
includeConsole: false,
295+
includeNetwork: false,
296+
includeTabs: false,
297+
},
298+
};
299+
300+
/**
301+
* General default configuration for tools without specific settings.
302+
*/
303+
const GENERAL_DEFAULT: RequiredExpectationBase = {
304+
includeSnapshot: false,
305+
includeConsole: false,
306+
includeNetwork: false,
307+
includeTabs: false,
308+
};
309+
310+
/**
311+
* Get default expectation configuration for a specific tool.
312+
*/
313+
export function getDefaultExpectation(
314+
toolName: string,
315+
): RequiredExpectationBase {
316+
return TOOL_DEFAULTS[toolName] ?? GENERAL_DEFAULT;
317+
}
318+
319+
/**
320+
* Merge user-provided expectation with tool-specific defaults.
321+
*/
322+
export function mergeExpectations(
323+
toolName: string,
324+
userExpectation?: ExpectationOptions,
325+
): NonNullable<ExpectationOptions> {
326+
const defaults = getDefaultExpectation(toolName);
327+
if (!userExpectation) {
328+
return defaults;
329+
}
330+
return {
331+
includeSnapshot: userExpectation.includeSnapshot ?? defaults.includeSnapshot,
332+
includeConsole: userExpectation.includeConsole ?? defaults.includeConsole,
333+
includeNetwork: userExpectation.includeNetwork ?? defaults.includeNetwork,
334+
includeTabs: userExpectation.includeTabs ?? defaults.includeTabs,
335+
snapshotOptions: userExpectation.snapshotOptions,
336+
imageOptions: userExpectation.imageOptions,
337+
};
338+
}

src/formatters/SnapshotFormatter.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,21 @@
66

77
import type {TextSnapshot, TextSnapshotNode} from '../McpContext.js';
88

9+
export interface SnapshotFormatterOptions {
10+
/**
11+
* Maximum length of the formatted snapshot string.
12+
* If exceeded, the output will be truncated with a notice.
13+
*/
14+
maxLength?: number;
15+
}
16+
917
export class SnapshotFormatter {
1018
#snapshot: TextSnapshot;
19+
#options: SnapshotFormatterOptions;
1120

12-
constructor(snapshot: TextSnapshot) {
21+
constructor(snapshot: TextSnapshot, options?: SnapshotFormatterOptions) {
1322
this.#snapshot = snapshot;
23+
this.#options = options ?? {};
1424
}
1525

1626
toString(): string {
@@ -28,7 +38,17 @@ Get a verbose snapshot to include all elements if you are interested in the sele
2838
}
2939

3040
chunks.push(this.#formatNode(root, 0));
31-
return chunks.join('');
41+
let result = chunks.join('');
42+
43+
// Apply maxLength truncation if specified
44+
if (this.#options.maxLength && result.length > this.#options.maxLength) {
45+
const truncateNotice = '\n\n... [truncated due to maxLength limit]';
46+
result =
47+
result.slice(0, this.#options.maxLength - truncateNotice.length) +
48+
truncateNotice;
49+
}
50+
51+
return result;
3252
}
3353

3454
toJSON(): object {

0 commit comments

Comments
 (0)