Commit Graph

2 Commits

Author SHA1 Message Date
Elie Habib
a409d5f79d fix(agent-readiness): WebMCP uses registerTool + static import (#3316) (#3361)
* fix(agent-readiness): WebMCP uses registerTool + static import (#3316)

isitagentready.com reported "No WebMCP tools detected on page load"
on prod. Two compounding bugs in PR #3356:

1) API shape mismatch. Deployed code calls
   navigator.modelContext.provideContext({ tools }), but the scanner
   SKILL and shipping Chrome implementation use
   navigator.modelContext.registerTool(tool, { signal }) per tool with
   AbortController-driven teardown. The older provideContext form is
   kept as a fallback.

2) Dynamic-import timing. The webmcp module was lazy-loaded from a
   deep init phase, so the chunk resolved after the scanner probe
   window elapsed.

Fix:

- Rewrite registerWebMcpTools to prefer registerTool with an
  AbortController. provideContext becomes a legacy fallback. Returns
  the AbortController so teardown paths exist.
- Static-import webmcp in App.ts and call registerWebMcpTools
  synchronously at the start of init, before any await. Bindings
  close over lazy refs so throw-on-null guards still fire correctly
  when a tool is invoked later.

Test additions lock in registerTool-precedes-provideContext ordering,
AbortController pattern, static import, and call-before-first-await.

* fix(agent-readiness): WebMCP readiness wait + teardown on destroy (#3316)

Addresses three findings on PR #3361.

P1 — startup race. Early registration is required for scanner probes,
but a tool invoked during the window between register and Phase-4 UI
init threw "Search modal is not initialised yet." Both scanners and
agents that probe-and-invoke hit this. Bindings now await a uiReady
promise that resolves after searchManager.init and countryIntel.init.
A 10s timeout keeps a broken init from hanging the caller. After
readiness, a still-null target is a real failure and still throws.

Mechanics: App constructor builds uiReady as a Promise with its
resolve stored on the instance; Phase-4 end calls resolveUiReady;
waitForUiReady races uiReady against a timeout; both bindings await it.

P2 — AbortController was returned and dropped. registerWebMcpTools
returns a controller so callers can unregister on teardown, but App
discarded it. Stored on App now and aborted in destroy, so test
harnesses and SPA re-inits don't accumulate stale registrations.

P2 — test coverage. Added assertions for: bindings await
waitForUiReady before accessing state; resolveUiReady fires after
countryIntel.init; waitForUiReady uses Promise.race with a timeout;
destroy aborts the stored controller. Kept silent-success guard
assertions so bindings still throw when state is absent post-readiness.

Tests: 16 webmcp, 6682 full suite, all green.

* test(webmcp): tighten init()/destroy() regex anchoring (#3316)

Addresses P2 from PR #3361 review. The init() and destroy() body
captures used lazy `[\s\S]+?\n  }` which stops at the first
2-space-indent close brace. An intermediate `}` inside init (e.g.
some exotic scope block) would truncate the slice; the downstream
`.split(/\n\s+await\s/)` would then operate on a smaller string and
could let a refactor slip by without tripping the assertion.

Both regexes now end with a lookahead for the next class member
(`\n\n  (?:public|private) `), so the capture spans the whole method
body regardless of internal braces. If the next-member anchor ever
breaks, the match returns null and the `assert.ok` guard fails
loudly instead of silently accepting a short capture.

P1 (AbortController silently dropped) was already addressed in
f3bbd2170 — `this.webMcpController` is stored and destroy() aborts
it. Greptile reviewed the first push.
2026-04-24 08:21:07 +04:00
Elie Habib
efb6037fcc feat(agent-readiness): WebMCP in-page tool surface (#3316) (#3356)
* feat(agent-readiness): WebMCP in-page tool surface (#3316)

Closes #3316. Exposes two UI tools to in-browser agents via the draft
WebMCP spec (webmachinelearning.github.io/webmcp), mirroring the static
Agent Skills index (#3310) for consistency:

- openCountryBrief({ iso2 }): opens the country deep-dive panel.
- openSearch(): opens the global command palette.

No bypass: both tools route through the exact methods a click would
hit (countryIntel.openCountryBriefByCode, searchModal.open), so auth
and Pro-tier gates apply to agent invocations unchanged.

Feature-detected: no-ops in Firefox, Safari, and older Chrome without
navigator.modelContext. No behavioural change outside WebMCP browsers.
Lazy-imported from App.ts so the module only enters the bundle if the
dynamic import resolves; keeps the hot-path init synchronous.

Each execute is wrapped in a logging shim that emits a typed
webmcp-tool-invoked analytics event per call; webmcp-registered fires
once at setup so we can distinguish capable-browser share from actual
tool usage.

v1 tools do not branch on auth state, so a single registration at
init is correct. Source-level comment flags that any future Pro-only
tool must re-register on sign-in/sign-out per the symmetric-listener
rule documented in the memory system.

tests/webmcp.test.mjs asserts the contract: feature-detect gate runs
before provideContext, two-or-more tools ship, ISO-2 validation lives
in the tool execute, every execute is wrapped in logging, and the
AppBindings surface stays narrow.

* fix(agent-readiness): WebMCP bindings surface missing-target as errors (#3316)

Addresses PR #3356 review.

P1 — silent-success via optional-chain no-op:
The App.ts bindings used this.state.searchModal?.open() and an
unchecked call to countryIntel.openCountryBriefByCode(). When the
underlying UI state was absent (pre-init, or in a variant that
skips the panel), the optional chain and the method's own null
guard both returned quietly, but the tool still reported "Opened"
with ok:true. Agents relying on that result would be misled.

Bindings now throw when the required UI target is missing. The
existing withInvocationLogging shim catches the throw, emits
ok:false in analytics, and returns isError:true, so agents get an
honest failure instead of a fake success. Fixed both bindings.

P2: dropped unused beforeEach import in tests/webmcp.test.mjs.

Added source-level assertions that both bindings throw when the
UI target is absent, so a future refactor that drops the check
fails loudly at CI time.
2026-04-24 07:14:04 +04:00