Feature detection lies: two things I learned shipping on WebMCP A developer building a parliamentary-procedure engine on WebMCP, the emerging standard for web pages to expose typed tools to AI agents, discovered that feature detection for the API is unreliable. The developer found that checking for the presence of 'registerTool' does not guarantee the API works, and that some clients, such as ChatGPT's in-app browser, do not support event subscriptions, causing runtime errors. The developer implemented a fallback polling mechanism to ensure the UI stays in sync with the tool list, highlighting the need for robust capability detection in WebMCP clients. I 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 idea is one sentence: an action that is out of order should not exist to be called. Not greyed out, not refused at runtime — absent from getTools entirely, with the rule that removed it printed beside the gap. Live: https://pointoforder.netlify.app https://pointoforder.netlify.app · Source MIT : https://github.com/edycutjong/mace https://github.com/edycutjong/mace Two things I learned are worth more than the product, and both came from running it in a client I did not control. 'registerTool' in modelContext does not tell you the API works The obvious feature detection: js export const modelContext = globalThis.document?.modelContext ?? globalThis.navigator?.modelContext ?? null; export const hasWebMCP = modelContext && 'registerTool' in modelContext ; That checks the property exists. It does not check that calling it works, and it 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 . addEventListener 'toolchange', … throws a TypeError .Subscribing is a separate capability from registering, and nothing in the shape of the object warns you. The failure mode was the worst one available. boot awaited start , the throw landed after the page had painted, and the page rendered completely and then announced it had failed to start. It looked alive, then called itself broken. The fix is to treat every capability as independently optional: js let toolEventsLive = false; if hasWebMCP && regErr { try { modelContext.addEventListener 'toolchange', = { renderAll ; } ; toolEventsLive = true; } catch err { console.error ' mace modelContext is not an EventTarget:', err ; } } …and then say so in the UI. The banner now reads "WebMCP live · document.modelContext — tools registered, but this client's modelContext is not an EventTarget, so there are no toolchange events." A product that names which mechanism is carrying it beats one that quietly degrades. toolchange is how you find out WebMCP has a declarative form: put toolname on a