fix(webmcp): survive a host whose modelContext is not an EventTarget - #9
Merged
Merged
Conversation
Traces was entirely down in ChatGPT Desktop's in-app browser: "Application
error: a client-side exception has occurred", from
Uncaught TypeError: e.addEventListener is not a function
The spec declares `ModelContext : EventTarget`, so `types/webmcp.d.ts` typed
the event methods as present and two components subscribed to `toolchange`
directly. ChatGPT Desktop exposes a `document.modelContext` without them, the
call threw out of a client effect, React escalated it, and the production
build served its global error page — in the one browser with native WebMCP,
which is the browser the challenge rules point judges at. Chrome never saw it
because our polyfill is a real EventTarget, and the failure is unreachable in
polyfill mode.
Subscription now goes through `lib/webmcp/tool-change.ts`, which guards on
`typeof`, wraps the call, and returns a no-op unsubscribe when there was
nothing to subscribe to. Absence is a console warning, printed once, and
silent in the UI: both callers already render a real tool list without the
event, so all that is lost is live updates when the surface changes shape
mid-session.
The type keeps `extends EventTarget` — it is what the spec says, and the
polyfill genuinely is one — with a note directing readers to the helper
rather than the raw methods.
Four tests cover the eventless host, the spec-shaped host, an
`addEventListener` that throws, and warn-once. 293 pass; `tsc --noEmit` clean.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
Traces was completely down in ChatGPT Desktop's in-app browser — not degraded, replaced:
That is the browser the challenge rules name first for judging: "Judges may test WebMCP tools using ChatGPT's in-app browser or Google Chrome with WebMCP enabled."
Root cause
The spec declares
interface ModelContext : EventTarget, sosrc/types/webmcp.d.tstypedaddEventListeneras present and two components subscribed totoolchangewithout a guard:src/components/ui/tool-status-banner.tsx— mounts first, so this is the one that actually threwsrc/components/ui/webmcp-badge.tsx— never reached, but the same faultChatGPT Desktop's
document.modelContexthas no event methods. Thrown from a client effect, React escalates to the nearest error boundary; in a production build that is Next.js's global one, so a missing optional feature took the whole page.Why nothing caught it:
lib/webmcp/polyfill.tsbuilds its shim asObject.assign(new EventTarget(), …), so the call is always valid in polyfill mode. The failure is only reachable on a native host — and only on a native host that does not honour that part of the spec.The fix
One helper,
src/lib/webmcp/tool-change.ts, and both call sites go through it. It guards ontypeof, wraps the subscribe, and returns a no-op unsubscribe when there was nothing to subscribe to — guarded and wrapped, because the lesson is not "this one method was missing", it is that a draft API on a host we do not control must never be able to take the page down.Absence is a
console.warn, once per page load, and silent in the UI. Both callers already render a real tool list fromgetTools()or from the registration result; all that is lost is live updates when the surface changes shape mid-session.The type keeps
extends EventTarget. It is what the spec says, the polyfill genuinely is one, and weakening it toPartial<EventTarget>would breakshim.dispatchEventinside our own polyfill. A docstring above the interface points readers at the helper instead.Tests
Four new cases in
tool-change.test.ts:EventTargethostaddEventListenerthat throwsA version of
onToolChangethat propagates the original TypeError passes none of them.Verification
Still to confirm after deploy: whether
registerToolandgetToolswork on that host. The banner throwing first means we never got to read what it said, so the answer is still unknown — this PR is what makes it readable.Scope
Five files, all inside the WebMCP surface.
tailwind.config.ts,globals.css,src/types/domain.tsandnext.config.mjsuntouched.