豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content

Commit 5f694c6

Browse files
authored
refactor: improve type safety for page scoped tools (#1051)
I think we should create proper classes for tool definitions soon. Follow-up for #1022
1 parent ef4b11d commit 5f694c6

27 files changed

+472
-197
lines changed

src/server.ts

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
SetLevelRequestSchema,
2424
} from './third_party/index.js';
2525
import {ToolCategory} from './tools/categories.js';
26-
import type {ToolDefinition} from './tools/ToolDefinition.js';
26+
import type {DefinedPageTool, ToolDefinition} from './tools/ToolDefinition.js';
2727
import {pageIdSchema} from './tools/ToolDefinition.js';
2828
import {createTools} from './tools/tools.js';
2929
import {VERSION} from './version.js';
@@ -107,7 +107,7 @@ export async function createMcpServer(
107107

108108
const toolMutex = new Mutex();
109109

110-
function registerTool(tool: ToolDefinition): void {
110+
function registerTool(tool: ToolDefinition | DefinedPageTool): void {
111111
if (
112112
tool.annotations.category === ToolCategory.EMULATION &&
113113
serverArgs.categoryEmulation === false
@@ -151,7 +151,9 @@ export async function createMcpServer(
151151
return;
152152
}
153153
const schema =
154-
tool.annotations.pageScoped && serverArgs.experimentalPageIdRouting
154+
'pageScoped' in tool &&
155+
tool.pageScoped &&
156+
serverArgs.experimentalPageIdRouting
155157
? {...tool.schema, ...pageIdSchema}
156158
: tool.schema;
157159

@@ -174,22 +176,31 @@ export async function createMcpServer(
174176
const response = serverArgs.slim
175177
? new SlimMcpResponse(serverArgs)
176178
: new McpResponse(serverArgs);
177-
const page =
178-
tool.annotations.pageScoped && serverArgs.experimentalPageIdRouting
179-
? context.resolvePageById(params.pageId as number | undefined)
180-
: undefined;
181-
if (page) {
182-
context.setRequestPage(page);
183-
}
184179
try {
185-
await tool.handler(
186-
{
187-
params,
188-
page,
189-
},
190-
response,
191-
context,
192-
);
180+
if ('pageScoped' in tool && tool.pageScoped) {
181+
const page =
182+
serverArgs.experimentalPageIdRouting && params.pageId
183+
? context.resolvePageById(params.pageId)
184+
: context.getSelectedPage();
185+
context.setRequestPage(page);
186+
await tool.handler(
187+
{
188+
params,
189+
page,
190+
},
191+
response,
192+
context,
193+
);
194+
} else {
195+
await tool.handler(
196+
// @ts-expect-error types do not match.
197+
{
198+
params,
199+
},
200+
response,
201+
context,
202+
);
203+
}
193204
const {content, structuredContent} = await response.handle(
194205
tool.name,
195206
context,

src/tools/ToolDefinition.ts

Lines changed: 59 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import type {PaginationOptions} from '../utils/types.js';
2020

2121
import type {ToolCategory} from './categories.js';
2222

23-
export interface ToolDefinition<
23+
export interface BaseToolDefinition<
2424
Schema extends zod.ZodRawShape = zod.ZodRawShape,
2525
> {
2626
name: string;
@@ -33,14 +33,14 @@ export interface ToolDefinition<
3333
*/
3434
readOnlyHint: boolean;
3535
conditions?: string[];
36-
/**
37-
* If true, the tool operates on a specific page.
38-
* The `pageId` schema field is auto-injected and the resolved
39-
* page is provided via `request.page`.
40-
*/
41-
pageScoped?: boolean;
4236
};
4337
schema: Schema;
38+
}
39+
40+
export interface ToolDefinition<
41+
Schema extends zod.ZodRawShape = zod.ZodRawShape,
42+
> extends BaseToolDefinition<Schema> {
43+
schema: Schema;
4444
handler: (
4545
request: Request<Schema>,
4646
response: Response,
@@ -50,8 +50,6 @@ export interface ToolDefinition<
5050

5151
export interface Request<Schema extends zod.ZodRawShape> {
5252
params: zod.objectOutputType<Schema, zod.ZodTypeAny>;
53-
/** Populated centrally for tools with `pageScoped: true`. */
54-
page?: Page;
5553
}
5654

5755
export interface ImageContentData {
@@ -215,28 +213,65 @@ export function defineTool<
215213
if (typeof definition === 'function') {
216214
const factory = definition;
217215
return (args: Args) => {
218-
const tool = factory(args);
219-
wrapPageScopedHandler(tool);
220-
return tool;
216+
return factory(args);
221217
};
222218
}
223-
wrapPageScopedHandler(definition);
224219
return definition;
225220
}
226221

227-
function wrapPageScopedHandler<Schema extends zod.ZodRawShape>(
228-
definition: ToolDefinition<Schema>,
229-
) {
230-
if (definition.annotations.pageScoped) {
231-
const originalHandler = definition.handler;
232-
definition.handler = async (request, response, context) => {
233-
// In production, main.ts resolves request.page centrally before calling
234-
// the handler. This fallback exists for tests that invoke handlers
235-
// directly without going through main.ts.
236-
request.page ??= context.getSelectedPage();
237-
return originalHandler(request, response, context);
222+
interface PageToolDefinition<
223+
Schema extends zod.ZodRawShape = zod.ZodRawShape,
224+
> extends BaseToolDefinition<Schema> {
225+
handler: (
226+
request: Request<Schema> & {page: Page},
227+
response: Response,
228+
context: Context,
229+
) => Promise<void>;
230+
}
231+
232+
export type DefinedPageTool<Schema extends zod.ZodRawShape = zod.ZodRawShape> =
233+
PageToolDefinition<Schema> & {
234+
pageScoped: true;
235+
handler: (
236+
request: Request<Schema> & {page: Page},
237+
response: Response,
238+
context: Context,
239+
) => Promise<void>;
240+
};
241+
242+
export function definePageTool<Schema extends zod.ZodRawShape>(
243+
definition: PageToolDefinition<Schema>,
244+
): DefinedPageTool<Schema>;
245+
246+
export function definePageTool<
247+
Schema extends zod.ZodRawShape,
248+
Args extends ParsedArguments = ParsedArguments,
249+
>(
250+
definition: (args?: Args) => PageToolDefinition<Schema>,
251+
): (args?: Args) => DefinedPageTool<Schema>;
252+
253+
export function definePageTool<
254+
Schema extends zod.ZodRawShape,
255+
Args extends ParsedArguments = ParsedArguments,
256+
>(
257+
definition:
258+
| PageToolDefinition<Schema>
259+
| ((args?: Args) => PageToolDefinition<Schema>),
260+
): DefinedPageTool<Schema> | ((args?: Args) => DefinedPageTool<Schema>) {
261+
if (typeof definition === 'function') {
262+
return (args?: Args): DefinedPageTool<Schema> => {
263+
const tool = definition(args);
264+
return {
265+
...tool,
266+
pageScoped: true,
267+
};
238268
};
239269
}
270+
271+
return {
272+
...definition,
273+
pageScoped: true,
274+
} as DefinedPageTool<Schema>;
240275
}
241276

242277
export const CLOSE_PAGE_ERROR =

src/tools/console.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {zod} from '../third_party/index.js';
88
import type {ConsoleMessageType} from '../third_party/index.js';
99

1010
import {ToolCategory} from './categories.js';
11-
import {defineTool} from './ToolDefinition.js';
11+
import {definePageTool} from './ToolDefinition.js';
1212
type ConsoleResponseType = ConsoleMessageType | 'issue';
1313

1414
const FILTERABLE_MESSAGE_TYPES: [
@@ -37,14 +37,13 @@ const FILTERABLE_MESSAGE_TYPES: [
3737
'issue',
3838
];
3939

40-
export const listConsoleMessages = defineTool({
40+
export const listConsoleMessages = definePageTool({
4141
name: 'list_console_messages',
4242
description:
4343
'List all console messages for the currently selected page since the last navigation.',
4444
annotations: {
4545
category: ToolCategory.DEBUGGING,
4646
readOnlyHint: true,
47-
pageScoped: true,
4847
},
4948
schema: {
5049
pageSize: zod
@@ -87,13 +86,12 @@ export const listConsoleMessages = defineTool({
8786
},
8887
});
8988

90-
export const getConsoleMessage = defineTool({
89+
export const getConsoleMessage = definePageTool({
9190
name: 'get_console_message',
9291
description: `Gets a console message by its ID. You can get all messages by calling ${listConsoleMessages.name}.`,
9392
annotations: {
9493
category: ToolCategory.DEBUGGING,
9594
readOnlyHint: true,
96-
pageScoped: true,
9795
},
9896
schema: {
9997
msgid: zod

src/tools/emulation.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,20 @@
88
import {zod, PredefinedNetworkConditions} from '../third_party/index.js';
99

1010
import {ToolCategory} from './categories.js';
11-
import {defineTool} from './ToolDefinition.js';
11+
import {definePageTool} from './ToolDefinition.js';
1212

1313
const throttlingOptions: [string, ...string[]] = [
1414
'No emulation',
1515
'Offline',
1616
...Object.keys(PredefinedNetworkConditions),
1717
];
1818

19-
export const emulate = defineTool({
19+
export const emulate = definePageTool({
2020
name: 'emulate',
2121
description: `Emulates various features on the selected page.`,
2222
annotations: {
2323
category: ToolCategory.EMULATION,
2424
readOnlyHint: false,
25-
pageScoped: true,
2625
},
2726
schema: {
2827
networkConditions: zod
@@ -105,7 +104,7 @@ export const emulate = defineTool({
105104
),
106105
},
107106
handler: async (request, _response, context) => {
108-
const page = request.page!;
107+
const page = request.page;
109108
await context.emulate(request.params, page);
110109
},
111110
});

0 commit comments

Comments
 (0)