microsoft/playwright · error · Error
WebMCP tool " " is not registered in this frame
Error message
WebMCP tool "${params.name}" is not registered in this frame What it means
The page exposes a WebMCP modelContext, but no registered tool with the requested name is visible in the target frame. In Chromium the code filters getTools() results by name and by the tool's window scope; if no match is found it throws. The tool name or the frame is wrong.
Solutions
- Run the webmcp-list command (or equivalent) to list the tools actually registered in the target frame and match the exact name.
- Target the frame where the tool is registered (iframes register their own modelContext).
- Check for typos and casing in the tool name.
Example fix
// before
await client.callTool('webmcp-call', { params: { tool: 'getweather', inputJson: '{}' } });
// after
const tools = await client.callTool('webmcp-list', { params: {} });
// pick the exact registered name, e.g. 'get-weather'
await client.callTool('webmcp-call', { params: { tool: 'get-weather', inputJson: '{}' } }); Defensive patterns
Strategy: validation
Validate before calling
const names = await page.evaluate(() => (navigator.modelContext?.getTools?.() ?? []).map(t => t.name));
if (!names.includes(toolName)) throw new Error(`Tool ${toolName} not registered. Available: ${names.join(', ')}`); Type guard
null
Try / catch
try {
return await webmcpCall(frame, toolName, inputJson);
} catch (e) {
if (e.message.includes('is not registered')) {
// list tools and pick the correct name/frame before retrying once
}
throw e;
} Prevention
- Always enumerate registered tools (webmcp-list) before invoking.
- Match tool names exactly, including casing and dashes.
- Dispatch to the frame where the tool was registered, especially for iframe tools.
When it happens
Trigger: Calling a WebMCP tool whose name is not registered (typo, tool registered under a different name), or the tool is registered in a different frame/window than the one the call is dispatched to (tools with a 'window' scope are filtered per-frame).
Common situations: Tool names changed after a page update; invoking an iframe-registered tool from the main frame context or vice versa; case-sensitivity mistakes in tool names.
Understand the failure class
Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.
Related errors
- At least one of "files" or "data" must be provided.
- Frame has been detached.
- : expected one of (load|domcontentloaded|networkidle|commit)
- No element matching
- Provide an object with a `url`, `path` or `content` property
AI-assisted analysis of microsoft/playwright@f1d33b5029 (2026-09-15).
Data as JSON: /api/errors/3e74dd946aff9bdc.
Report an issue: GitHub.
Appendix: source
Thrown at packages/playwright-core/src/tools/backend/webmcp.ts:135
consequential: annotations.consequentialHint ?? annotations.consequential,
} : undefined,
origin: tool.origin,
};
}));
}
function callToolInPage(params: { name: string, inputJson: string }) {
const modelContext = (document as DocumentWithModelContext).modelContext
?? (navigator as NavigatorWithModelContext).modelContext;
if (!modelContext)
throw new Error('WebMCP is not available on this page');
const stringify = (result: unknown) => result === undefined ? 'null' : JSON.stringify(result);
if (modelContext.executeTool) {
// Chromium: executeTool(registeredTool, inputJsonString) resolves to a JSON string.
return Promise.resolve(modelContext.getTools!()).then(tools => {
const tool = tools.filter(t => !('window' in t) || t.window === window).find(t => t.name === params.name);
if (!tool)
throw new Error(`WebMCP tool "${params.name}" is not registered in this frame`);
return modelContext.executeTool!(tool, params.inputJson);
}).then(result => typeof result === 'string' ? result : stringify(result));
}
// Firefox: invokeTool(name, inputObject) resolves to the value itself.
return Promise.resolve(modelContext.invokeTool!(params.name, JSON.parse(params.inputJson))).then(stringify);
}
const kTimedOut = Symbol('timedOut');
async function withTimeout<T>(promise: Promise<T>, timeout: number): Promise<T | typeof kTimedOut> {
let timer: NodeJS.Timeout | undefined;
const timeoutPromise = new Promise<typeof kTimedOut>(resolve => {
timer = setTimeout(() => resolve(kTimedOut), timeout);
});
try {
return await Promise.race([promise, timeoutPromise]);
} finally {
clearTimeout(timer);View on GitHub (pinned to f1d33b5029)