---
title: "I Built a DevTools Extension to Debug WebMCP and window.ai"
date: 2026-02-19T12:00:00.000Z
description: "WebMCP Debugger is a Chrome DevTools extension that intercepts and visualizes every AI interaction on a web page — tool registrations, prompt sessions, streaming responses, and tool executions."
tags: ["webmcp", "chrome-extension", "devtools", "window-ai", "model-context-protocol", "prompt-api", "developer-tools", "open-source", "typescript", "debugging"]
tokens: 2452
content-signal: search=yes, ai-input=yes, ai-train=no
---


![WebMCP Debugger Chrome DevTools extension intercepting AI events from a web page into a structured timeline](/images/posts/webmcp-debugger-chrome-extension/hero.png)

## 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](https://webmachinelearning.github.io/webmcp/) 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](/images/posts/webmcp-debugger-chrome-extension/panel-mockup.png)

### 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](/images/posts/webmcp-debugger-chrome-extension/architecture.png)

```
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](/images/posts/webmcp-debugger-chrome-extension/interception-pattern.png)

**LanguageModel (window.ai)**

```typescript
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.

```typescript
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

```typescript
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.

```typescript
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

```bash
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](https://webmachinelearning.github.io/webmcp/) 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](https://github.com/tech-sumit/webmcp-debugger-chrome-extension/issues) if you hit a rough edge. PRs are welcome.

**GitHub**: [tech-sumit/webmcp-debugger-chrome-extension](https://github.com/tech-sumit/webmcp-debugger-chrome-extension)
