deniz.in

Markets

Weather

Loading weather

· via dev.to (home feed)

Feature detection lies: hands-on lessons from shipping on WebMCP

A dev.to post describes two surprises from running a WebMCP app in a client the author did not control: presence checks that lie, and declarative tools that settle late.

Feature detection lies: hands-on lessons from shipping on WebMCP

What the experiment was

According to a post on dev.to, a developer spent eight days building a parliamentary-procedure engine on top of WebMCP, the emerging standard that lets a web page hand an AI agent a typed list of tools through document.modelContext. The design principle was unusual: an action that is out of order should not merely be greyed out or refused at runtime. It should be missing from getTools() altogether, with the rule that removed it displayed next to the empty spot. The interesting failures, the author writes, only appeared once the finished app ran inside a client they did not control — ChatGPT's in-app browser.

Presence is not capability

The first lesson concerns feature detection. The obvious check — look for registerTool on the modelContext object — confirms only that the property exists. It says nothing about whether calling it works, and nothing about the other capabilities hanging off the same interface.

In ChatGPT's in-app browser, the returned modelContext registers tools and answers getTools() correctly, but is not an EventTarget. Calling addEventListener for the toolchange event throws a TypeError. Subscribing to change events turns out to be a capability separate from registering tools, and nothing about the object's shape warns you.

The failure mode was ugly: the exception landed after the page had painted, so the app rendered completely and then announced it had failed to start — it looked alive, then called itself broken. The fix was to treat each capability as optional in its own right, wrapped in its own try/catch, and then state the result in the interface. The app's status banner now reports that tools are registered but that this client's modelContext is not an EventTarget, so there are no toolchange events. The author's argument is that telling users which mechanism is carrying the app beats silent degradation.

Declarative tools settle asynchronously

WebMCP also has a declarative form: add a toolname attribute to a form element and the browser adopts it as a tool; removing the attribute removes the tool. The author's app used both mechanisms deliberately, so a single state change could drop seven tools by aborting an AbortSignal and an eighth by deleting an attribute.

But adoption happens when the browser notices the DOM change, and there is no promise to await. Where toolchange fires, this is invisible — the event tells you the tool surface has settled and you re-render. Without the event, the render following a state change read getTools() one tick early: the panel said 16 tools, the API said 17, and the discrepancy was still on screen four seconds later. That is exactly the divergence the product claimed was impossible, since the panel is drawn from getTools() rather than internal bookkeeping.

With nothing to await, the workaround is polling: re-check getTools() every 50 milliseconds until the count stops moving, capped at twelve tries (about 600 milliseconds) and used only when the event is unavailable. To verify the fix, the author served a local build under the live origin — so the origin-trial token still applied — with addEventListener stripped from modelContext to reproduce the client's shape. Both shapes then returned the same tool counts at four checkpoints, with zero divergences and zero page errors.

A bug already settled upstream

A third issue sits on Chrome's side. On Chrome 151, executeTool's second argument must be a JSON string; passing the object the IDL specifies returns an error about failing to parse input arguments. This was resolved upstream in the WebMCP repository, with issue #243 closed as completed on 2026-08-17, but Chrome has not shipped the resolution. The pragmatic advice from the post: send the string, keep the object path as a fallback, and you are correct both today and when Chrome converges.

Numbers and a product lesson

The post also reports measurements from the deployed app: 90 comparisons between getTools() and what the screen showed, with zero divergences; getTools() round trips at p50 0.20 and p95 0.30 seconds; and 306 unit tests, including all 152 legality cells (7 phases by 19 gated tools), asserted against the rule table rather than against the implementation.

The non-technical lesson cost more. The author asked ten HOA board members whether the problem was real. Six said no — they deliberately avoid strict Robert's Rules, and the rulebook itself relaxes procedure for boards under roughly twelve members, which covers most HOA boards. The genuine user turned out to be one secretary at an association that manages millions, who records every motion and each member's vote by name. A narrower audience, backed by evidence the author did not have before shipping.

Why it matters

WebMCP is still an emerging standard, and this is a useful field report on what building for the web platform means when the consumer of your page is an AI agent inside a browser you do not control. Both technical lessons generalise beyond WebMCP: a presence check does not prove a capability works, and capabilities that appear to come from one interface — registering tools versus subscribing to changes — can fail independently of each other. Anyone building on modelContext today needs per-capability detection, honest degradation messages, and, until declarative adoption is awaitable or event support is universal, a bounded reconciliation loop. The benchmarks also show that the standard's core promise — the screen cannot disagree with the API — only holds if you actively defend it.

  • #webmcp
  • #feature-detection
  • #ai-agents
  • #web-platform
  • #chrome

Related posts