WebMCPTestingDebuggingDevTools

WebMCP Testing and Debugging: The Complete Developer Guide

Mukul Dutt
··12 min read

You've registered your WebMCP tools. The code looks right. The polyfill is loaded. But when you open the browser console and type navigator.modelContext.tools(), you get an empty array. Or worse, the tools show up but return garbage when you call them.

Sound familiar?

WebMCP testing and debugging is still a rough experience in 2026. There's no dedicated test framework yet. The tooling is scattered across Chrome flags, console commands, and ad-hoc Playwright scripts. Most developers end up guessing their way through issues because there's no single guide that covers the full workflow.

This is that guide. I'll walk you through setting up a proper testing environment, show you exactly how to debug tools in Chrome DevTools, cover automated testing strategies that actually work, and help you fix the most common bugs that trip developers up. Whether you're testing WebMCP tools locally for the first time or building a CI pipeline for production, everything you need is here.

Setting up your testing environment

Chrome Canary configuration

Before you can test anything, you need a browser that actually supports navigator.modelContext. Right now, that means Chrome Canary.

Download Chrome Canary from Google's channel page. It installs alongside your regular Chrome without replacing it. Once it's installed, open chrome://flags in the Canary address bar and search for "WebMCP" or "Model Context Protocol." Enable the flag and restart the browser.

After restart, open any page and type navigator.modelContext in the console. If you see an object instead of undefined, you're set. If it's still undefined, double-check that the flag is enabled and that you restarted Canary completely (not just the tab).

One thing that catches people: Chrome Canary auto-updates frequently, and flag names occasionally change between builds. If a flag disappears after an update, search for related terms like "model context" or "MCP" in the flags page. Google sometimes renames flags as features move through the release pipeline.

Local development server setup

WebMCP has an HTTPS requirement for production, but during local development you can work around this. The polyfill works on localhost over plain HTTP, and Chrome Canary treats localhost as a secure context by default.

Set up your local dev server however you normally would. If you're using Next.js, npm run dev is enough. For a static site, something like npx serve . works fine. The important thing is that your page loads the WebMCP polyfill or runs on a Chrome Canary build with the native flag enabled.

If you're testing with the polyfill, add it to your page before any tool registration code runs:

<script src="https://cdn.jsdelivr.net/npm/@anthropic-ai/webmcp-polyfill@latest/dist/polyfill.min.js"></script>
<script src="your-tools.js"></script>

Order matters here. The polyfill creates navigator.modelContext on the window object. If your tool registration script runs first, it'll try to call methods on an object that doesn't exist yet.

For HTTPS locally (useful if you're testing production-like conditions), use mkcert to generate a local SSL certificate:

mkcert -install
mkcert localhost 127.0.0.1
# Then configure your dev server to use the generated cert and key files

This gives you a trusted HTTPS connection on localhost without browser warnings.

Manual testing with Chrome DevTools

Inspecting registered tools

The DevTools console is your best friend for WebMCP debugging. Here are the commands I use constantly.

List all registered tools on the current page:

const tools = await navigator.modelContext.tools();
console.table(tools.map(t => ({ name: t.name, description: t.description })));

console.table makes the output much easier to read than the default object dump. You'll see each tool's name and description in a clean table format.

Inspect a specific tool's full schema:

const tools = await navigator.modelContext.tools();
const searchTool = tools.find(t => t.name === "search-products");
console.log(JSON.stringify(searchTool.inputSchema, null, 2));

This shows you the exact parameter definitions that an AI agent would see. Check that required fields are marked correctly, types match what your handler expects, and descriptions are clear enough for an agent to understand.

If navigator.modelContext.tools() returns an empty array, the issue is usually one of three things: the polyfill hasn't loaded yet (check the Network tab), your registration code threw an error (check the Console for errors), or the flag isn't enabled (type navigator.modelContext and see if it's undefined).

Simulating agent tool calls

Once you've confirmed tools are registered, test them the way an agent would: by calling them with parameters.

// Call a tool with test parameters
const result = await navigator.modelContext.callTool("search-products", {
  query: "headphones",
  maxPrice: 200
});
console.log(result);

Test with valid inputs first to verify the happy path. Then test the edges:

// Missing required parameter
await navigator.modelContext.callTool("search-products", {});

// Wrong parameter type
await navigator.modelContext.callTool("search-products", {
  query: 123,  // Should be string
  maxPrice: "not a number"
});

// Empty string
await navigator.modelContext.callTool("search-products", {
  query: ""
});

Watch what happens with each call. Does the tool return a clear error for invalid inputs? Does it handle missing optional parameters gracefully? Does it fail silently or throw?

I keep a snippet file of these test calls for each project. When I change a tool's implementation, I paste the full test suite into the console and verify nothing broke. It's manual, but it catches issues faster than you'd think.

One trick that saves time: use Chrome DevTools' "Sources" panel to set breakpoints inside your tool's execute function. When you call the tool from the console, execution pauses at your breakpoint and you can inspect variables, step through the logic, and verify the data flow.

Automated testing strategies

Unit testing tool handlers

Your tool's execute function is just a JavaScript function. You can test it outside the browser entirely.

Extract the handler into a separate module:

// tools/search-products.js
export async function searchProducts({ query, maxPrice }) {
  const response = await fetch(`/api/products?q=${encodeURIComponent(query)}&max=${maxPrice}`);
  const data = await response.json();
  return data.products.map(p => ({ name: p.name, price: p.price, url: p.url }));
}

// Registration (separate file)
import { searchProducts } from './tools/search-products.js';
navigator.modelContext.registerTool({
  name: "search-products",
  description: "Search products by keyword with optional price filter",
  inputSchema: { /* ... */ },
  execute: searchProducts
});

Now you can unit test searchProducts directly with Jest, Vitest, or any test runner:

import { searchProducts } from './tools/search-products';

test('returns products matching query', async () => {
  // Mock fetch with test data
  global.fetch = jest.fn().mockResolvedValue({
    json: () => Promise.resolve({
      products: [{ name: "Sony WH-1000XM5", price: 299, url: "/p/123" }]
    })
  });

  const result = await searchProducts({ query: "headphones", maxPrice: 500 });
  expect(result).toHaveLength(1);
  expect(result[0].name).toBe("Sony WH-1000XM5");
});

test('handles empty results', async () => {
  global.fetch = jest.fn().mockResolvedValue({
    json: () => Promise.resolve({ products: [] })
  });

  const result = await searchProducts({ query: "nonexistent", maxPrice: 100 });
  expect(result).toEqual([]);
});

This covers your business logic without needing a browser. It runs fast and works in any CI environment.

Integration testing with Playwright

For full end-to-end testing, you need Playwright driving Chrome Canary. This verifies that tools register correctly in a real browser, schemas are valid, and calls return expected data.

Configure Playwright to use Chrome Canary with the WebMCP flag:

// playwright.config.js
module.exports = {
  use: {
    channel: 'chrome-canary',
    launchOptions: {
      args: ['--enable-features=WebMCP']
    }
  }
};

Then write tests that verify tool behavior through the browser:

// tests/webmcp-tools.spec.js
const { test, expect } = require('@playwright/test');

test('tools are registered on page load', async ({ page }) => {
  await page.goto('http://localhost:3000');

  const tools = await page.evaluate(async () => {
    const t = await navigator.modelContext.tools();
    return t.map(tool => tool.name);
  });

  expect(tools).toContain('search-products');
  expect(tools).toContain('get-post');
});

test('search-products returns valid data', async ({ page }) => {
  await page.goto('http://localhost:3000');

  const result = await page.evaluate(async () => {
    return await navigator.modelContext.callTool('search-products', {
      query: 'test',
      maxPrice: 1000
    });
  });

  expect(Array.isArray(result)).toBe(true);
  if (result.length > 0) {
    expect(result[0]).toHaveProperty('name');
    expect(result[0]).toHaveProperty('price');
    expect(result[0]).toHaveProperty('url');
  }
});

These tests run in a real browser, call real tools, and verify real responses. They're slower than unit tests but catch issues that unit tests miss: registration failures, polyfill loading problems, and cross-origin errors.

CI/CD pipeline integration

Running WebMCP tests in CI requires Chrome Canary in your pipeline. GitHub Actions supports this through the setup-chrome action:

# .github/workflows/webmcp-tests.yml
name: WebMCP Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: browser-actions/setup-chrome@latest
        with:
          chrome-version: canary
      - run: npm ci
      - run: npm run dev &
      - run: npx playwright test --project=webmcp

The key detail is starting your dev server in the background (&) before running tests. Playwright needs a running server to navigate to.

If Chrome Canary isn't available in your CI environment, you can fall back to testing with the polyfill in standard Chrome. You won't catch native API differences, but you'll verify that your tool logic works correctly.

Common bugs and how to fix them

Tool registration failures

The most common issue: navigator.modelContext is undefined. This means either the WebMCP flag isn't enabled, the polyfill hasn't loaded, or you're running in a browser that doesn't support it.

Fix: Check the flag first. Then check the polyfill's network request in DevTools. If it loaded but navigator.modelContext is still undefined, the polyfill might have thrown an error during initialization. Check the console for errors.

Duplicate tool names are another frequent problem. If you register two tools with the same name, the behavior is undefined. Some implementations silently overwrite the first registration. Others throw an error. Either way, your agent won't get what it expects.

Fix: Use unique, descriptive tool names. A naming convention like {domain}-{action} (e.g., products-search, cart-add) prevents collisions.

Invalid schema definitions cause tools to register but fail when called. If your inputSchema has a syntax error or uses an unsupported JSON Schema feature, the tool appears in the registry but rejects every call.

Fix: Validate your schemas using a JSON Schema validator before registration. Or test by calling the tool in the console and watching for schema validation errors.

Execution errors

Async handler issues are the bug I see most often in production. If your tool's execute function returns a Promise but you forget to await an inner call, the tool returns undefined while the actual work happens in the background.

// Bug: missing await
execute: async ({ query }) => {
  fetch(`/api/search?q=${query}`);  // Missing await!
  // Returns undefined immediately
}

// Fix: await the fetch
execute: async ({ query }) => {
  const response = await fetch(`/api/search?q=${query}`);
  return await response.json();
}

CORS errors happen when your tool's execute function fetches data from a different origin. The browser blocks cross-origin requests unless the server sends the right headers. In the DevTools Network tab, you'll see the request marked as "blocked by CORS policy."

Fix: Either configure your API server to send Access-Control-Allow-Origin headers, or proxy the request through your own server. Don't try to disable CORS in the browser. That's a development shortcut that won't work in production.

Schema validation mismatches are sneaky. Your inputSchema says a field is a number, but your handler actually expects a string. Or the schema marks a field as required but your handler has a default fallback. The tool works sometimes and fails other times depending on which agent calls it and how.

Fix: Make your schema match your handler exactly. If the handler can work with or without a parameter, mark it as optional in the schema. If it needs a number, validate that the input is actually a number before using it.

Putting it all together

Start with manual testing in Chrome DevTools. Verify your tools register, check the schemas, call each tool with good and bad inputs. This catches 80% of issues in under ten minutes.

Then add unit tests for your handler functions. These run fast, catch logic bugs, and work in any CI environment without a browser.

Finally, add Playwright integration tests for the full browser flow. These are slower but verify the entire chain: polyfill loading, tool registration, schema validation, and execution in a real browser context.

That three-layer approach gives you confidence that your tools work correctly before any AI agent ever touches them. And when something does break in production, you'll have the debugging skills to track it down quickly.

Frequently asked questions

Can I test WebMCP without Chrome Canary?

Yes, using the WebMCP polyfill. Load it on your page and it creates navigator.modelContext in any modern browser. The polyfill covers the core API surface (tool registration, listing, and calling), so basic testing works everywhere. For testing native browser behavior and edge cases specific to Chrome's implementation, you'll still need Canary.

How do I debug tool descriptions that agents can't find?

Open the console and run navigator.modelContext.tools(). Check each tool's description field. Is it specific enough for an agent to understand when to use it? Vague descriptions like "do stuff" won't match any user intent. Good descriptions include what the tool does, what inputs it expects, and what it returns. Test by reading the description yourself and asking: would I know when to use this?

Is there a dedicated WebMCP testing framework?

Not yet as of early 2026. The current best practice is Playwright driving Chrome Canary with custom assertions. Several community projects are building WebMCP-specific test utilities, but none have reached production quality. For now, the combination of unit tests for handlers plus Playwright integration tests covers everything you need.

How do I test tools that require authentication?

Set up your test browser session with the required authentication state before running tool tests. In Playwright, use page.context().addCookies() to inject session cookies, or navigate to the login page and authenticate programmatically before testing protected tools. The tools themselves don't manage auth; they just check whether the browser session has the right permissions.

What's the fastest way to check if my tools are working?

Open Chrome Canary, navigate to your page, open the console, and run two commands: await navigator.modelContext.tools() to see registered tools, and await navigator.modelContext.callTool("your-tool-name", { param: "value" }) to test a call. If both return the expected data, your tools work. The whole check takes under 30 seconds.

Related Articles

Newsletter

Stay ahead of the curve

Get expert WebMCP insights, implementation guides, and ecosystem updates delivered to your inbox. No spam, unsubscribe anytime.