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

  1. Run the webmcp-list command (or equivalent) to list the tools actually registered in the target frame and match the exact name.
  2. Target the frame where the tool is registered (iframes register their own modelContext).
  3. 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

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


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)