refactor: extract errorResult() and routedPageId() in ToolHandler (#3251)

## Summary

`ToolHandler` built the same `{content: [{type: 'text', text}], isError:
true}` literal in three places and decided the routed page id inline.
This PR adds two module-level helpers:

- `errorResult(text)` returns the error `CallToolResult`.
- `routedPageId(tool, serverArgs, args)` returns the page id a page tool
call targets, or `undefined` when the call targets the selected page.

`routedPageId()` requires an integer id. The input schema rejects a
non-integer before the value is used, so no behavior changes.

This is PR 1 of a stack; PR 2 calls both helpers from a second place.

## Stack

1. **This PR**.
2. `feat: run independent tool calls in parallel with a TaskRunner`
(#3243).
3. `feat: run page-scoped tools on different pages in parallel` (#3252).

## Tests

No test changes. The existing `ToolHandler` tests cover the disabled
result, the validation error result, the handler error result, and page
id routing.
This commit is contained in:
Alex Rudenko authored and GitHub committed 2026-10-20 11:26:47 +00:00
1 parent c6df57885d
commit 9a6b03acf7
1 file changed
+30 -30
+30 -30
View File
@@ -214,6 +214,31 @@ function formatValidationError(toolName: string, error: zod.ZodError): string {
return `Input validation error: Invalid arguments for tool ${toolName}: ${details}`;
}
function errorResult(text: string): CallToolResult {
return {
content: [{type: 'text', text}],
isError: true,
};
}
/**
* The page id a page tool call targets, or `undefined` when the call targets
* the selected page.
*/
function routedPageId(
tool: AnyToolDefinition,
serverArgs: ParsedArguments,
args: Record<string, unknown>,
): number | undefined {
if (!serverArgs.pageIdRouting || isSlimTool(tool)) {
return undefined;
}
const {pageId} = args;
return typeof pageId === 'number' && Number.isInteger(pageId)
? pageId
: undefined;
}
export class ToolHandler {
#tool: AnyToolDefinition;
#config: ReadonlyServerConfig;
@@ -381,10 +406,9 @@ export class ToolHandler {
try {
await validateToolFiles(tool, params, context, isLocalBrowser(browser));
if (isPageScopedTool(tool)) {
const pageId =
typeof params.pageId === 'number' ? params.pageId : undefined;
const pageId = routedPageId(tool, serverArgs, params);
page =
serverArgs.pageIdRouting && pageId !== undefined && !isSlimTool(tool)
pageId !== undefined
? context.getPageById(pageId)
: context.getSelectedMcpPage();
await page?.init({emulateFocus: true});
@@ -437,30 +461,14 @@ export class ToolHandler {
}
if (this.#disabledReason) {
return {
content: [
{
type: 'text',
text: this.#disabledReason,
},
],
isError: true,
};
return errorResult(this.#disabledReason);
}
const tool = this.#tool;
const serverArgs = this.#config.resolved;
const parseResult = this.#registeredInputSchema.safeParse(rawArgs);
if (!parseResult.success) {
return {
content: [
{
type: 'text',
text: formatValidationError(tool.name, parseResult.error),
},
],
isError: true,
};
return errorResult(formatValidationError(tool.name, parseResult.error));
}
const params = parseResult.data;
@@ -532,15 +540,7 @@ export class ToolHandler {
if ('cause' in err && err.cause) {
errorText += `\nCause: ${err.cause.message}`;
}
return {
content: [
{
type: 'text',
text: errorText,
},
],
isError: true,
};
return errorResult(errorText);
} finally {
void ClearcutLogger.get()?.logToolInvocation({
toolName: tool.name,