---
phase: 16-shared-bridge-library
plan: 02
type: execute
wave: 2
depends_on: ["16-01"]
files_modified:
  - crates/mcp-preview/assets/index.html
  - crates/mcp-preview/assets/widget-runtime.js
  - crates/mcp-preview/assets/widget-runtime.mjs
  - Makefile
autonomous: true
requirements: [DEVX-03, DEVX-05]

must_haves:
  truths:
    - "Preview server serves the compiled widget-runtime at a stable URL and widgets can load it via `<script type='module'>`"
    - "The inline bridge injection in wrapWidgetHtmlProxy() and wrapWidgetHtmlWasm() is replaced by a reference to the shared library"
    - "Existing chess and map widgets continue to work without modification via the backward-compat shim"
    - "The Makefile `build` target builds TypeScript before compiling the Rust preview crate"
    - "The standalone widget-runtime.js IIFE is replaced by the compiled ESM output"
  artifacts:
    - path: "crates/mcp-preview/assets/widget-runtime.mjs"
      provides: "Compiled ES module served at /assets/widget-runtime.mjs"
    - path: "crates/mcp-preview/assets/index.html"
      provides: "Preview UI with shared library integration replacing inline bridge code"
      contains: "AppBridge"
    - path: "Makefile"
      provides: "Build orchestration: TypeScript before Rust"
      contains: "widget-runtime"
  key_links:
    - from: "crates/mcp-preview/assets/index.html"
      to: "crates/mcp-preview/assets/widget-runtime.mjs"
      via: "import { AppBridge } from '/assets/widget-runtime.mjs' in preview outer page"
      pattern: "import.*AppBridge.*widget-runtime"
    - from: "crates/mcp-preview/assets/index.html"
      to: "/api/tools/call"
      via: "AppBridge toolCallHandler proxies to preview API"
      pattern: "toolCallHandler.*fetch.*api/tools/call"
    - from: "packages/widget-runtime/dist/index.mjs"
      to: "crates/mcp-preview/assets/widget-runtime.mjs"
      via: "Makefile copy step"
      pattern: "cp.*widget-runtime"
---

<objective>
Integrate the compiled shared bridge library into the preview server, replacing inline bridge injection with `<script type="module">` references, and set up build orchestration so TypeScript compiles before Rust.

Purpose: This is where DEVX-03 (eliminating duplicated bridge code) actually manifests -- the three separate bridge implementations (proxy inline, WASM inline, standalone IIFE) are replaced by the single canonical ES module from Plan 01.

Output: Updated preview UI with shared library integration, updated standalone widget-runtime.js, Makefile with TypeScript build step.
</objective>

<execution_context>
@./.claude/get-shit-done/workflows/execute-plan.md
@./.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/16-shared-bridge-library/16-CONTEXT.md
@.planning/phases/16-shared-bridge-library/16-RESEARCH.md
@.planning/phases/16-shared-bridge-library/16-01-SUMMARY.md

# Files to modify
@crates/mcp-preview/assets/index.html
@crates/mcp-preview/assets/widget-runtime.js
@crates/mcp-preview/src/handlers/assets.rs
@Makefile
</context>

<tasks>

<task type="auto">
  <name>Task 1: Replace inline bridge injection with shared library in preview index.html</name>
  <files>
    crates/mcp-preview/assets/index.html
    crates/mcp-preview/assets/widget-runtime.mjs
  </files>
  <action>
**Copy compiled ESM to preview assets:**
- Copy `packages/widget-runtime/dist/index.mjs` to `crates/mcp-preview/assets/widget-runtime.mjs`
- This file will be embedded by `rust_embed` and served at `/assets/widget-runtime.mjs`

**Update the preview outer page (index.html) to use AppBridge:**

The preview's outer page (not the iframe) acts as the "host" in MCP Apps terminology. It must:

1. **Import AppBridge** from the shared library at the top of the main `<script type="module">`:
   ```javascript
   import { AppBridge } from '/assets/widget-runtime.mjs';
   ```

2. **Replace wrapWidgetHtmlProxy()** -- Instead of injecting ~100 lines of inline `window.mcpBridge = { ... }` JavaScript into the widget's srcdoc, inject a minimal loader:
   - The widget iframe HTML gets a `<script type="module">` that imports `App` from `/assets/widget-runtime.mjs` and calls `installCompat(app)` to set up `window.mcpBridge` for backward compatibility
   - The import uses an absolute URL path (`/assets/widget-runtime.mjs`) which works because the iframe is loaded via `srcdoc` but the preview server serves assets at that path
   - **IMPORTANT srcdoc + ES module pitfall:** `srcdoc` iframes have a `null` origin, so `<script type="module" src="/assets/...">` may fail. Workaround: embed the bridge code as an inline `<script type="module">` using dynamic `import()`:
     ```javascript
     <script type="module">
       const { App, installCompat } = await import('/assets/widget-runtime.mjs');
       const app = new App({ name: 'PreviewWidget', version: '1.0.0' });
       installCompat(app);
       await app.connect();
     </script>
     ```
   - This works because dynamic `import()` in inline modules resolves against the parent page's base URL, not the srcdoc origin

3. **Replace wrapWidgetHtmlWasm()** with the same loader approach -- the widget-side code is identical regardless of bridge mode. The difference is only on the HOST side (proxy vs WASM).

4. **Use AppBridge on the host side:**
   - In the preview runtime class, when loading a widget into the iframe, create an AppBridge instance:
     ```javascript
     this.appBridge = new AppBridge({
       iframe: this.widgetFrame,
       toolCallHandler: async (name, args) => {
         // Existing proxy: fetch('/api/tools/call', ...)
         // Or WASM: wasmClient.call_tool(name, args)
         // Dispatch based on this.bridgeMode
       },
       origin: window.location.origin
     });
     this.appBridge.initialize();
     ```
   - The AppBridge handles the `ui/initialize` handshake, routes `tools/call` requests, and manages the postMessage lifecycle

5. **Preserve DevTools logging:**
   - Hook into AppBridge events (or wrap the toolCallHandler) to log bridge calls to the Network tab
   - The existing `logBridgeCall()` function should still work -- just call it from within the toolCallHandler wrapper
   - Badge count, timing, expandable details -- all preserved

6. **Preserve bridge mode toggle:**
   - The Proxy/WASM toggle button stays
   - When toggling, destroy the current AppBridge, switch the toolCallHandler implementation, and create a new AppBridge
   - Proxy mode: toolCallHandler uses `fetch('/api/tools/call', ...)`
   - WASM mode: toolCallHandler uses the WASM client's `call_tool()`

7. **Keep backward compatibility:**
   - The widget-side `installCompat(app)` shim ensures `window.mcpBridge.callTool()` works
   - Existing chess/map widget HTML that calls `window.mcpBridge.callTool()` will work without changes
   - The `window.openai` alias is also installed by `installCompat()`

**What to remove:**
- The ~170 lines of inline bridge JavaScript in `wrapWidgetHtmlProxy()` (the `window.mcpBridge = { callTool: async (name, args) => { ... } }` block)
- The ~80 lines of inline WASM bridge JavaScript in `wrapWidgetHtmlWasm()` (the WASM client initialization and bridge adapter)
- The old `wrapWidgetHtml()` dispatch function (replaced by unified wrapping that uses AppBridge host-side)

**What to keep:**
- Resource picker, auto-load, connection status, reconnect button
- DevTools panel (Console, Network, Events, State tabs)
- Bridge toggle UI (Proxy/WASM buttons)
- All CSS styles
  </action>
  <verify>
Open the preview server in a browser: `cargo run -p cargo-pmcp -- pmcp preview` (or equivalent). Verify:
1. Widget loads and renders in the iframe
2. `window.mcpBridge.callTool()` works from the widget (backward compat)
3. DevTools Network tab shows bridge calls
4. Proxy/WASM toggle still works
5. No "Failed to fetch module" errors in console

If browser testing is not possible, verify by: reading the modified `index.html` and confirming the inline bridge code blocks are gone and replaced by import statements referencing `/assets/widget-runtime.mjs`.
  </verify>
  <done>
Preview `index.html` uses `AppBridge` from shared library on the host side. Widget iframe loads bridge via dynamic `import('/assets/widget-runtime.mjs')` instead of inline code. The ~250 lines of duplicated inline bridge JavaScript are eliminated. DevTools logging, bridge toggle, and all existing UI features preserved.
  </done>
</task>

<task type="auto">
  <name>Task 2: Replace standalone widget-runtime.js IIFE and update Makefile build orchestration</name>
  <files>
    crates/mcp-preview/assets/widget-runtime.js
    Makefile
  </files>
  <action>
**Replace standalone widget-runtime.js:**
- The current `crates/mcp-preview/assets/widget-runtime.js` is a hand-written IIFE that duplicates bridge code
- Replace it with a thin standalone loader that imports the shared ES module and creates an AppBridge in "mini host" mode:
  ```javascript
  // Standalone WASM Bridge Loader
  // Creates a mini-host that manages an iframe and proxies tool calls through the WASM MCP client
  // Usage: <script type="module" src="widget-runtime.js" data-mcp-url="http://localhost:3000/mcp"></script>
  ```
- The standalone loader:
  1. Reads `data-mcp-url` from the script tag (same pattern as current IIFE)
  2. Dynamically imports the WASM client modules
  3. Creates a WASM MCP client and connects to the server
  4. If running as a standalone page (not inside a host iframe), acts as a mini-host:
     - Creates an AppBridge with the WASM client as the toolCallHandler
     - Dispatches `mcpBridgeReady` event when connected
  5. Also installs `window.mcpBridge` via `installCompat()` for backward compatibility
  6. Fires `mcpBridgeError` event on failure
- This file remains an IIFE wrapper (for `data-mcp-url` script tag usage) but internally delegates to the shared library
- **Key difference from current:** The bridge API implementation is NOT duplicated -- it comes from the compiled ES module

**Update Makefile:**
- Add a new target `build-widget-runtime` that:
  1. Runs `cd packages/widget-runtime && npm run build`
  2. Copies `packages/widget-runtime/dist/index.mjs` to `crates/mcp-preview/assets/widget-runtime.mjs`
- Make this target a dependency of the existing build targets that compile `mcp-preview`
- Pattern: `build-preview: build-widget-runtime` (or equivalent Makefile dependency)
- Add a `clean-widget-runtime` target that removes `packages/widget-runtime/dist/` and `crates/mcp-preview/assets/widget-runtime.mjs`

Note: The user's `~/.claude/CLAUDE.md` prefers justfile over Makefile. However, the project currently uses a Makefile (no justfile exists). For this phase, update the existing Makefile to avoid creating a parallel build system. Migration to justfile can happen separately.
  </action>
  <verify>
1. `make build-widget-runtime` succeeds (TypeScript compiles, file copied to preview assets)
2. `crates/mcp-preview/assets/widget-runtime.mjs` exists and contains the compiled ES module
3. `crates/mcp-preview/assets/widget-runtime.js` exists and is a thin loader (not the old 136-line IIFE with duplicated bridge code)
4. `wc -l crates/mcp-preview/assets/widget-runtime.js` is significantly smaller than before (was 136 lines of duplicated bridge code)
  </verify>
  <done>
Standalone `widget-runtime.js` is a thin loader delegating to the shared library. Makefile has `build-widget-runtime` target that compiles TypeScript and copies output to preview assets. Build orchestration ensures TypeScript is compiled before Rust embeds the assets.
  </done>
</task>

</tasks>

<verification>
1. `make build-widget-runtime` -- exit code 0
2. `ls crates/mcp-preview/assets/widget-runtime.mjs` -- compiled ESM exists
3. `grep 'AppBridge' crates/mcp-preview/assets/index.html` -- AppBridge used in preview
4. `grep -c 'window.mcpBridge = {' crates/mcp-preview/assets/index.html` -- returns 0 (no more inline bridge definition)
5. `wc -l crates/mcp-preview/assets/widget-runtime.js` -- significantly reduced from 136 lines
6. `grep 'import.*widget-runtime' crates/mcp-preview/assets/index.html` -- shared library imported
</verification>

<success_criteria>
- Preview server uses AppBridge from shared library instead of inline bridge injection
- wrapWidgetHtmlProxy() and wrapWidgetHtmlWasm() no longer contain duplicated bridge JavaScript
- Widget iframe loads bridge via dynamic import of `/assets/widget-runtime.mjs`
- Standalone widget-runtime.js is a thin loader, not a duplicated bridge implementation
- Makefile builds TypeScript before Rust compilation
- All existing preview features preserved: DevTools, bridge toggle, resource picker, connection status
- Existing widgets using `window.mcpBridge.callTool()` continue to work via backward-compat shim
</success_criteria>

<output>
After completion, create `.planning/phases/16-shared-bridge-library/16-02-SUMMARY.md`
</output>
