↑ ↓ navigate↵ selectesc close
✏️ Blog PostAI & MCP Agents

WebMCP Explained: Exposing Browser-Native AI Tools with document.modelContext and Declarative HTML

Alex Patterson and AJ the Cat showcasing WebMCP browser tools with document.modelContext and registerTool
✏️ Visual Architecture Diagram

how this post connects together.

Follow AJ from first principles to production-ready architecture.

✏️ Visual Diagram: WebMCP Explained: Exposing Browser-Native AI Too…
✓ Good PatternVISUAL DIAGRAM
Visual Diagram: WebMCP Explained: Exposing Browser-Native AI Too…Visual architectural diagram showing: Step 1: WebMCP Explained (Mental model & why it matters) -> Step 2: Hands-On Implementation (Under the hood & live code) -> Step 3: Ship It Purrfectly (✓ Production trade-offs). Step-by-step diagram breakdown for WebMCP Explained: Exposing Browser-Native AI Tools with document.modelContext and Declarative HTML: start with WebMCP Explained, implement Hands-On Implementation, and ship to production.✓Step 01WebMCP ExplainedMental model & why it matters✓Step 02Hands-On ImplementationUnder the hood & live code✓Step 03Ship It Purrfectly✓ Production trade-offsunroll the yarn✓ production ready
[Diagram Walkthrough]Step-by-step diagram breakdown for WebMCP Explained: Exposing Browser-Native AI Tools with document.modelContext and Declarative HTML: start with WebMCP Explained, implement Hands-On Implementation, and ship to production.
AAA Contrast Verified

If you have watched a browser AI agent try to book a flight, filter an e-commerce catalog, or search documentation using visual screen-scraping, you already know how painful it is. The agent takes a high-resolution screenshot or dumps 15,000 tokens of raw accessibility tree nodes, guesses a CSS selector, clicks the wrong button when a modal animates in, and burns through your token budget in four seconds. WebMCP (Web Model Context Protocol) fixes this at the web platform layer.

Co-developed within the W3C Web Machine Learning Community Group by engineers from Google and Microsoft, WebMCP brings Model Context Protocol primitives directly into the browser tab. Instead of forcing agents to reverse-engineer your DOM, your web application explicitly registers structured, JSON-Schema-typed tools via document.modelContext (or navigator.modelContext) and declarative HTML <form toolname="..."> attributes. Even better: we already run WebMCP in production across CodingCat.dev and alexpatterson.dev.

The Mental Model: Screen-Scraping vs. WebMCP Native Tool Contracts

When you build a traditional backend MCP server over stdio or HTTP SSE, you immediately hit the authentication wall: the remote MCP server does not have the user's active browser cookies, CSRF tokens, or in-memory React/Astro state. WebMCP executes inside the active browser tab, meaning registered tools automatically inherit the user's same-origin session while exposing a clean, deterministic JSON Schema contract to the browser assistant.

Dark-mode Architecture Diagram comparing brittle 15,000-token DOM screen-scraping against 150-token WebMCP native browser tools
Architecture Diagram: Legacy DOM Screen-Scraping (left, Coral anti-pattern) vs. WebMCP Imperative and Declarative Browser Tools (right, Mint recommended pattern).

Comparing DOM Actuation, Backend MCP, and Browser WebMCP

DimensionDOM / Screenshot ScrapingRemote Backend MCP ServerBrowser-Native WebMCP (Recommended)
Token Overhead per Turn10,000 to 25,000 tokens (DOM + Vision)150 to 400 tokens (JSON Schema)100 to 250 tokens (In-Tab JSON Schema)
Authentication & Session StateShares tab cookies but clicks blindlyRequires separate OAuth 2.1 / API token flowInherits same-origin cookies and live UI state automatically
Resilience to UI RedesignsBreaks whenever CSS classes or layouts shiftDecoupled from UI100% decoupled from visual CSS changes
Implementation SurfaceZero publisher controlDedicated Node / Cloudflare Worker endpointdocument.modelContext.registerTool() or <form toolname>

Pathway 1: The Imperative WebMCP API (registerTool & AbortController)

Early Chrome Canary prototypes exposed WebMCP under navigator.modelContext, while newer 2026 specification updates bind the context directly to the document lifecycle via document.modelContext. In production, always feature-detect both and pass an AbortController signal so Single-Page Application (SPA) or Astro View Transition navigations cleanly unregister tools when a component unmounts. Here is the exact pattern we ship in CodingCat.dev's BaseLayout.astro:

// src/lib/webmcp/register-codingcat-tools.ts
export interface WebMcpToolDefinition<TArgs = Record<string, unknown>> {
  name: string;
  description: string;
  annotations?: {
    readOnlyHint?: boolean;
    destructiveHint?: boolean;
    idempotentHint?: boolean;
    untrustedContentHint?: boolean;
  };
  inputSchema: {
    type: "object";
    properties: Record<string, unknown>;
    required?: string[];
  };
  execute: (args: TArgs) => Promise<unknown>;
}

interface ModelContextContainer {
  registerTool?: <T>(tool: WebMcpToolDefinition<T>, options?: { signal?: AbortSignal }) => void;
  unregisterTool?: (name: string) => void;
}

export function getBrowserModelContext(): ModelContextContainer | null {
  if (typeof document !== "undefined" && "modelContext" in document) {
    return (document as unknown as { modelContext: ModelContextContainer }).modelContext;
  }
  if (typeof navigator !== "undefined" && "modelContext" in navigator) {
    return (navigator as unknown as { modelContext: ModelContextContainer }).modelContext;
  }
  return null;
}

export function mountCodingCatWebMcpTools(): () => void {
  const mc = getBrowserModelContext();
  const controller = new AbortController();

  if (!mc || typeof mc.registerTool !== "function") {
    return () => controller.abort();
  }

  mc.registerTool<{ query: string; pillar?: "course" | "podcast" | "post" }>(
    {
      name: "search_content",
      description:
        "Search CodingCat.dev Courses, Podcasts, and Blog Posts by technical topic or framework.",
      annotations: {
        readOnlyHint: true,
        idempotentHint: true,
        untrustedContentHint: true,
      },
      inputSchema: {
        type: "object",
        properties: {
          query: {
            type: "string",
            description: "Search keyword (e.g. WebMCP, Gemini Nano, Astro, Sanity, Cloudflare D1)",
          },
          pillar: {
            type: "string",
            enum: ["course", "podcast", "post"],
            description: "Optional CodingCat.dev content pillar filter",
          },
        },
        required: ["query"],
      },
      execute: async ({ query, pillar }) => {
        const params = new URLSearchParams({ q: query });
        if (pillar) params.set("type", pillar);
        const response = await fetch(`/api/search?${params.toString()}`, {
          headers: { Accept: "application/json" },
          credentials: "same-origin",
        });
        if (!response.ok) {
          throw new Error(`Search API returned HTTP ${response.status}`);
        }
        return await response.json();
      },
    },
    { signal: controller.signal }
  );

  return () => controller.abort();
}

Pathway 2: The Declarative WebMCP API (HTML Form Annotations)

Not every page needs custom JavaScript to expose agent tools. If your application already uses semantic HTML <form> elements for filtering, search, or booking, you can add WebMCP declarative attributes (toolname, tooldescription, toolparamdescription, and toolautosubmit). The browser automatically parses your form inputs (type, pattern, required, and <select> options) into a structured JSON Schema for the agent:

<!-- Declarative WebMCP Tool Form (as shipped on alexpatterson.dev & codingcat.dev/demos/web-ai) -->
<form
  action="/speaker-kit.json"
  method="get"
  toolname="get_speaker_kit_json"
  tooldescription="Fetch Alex Patterson's programmatic speaker kit JSON including bios, talk abstracts, Sanity CDN headshots, and speaking ledger"
  toolautosubmit="true"
>
  <label for="speaker-kit-section">Speaker Kit Section</label>
  <select
    id="speaker-kit-section"
    name="section"
    required
    toolparamdescription="Speaker kit section to retrieve (all, bios, talks, headshots, ledger)"
  >
    <option value="all">Full Speaker Kit (JSON)</option>
    <option value="bios">Bios (6 Variants)</option>
    <option value="talks">Talk Abstracts</option>
    <option value="headshots">Press Headshots</option>
    <option value="ledger">Speaking Track Record</option>
  </select>
  <button type="submit">Download JSON</button>
</form>

AJ Pro-Tip: Security Annotations and Prompt Injection Guardrails

AJ Pro-Tip: Always Set readOnlyHint and untrustedContentHint Accurately. Because WebMCP tools execute with the user's active session cookies, a malicious third-party comment on a page could attempt indirect prompt injection to trick a browser agent into invoking a state-mutating tool. Always set readOnlyHint: true on safe query tools so the browser agent knows it can run them without confirmation, and omit readOnlyHint (or set destructiveHint: true) on checkout, deletion, or credential-modifying actions so Chrome prompts the user for explicit human-in-the-loop approval.

Try the Live Browser AI & WebMCP Playground

You can test every pattern from this article live right now in our interactive Web AI & WebMCP Playground on CodingCat.dev, or inspect our production discovery manifests on the Developers & Agent APIs page. If you are organizing a developer conference or engineering summit, check out my Web AI talk abstracts on alexpatterson.dev and explore more hands-on Courses, Podcasts, and Blog Posts right here on CodingCat.dev.