“WebMCP is currently focused on the imperative
document.modelContext.registerTool()API described above, which is what the specification defines.”— WebMCP README, November 20261
If you wrote WebMCP code in the first half of 2026, or you’re reading a tutorial from back then, it probably starts with navigator.modelContext.provideContext({ tools: [...] }). None of that runs in a current browser. This appendix shows what changed, when, and how to port the code, using the same Fjord Table tools in both shapes. Chapter 5 tells the story behind the two big changes. This appendix is the practical checklist, and since this edition it also covers the shorter step from the book’s first edition (October 2026) to today, plus the proposals that could make you port again.
The dates come from the commit history of the specification repository, webmachinelearning/webmcp, read on 14 October 2026 and re-checked on 15 October 2026. I checked the merge times of the key pull requests against GitHub’s own records.2 The specification has no changelog section of its own, so the commit log is the record. The “after” code was tested in Chromium 156.0.8078.4 with --enable-features=WebMCPTesting. The listings and their tests are in the book’s demos repository, in appendices/d-migration/, and I re-ran all 20 checks on 2 November 2026: they still pass.
D.1 What changed, and when
Table D.1 — API-shape changes in the WebMCP specification, February to October 2026
| Date (UTC) | Change | Pull request / commit |
|---|---|---|
| 5 March 2026 | provideContext() and clearContext() removed |
#132, fe84c673 |
| 26 March 2026 | registerTool() takes an optional AbortSignal, and unregisterTool(name) is removed |
#147, 6708e334 |
| 19 May 2026 | ModelContext scoped to the Document instead of the Navigator |
#177, 986f4615 |
| 27 May 2026 | The modelContext getter moved from Navigator to Document |
#184, c7b5c706 |
| 8 June 2026 | registerTool() returns a promise |
#200, ec37ec47 |
| 11 June 2026 | ModelContextClient (the execute callback’s second argument, with requestUserInteraction()) removed |
#205, 067a1c98 |
| 19 August 2026 | The execute callback receives { signal } for cancellation |
#247, 83e893a9 |
| 8 October 2026 | Declarative API removed from the specification | #338, 14ae81310 |
| 8 October 2026 | UnknownError replaced by specific exception names in executeTool() |
#339, b206dae11 |
Two of these rows break old code outright: 5 March and 27 May. The others change details you’ll meet while porting. The two October rows don’t affect migration from navigator, but they explain what you see in Chrome today (Appendix A, Tables A.7 and A.8). And the table stops in October for a reason. The draft of 27 October 2026 has exactly the same IDL as the 15 October one, so nothing after 8 October changes the API shape (Section D.6).

D.2 Before: the early-2026 shape
Listing D.1 is Fjord Table’s two tools written the way the explainer and early tutorials wrote them, close to the example in the explainer of November 2025.12 It uses everything that has gone since: the navigator namespace, provideContext() to set the whole tool list in one call, clearContext() to drop it, and a second execute argument (a ModelContextClient) whose requestUserInteraction() asked the browser to let the page talk to the user mid-call.
appendices/d-migration/before/fjord-tools.before.js — before: Fjord Table’s tools with provideContext()// BEFORE (navigator.modelContext, early 2026). Does not run in current browsers.// provideContext() replaced the page's whole tool set; clearContext() removed it.if ('modelContext' in navigator) { navigator.modelContext.provideContext({ tools: [ { name: 'search_availability', description: 'Find open tables at Fjord Table for a date, time and party size.', inputSchema: { type: 'object', properties: { date: { type: 'string', description: 'Visit date, YYYY-MM-DD' }, time: { type: 'string', description: 'Preferred time, 24h HH:MM' }, partySize: { type: 'integer', minimum: 1, maximum: 12 } }, required: ['date', 'partySize'] }, async execute({ date, time, partySize }) { const slots = await availability.search({ date, time, partySize }); return { date, partySize, slots: slots.map((s) => s.time) }; } }, { name: 'book_table', description: 'Book a table at Fjord Table. Asks the user to confirm first.', inputSchema: { type: 'object', properties: { date: { type: 'string' }, time: { type: 'string' }, partySize: { type: 'integer', minimum: 1, maximum: 12 }, name: { type: 'string' } }, required: ['date', 'time', 'partySize', 'name'] }, // The second argument was a ModelContextClient. async execute(booking, client) { const ok = await client.requestUserInteraction(() => confirmBooking(booking)); if (!ok) return { booked: false, reason: 'The user declined.' }; return { booked: true, reference: await bookings.create(booking) }; } } ] }); // Leaving the booking view: drop every tool on the page. window.addEventListener('pagehide', () => navigator.modelContext.clearContext());}Listing D.2 is the other early pattern: adding and removing single tools with registerTool() and unregisterTool(name). registerTool() returned nothing then.47
appendices/d-migration/before/register-unregister.before.js — before: registering and unregistering by name// BEFORE (navigator.modelContext, February 2026). Does not run in current browsers.// registerTool() returned nothing, and tools were removed by name.navigator.modelContext.registerTool({ name: 'search_availability', description: 'Find open tables at Fjord Table for a date, time and party size.', inputSchema: { type: 'object', properties: { date: { type: 'string' } }, required: ['date'] }, async execute({ date }) { return { date, slots: (await availability.search({ date })).map((s) => s.time) }; }});// Any script on the page could do this, including to tools it didn't register.navigator.modelContext.unregisterTool('search_availability');In Chromium 156 neither listing works (tested). 'modelContext' in navigator is false, so Listing D.1’s guard quietly skips everything: no error, and no tools. Listing D.2 has no guard and throws a TypeError on its first line. Guess which failure is the dangerous one? The quiet one. The page keeps working for people, and agents simply find nothing.
D.3 After: the current shape
Listing D.3 is the same two tools for the current draft, dated 27 October 2026.13 It is close to Chapter 5’s Listing 5.1, wrapped in a function so the test can call it, and its results follow Chapter 11’s convention: a booking returns status: 'booked', and a guest who says no in the page’s dialog gives status: 'cancelled_by_user'.
appendices/d-migration/after/fjord-tools.js — after: Fjord Table’s tools on document.modelContext// AFTER (document.modelContext, 27 October 2026 draft; Chromium 156).// One registerTool() call per tool, one AbortController per view.export async function registerFjordTools({ availability, bookings, confirmBooking }) { if (!('modelContext' in document)) return null; // progressive enhancement const view = new AbortController(); const options = { signal: view.signal }; await document.modelContext.registerTool({ name: 'search_availability', description: 'Find open tables at Fjord Table for a date, time and party size.', inputSchema: { type: 'object', properties: { date: { type: 'string', description: 'Visit date, YYYY-MM-DD' }, time: { type: 'string', description: 'Preferred time, 24h HH:MM' }, partySize: { type: 'integer', minimum: 1, maximum: 12 } }, required: ['date', 'partySize'] }, annotations: { readOnlyHint: true }, async execute({ date, time, partySize }, { signal }) { const slots = await availability.search({ date, time, partySize, signal }); return { date, partySize, slots: slots.map((s) => s.time) }; } }, options); await document.modelContext.registerTool({ name: 'book_table', description: 'Book a table at Fjord Table. The user confirms in the page first.', inputSchema: { type: 'object', properties: { date: { type: 'string' }, time: { type: 'string' }, partySize: { type: 'integer', minimum: 1, maximum: 12 }, name: { type: 'string' } }, required: ['date', 'time', 'partySize', 'name'] }, annotations: { consequentialHint: true }, // The second argument is now { signal }. Confirmation is the page's own UI. async execute(booking, { signal }) { const ok = await confirmBooking(booking, { signal }); if (!ok) return { status: 'cancelled_by_user', message: 'The guest declined in the booking dialog.' }; return { status: 'booked', reference: await bookings.create(booking) }; } }, options); // Leaving the booking view: abort() unregisters exactly these two tools. window.addEventListener('pagehide', () => view.abort(), { once: true }); return view;}Table D.2 lists the changes one by one.
Table D.2 — Porting map: early-2026 shape to the 27 October 2026 draft
| Before | After | Notes |
|---|---|---|
'modelContext' in navigator |
'modelContext' in document |
The getter lives on Document and exists only in secure contexts.13 |
navigator.modelContext.provideContext({ tools }) |
One document.modelContext.registerTool(tool, { signal }) per tool |
Duplicate names reject with InvalidStateError instead of silently replacing a tool. |
navigator.modelContext.clearContext() |
controller.abort() on the signal(s) you registered with |
You can only remove tools you registered yourself. |
navigator.modelContext.unregisterTool(name) |
controller.abort() |
Unregistering doesn’t cancel calls already running (Chapter 6). |
registerTool(tool) returns undefined |
await registerTool(tool, options) |
Errors arrive as a rejected promise, not a throw. |
execute(input, client) |
execute(input, { signal }) |
The second argument is now options with an AbortSignal. |
client.requestUserInteraction(cb) |
The page’s own confirmation UI, plus annotations: { consequentialHint: true } |
See the callout below. |
| Results: any value | Return an object | The browser serializes it to a JSON string. Chromium 156 passes plain strings through unquoted, which the spec’s editors call a Chrome bug (Appendix A, Table A.5, and Section D.6). Use Chapter 11’s statuses, such as cancelled_by_user when the person declines. |
executeTool(tool, '{"date":"…"}') (string input) |
executeTool(tool, { date: '…' }) |
Chrome deprecated JSON-string input from Chrome 155.14 |
Tested in Chromium 156: both tools register, getTools() returns them with their annotations, search_availability returns its slots through executeTool(), book_table goes through the page’s confirmation (both the yes and the no path), and view.abort() removes both tools and fires toolchange.
Still in fluxasking the user mid-call
requestUserInteraction()was removed on 11 June 2026 because, in the words of the pull request, “this interface and its only method are not fully specified and not implemented yet”. The removal is meant to last until issue #165 and pull request #204 settle a more permanent design.8 So Listing D.1’sbook_tablenever worked anywhere as written. Pull request #204, now titled “Replace requestUserInteraction with requestUserInput”, was still open on 2 November 2026 (Section D.6).15 Until a replacement lands, confirm consequential actions in your own UI, as Listing D.3 does and Chapter 13 explains.
D.4 Keeping the “swap the tool set” pattern
To be fair to provideContext(), it had one genuinely useful property: a single-page app could switch views and replace the whole tool list in one call. It was removed because the same call let any script on the page wipe out tools it didn’t own.3 Chapter 5 tells that story. Listing D.4 keeps the useful half. A tool set owns one AbortController, and replace() aborts it and registers the new tools under a fresh one. In simple words, it can only ever remove its own tools.
appendices/d-migration/after/tool-set.js — a view-scoped tool set that replaces provideContext()// AFTER: the useful half of provideContext(), "swap this view's tools",// without the dangerous half. It can only remove tools it registered itself,// and it registers each set all or nothing.export function createToolSet(modelContext = document.modelContext) { let current = null; return { async replace(tools) { current?.abort(); // drop the previous set (ours only) const controller = new AbortController(); current = controller; try { await Promise.all(tools.map((tool) => modelContext.registerTool(tool, { signal: controller.signal }))); } catch (err) { controller.abort(); // roll back the tools that did register if (current === controller) current = null; throw err; } }, clear() { current?.abort(); current = null; } };}Tested in Chromium 156: with a tool registered by other code on the same page, replace([menu_search, opening_hours]) followed by replace([book_table]) leaves exactly book_table and the other code’s tool, and clear() leaves only the other code’s tool. Two tools with the same name in one replace() call reject with InvalidStateError, which provideContext() would have accepted without complaint. That rejection is why replace() has a catch. Since 30 October 2026 Chrome’s best-practices page recommends registering a view’s tools all at once and rolling back the whole batch when one of them fails, so an agent never sees half a tool surface.16 The first edition’s version of Listing D.4 didn’t roll back: after a failed replace(), the tools that did register stayed. Tested in Chromium 156, the version above leaves no tool of the failed set behind.
D.5 Finding old code paths
A site that ported half its code, or a library that still has a navigator branch, fails quietly. Listing D.5 tells the cases apart, so you can log them.
appendices/d-migration/after/detect.js — feature detection that spots the legacy namespace// AFTER: feature detection that also spots outdated code paths.export function webmcpStatus() { if ('modelContext' in document) return 'current'; // document.modelContext if ('modelContext' in navigator) return 'legacy-navigator'; // a pre-May-2026 build return 'unavailable';}In Chromium 156 with WebMCPTesting, webmcpStatus() returns 'current' (tested). 'legacy-navigator' should only appear in old builds. A quick search of your codebase for navigator.modelContext, provideContext, clearContext, unregisterTool and requestUserInteraction finds the rest.
D.6 From the first edition to now (October to November 2026)
If you followed the first edition of this book, I have good news: you don’t need to port anything. The draft of 27 October 2026 has the same IDL as the 15 October draft the first edition used, line for line (I compared the two IDL indexes), and every listing in this appendix ran unchanged in the same Chromium 156 build.13 What moved in those weeks is around the API, not in it.
Table D.3 — What changed between the first edition and November 2026, and what to do
| What changed | When | What to do in your code |
|---|---|---|
The spec’s editors said Chrome is wrong to return plain strings unquoted, and proposed that undefined becomes "null" instead of rejecting (issue #361) |
26 October 2026 | Nothing, if you return objects. If a tool returns a bare string, expect it to arrive JSON-quoted once Chrome follows the spec. If a tool returns nothing, don’t depend on the "undefined" that Chromium 156 produces.17 |
Chrome’s Imperative API page now points React developers to the use-webmcp-tool package (a useWebMCP hook) instead of usewebmcp, and Vue developers to the useWebMCP composable in @vueuse/core |
16 October 2026 | Check which package your components use. Chapter 10 covers the React and Vue options.18 |
| The same-origin to-do note in the spec’s security section became §6.3.4, “Interaction with the Same-Origin Policy”, with the two-sided opt-in written up | 27 October 2026 | No code change. Links to the old #violation-same-origin-boundaries anchor no longer resolve, so update them in your docs and comments.19 |
| Chrome’s best-practices page added “register tools all at once” and explicit commit points for cancellation | 30 October 2026 | Make tool sets all or nothing (Listing D.4) and decide where a running tool stops honouring its signal (Chapter 6).16 |
| Chrome 156 became the stable channel, Chrome 157 follows on 3 November 2026 | 20 October 2026 | Re-run your tests on each new stable version. The origin trial still runs to Chrome 162 (Chapter 15).20 |
Still in fluxthe next migration
Four open pull requests would change the API again if they merge:
Toolas an interface instead of theRegisteredTooldictionary (#348),outputSchema(#254),requestUserInput()(#204) and anopenWorldHintannotation (#355). Only the first one could break existing code, and only code that builds or copiesRegisteredToolobjects by hand. Appendix A, Section A.9, lists what each would change.21 My advice for today is simple: treat whatgetTools()returns as opaque, pass it back toexecuteTool()as it is, and return objects from every tool.
D.7 Checklist
- Replace
navigator.modelContextwithdocument.modelContext, and the feature test with'modelContext' in document. - Turn each
provideContext({ tools })into oneregisterTool(tool, { signal })per tool, sharing oneAbortControllerper view or component. - Replace
clearContext()andunregisterTool(name)withabort(). awaiteveryregisterTool()and handle rejections (InvalidStateError,NotAllowedError,SecurityError, see Appendix A, Table A.7).- Change
execute(input, client)toexecute(input, { signal }), and pass the signal to anything long-running. - Remove
requestUserInteraction(). Confirm in your own UI instead and mark the toolconsequentialHint: true. - Return objects, not strings or
undefined. The spec and Chrome still disagree on both (Section D.6). - Pass objects, not JSON strings, to
executeTool(). - Register each view’s tools all or nothing (Listing D.4).
- Re-test in Chrome with
chrome://flags/#enable-webmcp-testingenabled, and again on every new stable version.22
D.8 Summary
- Two changes break early-2026 code:
provideContext()andclearContext()were removed on 5 March 2026, andnavigator.modelContextmoved todocument.modelContexton 27 May 2026. - In Chromium 156 old code fails quietly:
navigator.modelContextdoesn’t exist, guarded code registers nothing, and unguarded code throws. - Register one tool per call and unregister by aborting the signal you passed. A small tool-set helper (Listing D.4) keeps the view-switching convenience of
provideContext()without its risk, and now rolls back a failed set. - The execute callback’s second argument is
{ signal }.requestUserInteraction()is gone until a new design lands, so confirmation belongs in your page. - Code written for the book’s first edition needs no porting: the 27 October 2026 draft has the same IDL. Return objects, keep
getTools()results opaque, and watch the four open pull requests.
Notes
webmachinelearning/webmcp README, section “Future Exploration: Declarative API”, as changed on 8 October 2026 (its last change as of 2 November 2026) (retrieved November 2026). https://github.com/webmachinelearning/webmcp#future-exploration-declarative-api
webmachinelearning/webmcp commit history (
git logofmainandindex.bs), read 14 October 2026 (head 1636b33) and again on 15 October 2026 (head 3279798, 15 October 2026). Every date in Table D.1 was re-checked against the commits that day. Merge times for pull requests #132, #184 and #205 were read from the GitHub API the same day. https://github.com/webmachinelearning/webmcp/commits/main/index.bswebmachinelearning/webmcp pull request #132, “Remove
provideContext()andclearContext()”, by Dominic Farolino, merged 5 March 2026 at 19:47 UTC (commit fe84c67). Its description says it closes issue #101, “navigator.modelContext.provideContextallows overwriting of previously registered tools in the same environment” (opened by François Beaufort, 20 February 2026), and follows the proposal made there, with tool unregistration to be designed in issue #130. Read 15 October 2026. Issue: https://github.com/webmachinelearning/webmcp/issues/101. Pull request: https://github.com/webmachinelearning/webmcp/pull/132webmachinelearning/webmcp commit 6708e33, “
registerTool()takes an optionalAbortSignal(#147)”, 26 March 2026: addsModelContextRegisterToolOptions.signaland removesunregisterTool(DOMString name)from the IDL. Read 14 October 2026. https://github.com/webmachinelearning/webmcp/commit/6708e3399871458c07ed4cddc04057dec2549955webmachinelearning/webmcp commit 986f461, “Scope
ModelContexttoDocument, and handle detached documents (#177)”, committed 18 May 2026 at 22:19 −04:00 (19 May UTC). Read 14 October 2026. https://github.com/webmachinelearning/webmcp/commit/986f4611d6696843c4ba86c96b7623a5cf2e5413webmachinelearning/webmcp pull request #184, “Move the
modelContextgetter toDocument”, by Dominic Farolino, merged 27 May 2026 at 15:04 UTC (commit c7b5c70). It closes issue #173 and replacespartial interface Navigatorwithpartial interface Document. Read 15 October 2026. https://github.com/webmachinelearning/webmcp/pull/184webmachinelearning/webmcp commit ec37ec4, “Make
registerTool()return a promise (#200)”, 8 June 2026:undefined registerTool(…)becomesPromise<undefined> registerTool(…). Read 14 October 2026. https://github.com/webmachinelearning/webmcp/commit/ec37ec4878e08a166f1460ac40b1ca4ab8096d14webmachinelearning/webmcp pull request #205, “Remove
ModelContextClientfor now”, merged 11 June 2026 (commit 067a1c9): the callback changes from(object input, ModelContextClient client)to(object input). Quoted from its description, read 15 October 2026. https://github.com/webmachinelearning/webmcp/pull/205webmachinelearning/webmcp commit 83e893a, “Spec
AbortSignalintegration with tool execution (#247)”, 19 August 2026: introducesToolExecuteCallbackOptionswithsignal. Read 14 October 2026. https://github.com/webmachinelearning/webmcp/commit/83e893ac3eaa8d72610062933c33a79494bf4fb5webmachinelearning/webmcp commit 14ae813, “Remove declarative API from the spec for now (#338)”, by François Beaufort, committed 8 October 2026. Read 14 October 2026. https://github.com/webmachinelearning/webmcp/commit/14ae813cc4e9fe0d39313f4b1ee5b231746731ad
webmachinelearning/webmcp commit b206dae, “Replace UnknownError with specific DOMException types. (#339)”, by Mark Foltz, committed 8 October 2026. Read 14 October 2026. https://github.com/webmachinelearning/webmcp/commit/b206dae8bad34ea8e6ff7213b417b50b14a37e9a
webmachinelearning/webmcp
docs/proposal.mdas of commit adbc2c5, “Fix provideContext API example”, 17 November 2025: thewindow.navigator.modelContext.provideContext({ tools: [...] })example,execute({ text }, agent), andregisterTool/unregisterTool("add-todo"). Read 14 October 2026. https://github.com/webmachinelearning/webmcp/blob/adbc2c5374ec898d54bc312a871f9de62e582671/docs/proposal.mdWebMCP specification, Draft Community Group Report, 27 October 2026, §4.1 Extensions to Document, §4.2 ModelContext interface and the IDL index, compared with the IDL index of the 15 October 2026 draft: identical (retrieved November 2026). https://webmachinelearning.github.io/webmcp/#idl-index
Chrome for Developers, “Imperative API” (published 18 May 2026, last updated 16 October 2026): “JSON stringified input arguments are deprecated from Chrome 155.” (retrieved November 2026). https://developer.chrome.com/docs/ai/webmcp/imperative-api
webmachinelearning/webmcp pull request #204, “Replace requestUserInteraction with requestUserInput”, by Brandon Walderman, opened 9 June 2026, last updated 29 October 2026, open on 2 November 2026. https://github.com/webmachinelearning/webmcp/pull/204
Chrome for Developers, “WebMCP best practices” (published 18 May 2026, last updated 30 October 2026), section “Enforce architectural constraints”: register tools all at once, establish explicit commit points (retrieved November 2026). https://developer.chrome.com/docs/ai/webmcp/best-practices
webmachinelearning/webmcp issue #361, about how
executeTool()serializes strings andundefinedin the spec and in Chrome, opened 23 October 2026, with François Beaufort’s reply of 26 October 2026 (“The WPT assertions are wrong, and so is Chrome.”). Open on 2 November 2026. https://github.com/webmachinelearning/webmcp/issues/361Chrome for Developers, “Imperative API” (last updated 16 October 2026), the React section (
use-webmcp-tool,useWebMCPhook) and the new Vue section (@vueuse/core,useWebMCPcomposable) (retrieved November 2026). https://developer.chrome.com/docs/ai/webmcp/imperative-apiwebmachinelearning/webmcp pull request #326, “Document cross-origin tool exposure and two-sided opt-in in Section 6.”, merged 27 October 2026. The new section is at https://webmachinelearning.github.io/webmcp/#interaction-with-same-origin-policy. Pull request: https://github.com/webmachinelearning/webmcp/pull/326
Chromium Dash, release schedule: Chrome 156 stable since 20 October 2026, Chrome 157 stable date 3 November 2026 (retrieved November 2026). Origin trial milestones from Chrome Platform Status, feature 5117755740913664 (trial extended to Chrome 162). https://chromiumdash.appspot.com/schedule
webmachinelearning/webmcp pull requests #348, #254, #204 and #355, all open on 2 November 2026 (states read from the GitHub API). https://github.com/webmachinelearning/webmcp/pulls
Chrome for Developers, “WebMCP” overview (published 18 May 2026, last updated 7 October 2026), section “Local WebMCP” (retrieved November 2026). https://developer.chrome.com/docs/ai/webmcp