microsoft/playwright · error · Error

WebMCP is not available on this page

Error message

WebMCP is not available on this page

What it means

This error is thrown inside an injected page-side function: neither document.modelContext nor navigator.modelContext is present, meaning the page does not expose the WebMCP model-context API. Only pages that registered a WebMCP ModelContext can have tools invoked in them.

Solutions

  1. Navigate to a page that actually registers a WebMCP modelContext before calling tools.
  2. Verify document.modelContext or navigator.modelContext exists in the target frame (evaluate a probe expression).
  3. Ensure the browser build supports WebMCP and that the page's MCP registration script has finished loading.

Example fix

// before
await client.callTool('webmcp-call', { params: { tool: 'get_weather', inputJson: '{}' } }); // page has no modelContext
// after
await page.goto('https://example.com/webmcp-enabled');
await page.waitForFunction(() => 'modelContext' in navigator || 'modelContext' in document);
await client.callTool('webmcp-call', { params: { tool: 'get_weather', inputJson: '{}' } });
Defensive patterns

Strategy: validation

Validate before calling

const hasWebMcp = await page.evaluate(() => 'modelContext' in navigator || 'modelContext' in document);
if (!hasWebMcp) throw new Error('This page does not expose WebMCP modelContext');

Type guard

function pageHasModelContext(doc: Document): doc is Document & { modelContext: object } {
  return 'modelContext' in doc || 'modelContext' in navigator;
}

Try / catch

try {
  return await webmcpCall(frame, tool, inputJson);
} catch (e) {
  if (e.message.includes('WebMCP is not available')) throw new Error(`Frame ${frame.url()} has no WebMCP modelContext`);
  throw e;
}

Prevention

When it happens

Trigger: Invoking a WebMCP tool call (via the webmcp backend callToolInPage) against a frame whose document/navigator lack a modelContext — e.g. an about:blank page, a non-MCP-enabled site, or a browser build without WebMCP support.

Common situations: Pointing the WebMCP CLI/tooling at the wrong tab or frame; testing on a page before its WebMCP polyfill/script has loaded; running against an older browser that has not shipped the modelContext API.

Related errors


AI-assisted analysis of microsoft/playwright@f1d33b5029 (2026-09-15). Data as JSON: /api/errors/1c572a347524abe7. Report an issue: GitHub.

Appendix: source

Thrown at packages/playwright-core/src/tools/backend/webmcp.ts:128

      title: tool.title || undefined,
      description: tool.description ?? '',
      inputSchema,
      // The JS surface uses the `*Hint` names, the CDP WebMCP domain uses the short ones.
      annotations: annotations ? {
        readOnly: annotations.readOnlyHint ?? annotations.readOnly,
        untrustedContent: annotations.untrustedContentHint ?? annotations.untrustedContent,
        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;

View on GitHub (pinned to f1d33b5029)