Build1 distinct publisher3 min readPublished
A modelContext that registers tools correctly can still refuse addEventListener, and the throw lands after the page has painted. The repair is a bounded poll plus a banner that names the mechanism actually carrying the app.
The Engineer · Build desk

Compiled by The EngineerSomething wrong?How this is made
Registering a tool and subscribing to tool changes arrive on the same object, so they read as one capability. They are not. `addEventListener` comes from `EventTarget`, and whether a client's `modelContext` inherits from `EventTarget` is an implementation decision separate from whether it implements `registerTool` and `getTools` [4]. Nothing in the shape of the returned object records which choices were made. The line most people will write, `!!(modelContext && 'registerTool' in modelContext)`, tests one property and generalises past it not at all [3].
The cost of getting that wrong is set by where the throw lands. In the write-up's app, `boot()` awaited `start()`, so the TypeError arrived after the page had painted: a fully rendered page that then announced it had failed to start [5]. That is the one bug shape a screenshot cannot explain.
The timing lesson is harder to design around. WebMCP has a declarative form, where `toolname` on a `<form>` makes the browser adopt the form as a tool, and removing the attribute removes the tool [7]. Adoption happens when the browser notices the DOM change, and there is no promise to await [8]. Where `toolchange` fires, the event tells you the surface has settled and you re-render [8]. Where it does not, the render triggered by a state change reads a surface that is still moving. In this app one state change drops eight tools at once, seven by aborting an `AbortSignal` and one by dropping an attribute [7][17], so the window is wide enough to be visible on screen.
Twelve ticks at 50 ms is a 600 ms worst case before the poll gives up, which matches the roughly 600 ms bound the author quotes [11][12]. A tick that finds a count different from what was rendered re-renders instead of sleeping, and that render re-arms the poll [11].
The verification is the part worth reproducing before trusting it. According to the post, the local build was served under the live origin so the origin-trial token still applied, with `addEventListener` stripped off `modelContext` to imitate the client's shape, and both shapes returned 5 / 17 / 15 / 9 tools at four checkpoints with zero divergences and zero page errors [13]. That result transfers only if the missing `EventTarget` is the only relevant difference between the real client and the local mock. If ChatGPT's browser also adopts declarative tools on a slower schedule than Chrome does, a 600 ms ceiling tuned against a stripped local object is a guess about someone else's scheduler.
There is one more default to read before shipping. Passing `executeTool` the object the IDL specifies returns `UnknownError: Failed to parse input arguments` on Chrome 151 [14]. The author's rule is to send the string and keep the object path as a fallback, which is correct today and on the day Chrome converges [16].
The product's claim was that the panel cannot disagree with the API, because the left column is rendered from `getTools()` [10]. In one client they disagreed [10], and the response was to scope the claim in the UI rather than delete it: the banner now states that tools registered, that this client's `modelContext` is not an `EventTarget`, and that there are therefore no `toolchange` events [6]. Naming the mechanism that is carrying you is cheap, and it is the only part of this that survives the next client.
Ranked by verification strength, evidence, and original report placement.
A developer spent eight days building a parliamentary-procedure engine on WebMCP, the emerging standard that lets a web page hand an AI agent a typed tool list through document.modelContext.
The app's design rule is that an action which is out of order should be absent from getTools() entirely rather than greyed out or refused at runtime, with the rule that removed it printed beside the gap.
The obvious feature detection, hasWebMCP = !!(modelContext && 'registerTool' in modelContext), checks that the property exists; it does not check that calling it works and says nothing about the other capabilities hanging off the same interface.
ChatGPT's in-app browser hands back a modelContext that registers tools correctly and answers getTools() correctly, and is not an EventTarget, so addEventListener('toolchange', ...) throws a TypeError; subscribing is a separate capability from registering and the object's shape gives no warning.
In the app, boot() awaited start(), so the throw landed after the page had painted: the page rendered completely and then announced it had failed to start.
The fix wraps addEventListener in try/catch and sets a toolEventsLive flag, and the banner now reads that tools registered but this client's modelContext is not an EventTarget, so there are no toolchange events.
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 30, 2026
Follow any of these and your For You feed starts watching them — no settings page required.
build
Shopify flipped agent access on for you. The only decision left is what you log.1 distinct publisher
build
In Manifest V3 every message handler is a cold start, and the fix for lost state hides new failures1 distinct publisher
build
The exit ping that never left: fetch in beforeunload loses to the browser's unload policy1 distinct publisher
build
The stroke width that never rendered: SVG attributes lose every cascade fight1 distinct publisher
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
One builder's logbook, but he left the receipts out
Everything here comes from the person it happened to, and he is describing software he wrote — yet the account is unusually checkable for a first-person post. The detection snippet, the guard, and the poll are quoted in full; the repair is tested by stripping addEventListener off modelContext under the live origin so the origin-trial token still applies, giving matched 5 / 17 / 15 / 9 counts across both shapes; the spec dispute is pinned to issue #243 with a close date. What no one has verified is the pair of facts that matter most to other developers: that ChatGPT's in-app browser really omits EventTarget, and that Chrome 151 really rejects the IDL-specified object.
One app, one origin trial, two half-implementations
Usage evidence is thin and honest about it: a single demo behind an origin-trial token, an MIT repo, and no user numbers anywhere. The genuine adoption signal is on the implementer side rather than the app side — Chrome ships executeTool with an argument shape its own IDL contradicts, and ChatGPT's in-app browser exposes a modelContext missing event support. Two vendors partway into a standard, and one developer's engine sitting on top of them.
He argues against his own pitch, twice
Two moves push this below the line rather than above it. The post volunteers that the app's central promise — the panel cannot lie, because the left column comes from getTools() — broke in a real client, showing 16 against the API's 17 and staying wrong four seconds later. Then it reports that six of ten HOA board members said the problem wasn't real, that the rulebook itself relaxes procedure for the small boards he had targeted, and that his opening line was false and already shipped. Publishing your own disconfirmation is the opposite of overselling.
A portfolio post with the failures left in
The structural pull is obvious: this is a developer publishing his own demo link and repo on a platform where build logs earn attention, and a rising standard's momentum flatters anyone visibly early on it. What blunts the pull is what he chose to include — a divergence that embarrasses the product's core claim, a Chrome bug that makes his stack look unfinished, and interviews suggesting the target market mostly doesn't want the thing. No promotional incentive produces that last paragraph.
Firm on mechanism, unproven on breadth
We would defend the mechanics without hesitation: the capability-by-capability probe, the after-paint throw, the bounded poll, and the string-versus-object mismatch all hang together and are shown in code. Confidence drops on scope. One developer, one app, one agent client, one Chrome build, no second observer — so how general the ChatGPT browser finding is, and how long Chrome's argument handling stays out of step, are open questions this reporting cannot close.