Back to Blog

I Built a DevTools Extension to Debug WebMCP and window.ai

WebMCP Debugger Chrome DevTools extension intercepting AI events from a web page into a structured timeline

TL;DR - Key Takeaways

  1. WebMCP Debugger is an open-source Chrome DevTools extension that gives you a Network-panel-like view into every window.ai and navigator.modelContext interaction on any web page
  2. Monkey-patching in MAIN world — the extension intercepts LanguageModel.create, prompt, promptStreaming, and all modelContext methods without modifying page code
  3. Three-layer architecture — MAIN world interceptor, ISOLATED world bridge, and background service worker pass events to a React-based DevTools panel
  4. Real-time tool tracking — see registered WebMCP tools, inspect their JSON schemas, and execute them manually from the panel
  5. Timeline and sessions — chronological event log with filtering, plus per-session prompt/response threads with streaming visualization
  6. Available now — requires Chrome 146+ with chrome://flags/#webmcp-testing enabled; install by loading the dist/ folder as an unpacked extension

Why I Built This

If you've been following the WebMCP standard or experimenting with Chrome's built-in window.ai (Prompt API / Gemini Nano), you've probably hit the same wall I did: there's no way to see what's actually happening.

You register a tool with navigator.modelContext.registerTool() — did it work? An AI agent calls your tool — what arguments did it pass? A promptStreaming() call starts returning chunks — are they accumulating correctly?

The browser's console gives you console.log and breakpoints, but AI interactions are asynchronous, multi-step, and often involve streaming. What I wanted was something closer to the Network panel — a chronological, filterable, inspectable view of every AI event happening on the page.

So I built one.


What It Does

WebMCP Debugger adds a new panel to Chrome DevTools. Open DevTools on any page running WebMCP or window.ai code, and you get a real-time dashboard of all AI activity.

WebMCP Debugger panel showing a timeline of AI events with category filters and a JSON payload detail pane

Timeline View

Every AI event gets logged chronologically — tool registrations, prompt sends, streaming chunks, tool calls, tool results, context clears, page reloads. You can filter by event category (Tools, Prompts, Streaming, System) and click any entry to inspect its full payload.

Tool Discovery and Execution

The panel derives the current set of registered tools from the event history, accounting for unregistrations, provideContext replacements, and page reloads. For each tool you can:

  • View the full JSON Schema (inputSchema)
  • Read the description and annotations
  • Execute the tool manually with a built-in JSON editor
  • See the result (or error) in the timeline

AI Session Threads

When LanguageModel.create() is called, the extension tracks the session. Subsequent prompt() and promptStreaming() calls are grouped by session ID, showing the full request-response thread. For streaming responses, you can see the text accumulate in real time.


Architecture: Three Worlds, One Event Stream

Chrome extensions have a unique constraint: content scripts run in an ISOLATED world that shares the DOM but not the JavaScript context of the page. The page's window.ai and navigator.modelContext objects live in the MAIN world. The DevTools panel runs in yet another context. Getting events from the page to the panel requires a three-hop relay.

Four-layer architecture: MAIN world interceptor to ISOLATED bridge to background service worker to React DevTools panel

ai-interceptor.ts (MAIN world)
    │  monkey-patches LanguageModel + modelContext
    │  posts events via window.postMessage
    ▼
bridge.ts (ISOLATED world)
    │  receives postMessage events
    │  forwards via chrome.runtime.sendMessage
    ▼
background/index.ts (Service Worker)
    │  stores events in per-tab EventStore
    │  forwards to connected panels via port
    ▼
panel/src/App.tsx (DevTools Panel)
    │  React app with filter bar, event table, detail pane
    │  connected via chrome.runtime.connect port

Layer 1: The MAIN World Interceptor

The core of the extension is ai-interceptor.ts, injected into the page's MAIN world via chrome.scripting.executeScript({ world: 'MAIN' }). It monkey-patches two APIs:

How monkey-patching works: the interceptor wraps original API calls, emits events to the bridge, and returns results unchanged

LanguageModel (window.ai)

const originalCreate = LM.create.bind(LM);
LM.create = async function (options) {
  const session = await originalCreate(options);
  const sessionId = crypto.randomUUID();

  // Wrap prompt()
  const originalPrompt = session.prompt.bind(session);
  session.prompt = async function (input, opts) {
    emit("PROMPT_SENT", { sessionId, input, opts, ts: Date.now() });
    const result = await originalPrompt(input, opts);
    emit("PROMPT_RESPONSE", { sessionId, result, ts: Date.now() });
    return result;
  };

  // Wrap promptStreaming() — returns a new ReadableStream that
  // re-emits every chunk while accumulating the full text
  // ...

  emit("SESSION_CREATED", { sessionId, options, ts: Date.now() });
  return session;
};

Every method gets wrapped with a before/after event emission. The interceptor preserves the original behavior — it calls the real function, captures the result, emits an event, and returns the result unchanged. Errors are also captured and re-thrown.

navigator.modelContext (WebMCP)

The same pattern applies to registerTool, unregisterTool, provideContext, and clearContext. One subtlety: events are emitted after calling the original function, not before. This prevents false positives — if the browser throws (e.g., duplicate tool name or invalid schema), no event is emitted.

mc.registerTool = function (toolDef) {
  const result = origRegister(wrapToolExecute(toolDef));
  emit("TOOL_REGISTERED", { tool: toolMeta(toolDef), ts: Date.now() });
  return result;
};

Tool execute callbacks get wrapped too, so the extension captures TOOL_CALL and TOOL_RESULT_AI events when an AI agent invokes a tool.

Layer 2: The ISOLATED World Bridge

The ISOLATED world content script (bridge.ts) is declared in manifest.json and runs at document_start. It does two things:

  1. Listens for window.postMessage events from the interceptor and forwards them via chrome.runtime.sendMessage
  2. Listens for native WebMCP window events (toolactivated, toolcancel) and forwards those too
window.addEventListener("message", (event) => {
  if (event.source !== window) return;
  if (event.data?.source !== "webmcp-debugger") return;
  forward({ source: "webmcp-debugger", type: event.data.type, data: event.data.data });
});

The bridge is intentionally thin — it doesn't transform or filter events, just relays them across the extension boundary.

Layer 3: Background Service Worker

The background script receives events from the bridge and handles three responsibilities:

Per-tab event storage. An EventStore class (backed by chrome.storage.session) maintains a separate event list for each tab. This means multiple tabs can run WebMCP independently without cross-contamination.

Panel management. A TabManager tracks which DevTools panel port is observing which tab. When an event arrives for tab 42, only the panel inspecting tab 42 receives it.

Navigation handling. When a tab navigates (chrome.tabs.onUpdated with status: "loading"), the background re-injects the MAIN world interceptor. This is necessary because navigation destroys the MAIN world context — the monkey-patches are gone. The background also emits a PAGE_RELOAD event so the panel knows to reset its tool list.

chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
  if (changeInfo.status === "loading") {
    eventStore.add(tabId, { type: "PAGE_RELOAD", ts: Date.now() });
    tabManager.notifyPanel(tabId, { type: "PAGE_RELOAD" });
    chrome.scripting.executeScript({
      target: { tabId },
      world: "MAIN",
      files: ["content/ai-interceptor.js"],
    });
  }
});

Layer 4: The DevTools Panel

The panel is a React app with Tailwind CSS, connected to the background via chrome.runtime.connect. It uses a reducer-based state management pattern (via InspectorContext) and renders three main components:

Component Purpose
FilterBar Category tabs (All, Tools, Prompts, Streaming, System) + clear button
EventTable Chronological list of events with type icons, timestamps, and summary
EventDetail Expandable JSON view of the selected event's full payload
StatusBar Connection status, event count, tool count

The panel requests initial state (GET_STATE) when it connects, hydrating from the EventStore so you don't lose events if you open DevTools after the page has been running.


Lessons Learned Building It

MAIN vs ISOLATED World Is the Fundamental Constraint

Chrome extensions cannot directly access page JavaScript from content scripts. The content script sees the DOM but runs in a separate JavaScript context. This means you can't just import the page's LanguageModel from a content script — it doesn't exist there.

The solution is chrome.scripting.executeScript({ world: 'MAIN' }), which injects code into the page's own context. But MAIN world scripts can't use chrome.* APIs. Hence the two-layer content script architecture: MAIN world for interception, ISOLATED world for Chrome API access.

Event Ordering Matters More Than You'd Think

An early version of the interceptor emitted TOOL_REGISTERED before calling the real registerTool(). This caused a subtle bug: if the browser rejected the registration (e.g., duplicate tool name), the panel would show a tool that didn't actually exist. Emitting events only after the real call succeeds was a simple fix with a big impact.

Similarly, provideContext() in the WebMCP spec replaces the entire tool set atomically. The extension emits a CONTEXT_CLEARED event followed by individual TOOL_REGISTERED events to match the spec's semantics.

Badge Accuracy Requires Derived State

A naive badge implementation counted TOOL_REGISTERED events. But tools can be unregistered, context can be cleared, and pages can reload. The correct approach is to derive the current tool set from the full event history — replaying registrations, unregistrations, and clears to arrive at the current state.

Streaming Needs Accumulation, Not Replacement

The promptStreaming() wrapper reads chunks from the original ReadableStream and re-emits them through a new stream. An early bug used fullText = value instead of fullText += value, which meant the STREAM_END event only contained the last chunk instead of the full accumulated text.


How to Use It

Prerequisites

  • Chrome 146+ (currently Canary) with the following flags enabled:
    • chrome://flags/#webmcp-testing — enables navigator.modelContext
    • chrome://flags/#optimization-guide-on-device-model — enables window.ai (Gemini Nano)

Install

git clone https://github.com/tech-sumit/webmcp-debugger-chrome-extension.git
cd webmcp-debugger-chrome-extension
pnpm install && pnpm build

Then in Chrome:

  1. Open chrome://extensions/
  2. Enable Developer mode
  3. Click Load unpacked and select the dist/ directory

Use

  1. Navigate to any page that uses window.ai or navigator.modelContext
  2. Open Chrome DevTools (F12)
  3. Find the WebMCP Debugger panel tab
  4. Watch events flow in as the page interacts with AI APIs

You can also use the extension's badge — it shows the number of currently registered WebMCP tools for the active tab.


Tech Stack

Layer Technology
Panel UI React 19, Tailwind CSS 3.4
Build Vite 6 (four separate configs for panel, bridge, interceptor, background)
Language TypeScript 5.6
Extension Manifest V3
Testing Vitest
Linting ESLint 9 + TypeScript parser
Package manager pnpm

The build system uses four Vite configs because Chrome extensions need different output formats: the panel is a standard SPA, the content scripts are IIFEs, and the background is a service worker module. A single pnpm build runs all four sequentially and copies the manifest.


What's Next

WebMCP is still behind a flag in Chrome Canary, but the W3C specification is actively developing. As more sites adopt WebMCP tools and the Prompt API matures, developer tooling becomes critical. A few directions I'm exploring:

  • Request/response matching — linking PROMPT_SENT to its PROMPT_RESPONSE (or error) in the timeline for clearer debugging
  • Tool execution playground — a standalone mode for testing WebMCP tools without needing an AI agent
  • Export and replay — save event traces and replay them for testing or sharing bug reports
  • Performance overlay — visualize prompt latency and streaming throughput

If you're building with WebMCP or window.ai, give the extension a try and open an issue if you hit a rough edge. PRs are welcome.

GitHub: tech-sumit/webmcp-debugger-chrome-extension

SA
Written by Sumit Agrawal

Software Engineer & Technical Writer specializing in full-stack development, cloud architecture, and AI integration.

Related Posts