Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,14 @@ loopback origin serves every page opened by that MCP process under a separate
random capability path. It writes no service state and disappears with the MCP
session; the page directory and `comments.jsonl` remain the durable record.

The embedded route is experimental and local-host-only. A comments-only authored
snapshot remains available as an explicit fallback, and builds that do not render
the app use the normal full browser page. In every case the detached Codex adapter,
not an MCP host message, carries durable feedback into the next task turn.
The embedded route is experimental and local-host-only. One adaptive app resource
renders either that complete route or the explicit comments-only authored snapshot,
according to the tool result. It keeps the nested route hidden until Leaf reports
that it is ready, skips origins absent from the host's approved frame domains, and
falls back to the snapshot when a host disallows or cannot load the frame.
Builds that do not render the app use the normal full browser page. In every case
the detached Codex adapter, not an MCP host message, carries durable feedback into
the next task turn.

No config or account is required. It needs
[`uv`](https://docs.astral.sh/uv/) and
Expand All @@ -67,14 +71,19 @@ Then ask the agent for a page. The explicit skill is `/leaf [topic]` in Claude C
and `$leaf [topic]` in Codex; with no argument it presents whatever the session is
currently about.

To expose the same resources from a checkout to another local MCP Apps host, run:
To expose the same resource from a checkout to another local MCP Apps host, run:

```sh
bin/leaf mcp
```

The model-visible `leaf_present` tool takes an initialized page's absolute
directory. `leaf_present_snapshot` selects the smaller fallback explicitly.
directory. `leaf_present_snapshot` selects the smaller fallback explicitly. The
presentation and refresh tools use the read-only hint so opening a page does not
request write approval. A presentation may materialize a changed, valid `index.html`
as Leaf's next immutable revision inside that page directory; it does not edit the
source, append an event, or write outside Leaf's revision store. Only a snapshot
comment append requests write approval.

## Packages

Expand Down
37 changes: 23 additions & 14 deletions notes/mcp-apps/PROJECT.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# MCP Apps research

## Research Questions

**Primary**: Can Leaf add MCP Apps as a delivery surface without adding a second interface or state authority?
Expand All @@ -10,32 +12,39 @@

## Current Status

### Latest Results: experiment 30
### Latest Results: experiment 34

The production candidate passed in the official reference host: one exact CSP
origin served the canonical page at a queryless random capability path, reached
Leaf's presentation gate, appended a keyboard option action through the ordinary
event endpoint, negotiated fullscreen, and produced no Leaf-origin console
failure. This removes the earlier wildcard CSP, per-page server, query token,
partitioned cookie, and hard-coded single-choice compact interface.
A fresh `leaf_candidate_final` process loaded the current shared resource and
completed `leaf_present` without an approval prompt. Codex committed and ran the
widget, then issued one successful app-initiated tool call for the automatic
`leaf_snapshot_refresh`. It made no nested localhost attempt and logged none of
experiment 32's CSP or renderer failures. A retained screenshot shows the
correct full-mode shell and its immediate capability-gate status, with no broken
frame. The successful refresh log and Chromium tests cover the final snapshot;
another app took focus before its screenshot could be retained.

### Current Experiment: none

**Status**: Local reference-host candidate complete
**Purpose**: The combined registered server now covers the complete canonical
page, an explicit authored-snapshot fallback, and exact-origin capability routing.
**Status**: A fresh Codex process visually confirmed the capability-gate
transition. Logs and Chromium cover the rendered fallback; its final Codex
screenshot remains outstanding.
**Purpose**: Retain visual evidence of the final snapshot without changing the
validated transport.

## Next Steps

1. Probe the installed Codex host and record whether its private-network policy
accepts the exact localhost frame origin.
1. Repeat the fresh-identity `leaf_present` probe as experiment 35 while keeping
its task foregrounded long enough to retain the final snapshot screenshot.
2. Keep the detached Codex adapter as the sole durable wake and acknowledgement
carrier; no remaining implementation decision depends on `ui/message` policy.
3. If accessibility parity becomes the next question, compare Axe before and
after the option action to isolate experiment 30's moderate `region` result.

## Reference

- Reproduce experiment 30: `bash notes/mcp-apps/experiments/30/commands.sh`
- Read its result: `cat notes/mcp-apps/experiments/30/results/reference-host.json`
- Read the fresh-process Codex result: `cat notes/mcp-apps/experiments/34/README.md`
- Read the invalid reused-process attempt: `cat notes/mcp-apps/experiments/33/README.md`
- Read the unified candidate result: `cat notes/mcp-apps/experiments/32/README.md`
- Read the installed-main baseline: `cat notes/mcp-apps/experiments/31/README.md`
- Reproduce the last reference-host run: `bash notes/mcp-apps/experiments/30/commands.sh`
- Inspect the current project: `sed -n '1,240p' notes/mcp-apps/PROJECT.md`
56 changes: 56 additions & 0 deletions notes/mcp-apps/experiments/31/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Experiment 31: Installed Codex presentation boundary

## Purpose

Test whether the installed Codex host attaches registered Leaf MCP Apps, whether
it distinguishes multiple resource URIs from one MCP server, and keep resource
attachment separate from the nested page's localhost policy.

**Configuration:**

- A fresh projectless Codex task with the installed Leaf plugin from main.
- The model-visible `leaf_present` and `leaf_present_snapshot` tools.
- One older initialized page whose vendored registry omitted the current
`pickup` event kind, followed by a freshly initialized compatible page.

**Expected outcomes:**

- A stale vendored layer produces a readable compatibility refusal.
- A compatible tool result attaches its bound MCP App resource when the task is
viewed.
- The complete app attempts its exact localhost frame only after the host runs
the matching full-page bundle.

## Findings

Both tools were registered against the older page, but each returned only its
generic `Error executing tool ...` text and attached no app. Replaying that page
through the shipped stdio server exposed the hidden exception: source validation
reported that `$events.kinds` omitted `pickup`, `PageStateService` returned
`browser=None`, and both presentation summaries indexed
`state["browser"]["basis"]`. The required compatibility behavior is a readable
`ToolError` that names the stale layer and the `leaf page init` repair.

Both tools then completed successfully against a freshly initialized compatible
page. `leaf_present` returned its ready-at-Draft text and
`leaf_present_snapshot` returned ready-at-r1. The first report that neither app
attached was invalid because the completed probe task was still in the
background. When the desktop navigated to that task, logs at 18:24:25–27 recorded
`sandbox_requested`, `guest_attached`, `widget_frame_committed`, and
`widget_running` for both tool call ids.

The host had logged exactly one `mcpServer/resource/read` after both calls, and
both widgets used the same server-level sandbox source,
`source-898a63e66648a40b`. Direct stdio reads showed that Leaf's two registered
URIs returned distinct, correct HTML blobs. The shared host HTML was demonstrably
the snapshot bundle: the `Leaf present snapshot` card rendered the authored
`Codex MCP Apps probe` content and controls, while the primary `Leaf present`
card showed `rundefined · event undefined` and then
`Cannot read properties of undefined (reading 'replaceAll')` when that renderer
received the full-page result.

This run proves that Codex renders Leaf Apps lazily and that the snapshot works
inline. It also proves that this host chooses or caches one UI resource per MCP
server rather than preserving Leaf's two URI bindings. The primary failed before
its nested localhost frame could be attempted, so the run gives no evidence for
or against Codex's private-network or frame policy.
76 changes: 76 additions & 0 deletions notes/mcp-apps/experiments/32/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Experiment 32: Unified Codex App candidate

## Purpose

Submit the one-resource adaptive candidate to the real Codex host, verify that
tool annotations avoid read approvals, and test the complete-page frame only
after removing experiment 31's resource collision.

**Changes from experiment 31:**

- Every presentation and app-only tool binds one adaptive
`ui://leaf/page/v1.html` resource.
- The app selects full-page or snapshot behavior from the result's explicit
format and mode.
- Presentation and refresh tools are read-only; only snapshot event append is a
write.

**Configuration:**

- Temporary MCP server name: `leaf_candidate` only; the installed `leaf` server
was disabled for the probe.
- Codex `default_tools_approval_mode`: `writes`.
- A compatible freshly initialized `Codex MCP Apps probe` page.
- One task called both presentation tools; a separate task called only
`leaf_present` to identify the full-mode card unambiguously.

**Expected outcomes:**

- No approval prompt means Codex honors the read-only tool annotations.
- Correct chrome on both cards means one adaptive resource removes the
server-level cache collision.
- Authored content inside the primary frame means Codex permits the exact
localhost frame domain; a frame error isolates the remaining host boundary.

## Findings

Both presentation calls completed with zero approval prompts under `writes`, so
Codex honored the read-only annotations. The snapshot selected the unified app's
snapshot mode and rendered the authored content and controls correctly.

The full-only task selected the right renderer too: its shell showed
`Complete page · Draft · event 0`, with no undefined fields or `replaceAll`
failure. The nested surface displayed Chromium's broken-frame document instead
of the Leaf page. Desktop log line 18919 identified the exact boundary:
`mcp_app_sandbox.guest_load_failed`, `errorCode=-30`,
`errorDescription=ERR_BLOCKED_BY_CSP`, `isMainFrame=false`, with a validated
`http://localhost:<port>/p/<capability>` URL. The browser error document was a
consequence of Codex's nested-frame CSP enforcement, not a connection refusal or
private-network-access failure.

The candidate shell also announced `Leaf page loaded.` because an iframe `load`
event fires for that error document. That is not a valid readiness signal. This
submission therefore proves snapshot success, permission success, and correct
adaptive dispatch, but not complete-page success. Codex blocks this localhost
nested frame despite the resource's exact `frameDomains` declaration. The
restriction is host-specific: experiment 30's official reference host accepted
the same exact-origin frame.

Review of Codex desktop 26.825.32147 made the policy mechanism decisive. Its
internal webview bundle normalizes `ui.csp.frameDomains` through a helper that
accepts only `https:` entries; `connectDomains` additionally permits `wss:`, and
`resourceDomains` does not authorize frames. The helper therefore removes Leaf's
exact `http://localhost:<port>` frame origin before the sandbox CSP is built,
which exactly predicts log line 18919's `ERR_BLOCKED_BY_CSP`. MCP Apps hosts may
apply stricter CSP policy than a resource requests, so this behavior is allowed
and does not contradict the official reference-host result.

The ext-apps host contract exposes that approved set as
`sandbox.csp.frameDomains`. A subsequent candidate can therefore skip the frame
without host-name sniffing when the exact Leaf origin is absent, while retaining
a readiness timeout for hosts that approve the origin or do not report the
field. That response belongs to the next submission, not this experiment.

Captured evidence was reviewed from `/tmp/leaf-unified-probe.IEKkq3/screen.png`
and `/tmp/leaf-unified-probe.IEKkq3/full-only.png`; it is not copied into the
repository.
38 changes: 38 additions & 0 deletions notes/mcp-apps/experiments/33/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Experiment 33: Reused Codex candidate process

## Purpose

Submit the capability- and readiness-gated candidate from experiment 32 and
verify that Codex skips its disallowed localhost frame before rendering the
snapshot fallback.

**Changes from experiment 32:**

- The app checks the host-approved frame domains before assigning the complete
page URL.
- An attempted frame remains hidden until the canonical page posts its readiness
marker, with a bounded snapshot fallback when no marker arrives.

**Configuration:**

- MCP server name: `leaf_candidate`.
- Codex task: `01a05e6e-c363-7fc2-a0e3-b774e2c68f4a`.
- Tool: `leaf_present`.

**Expected outcomes:**

- A newly launched candidate process serves the current adaptive bundle.
- Codex reports no approved HTTP frame domain, so the app avoids a nested
localhost request and calls `leaf_snapshot_refresh`.

## Findings

This attempt was invalid because `leaf_candidate` reused the already-running
pre-fix MCP process. The tool call completed without an approval prompt, but the
card still ran the old snapshot bundle against the full-page result. It showed
`rundefined` and failed while calling `replaceAll`.

The result measures process reuse, not the submitted candidate. Screenshot
evidence remains at `/tmp/leaf-experiment-33.png`; it is not copied into the
repository. Experiment 34 changes the server identity so Codex must launch a new
process.
51 changes: 51 additions & 0 deletions notes/mcp-apps/experiments/34/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Experiment 34: Fresh Codex candidate process

## Purpose

Repeat experiment 33 with a new MCP server identity so Codex loads the current
adaptive resource, then verify the capability-gated snapshot fallback from the
desktop host logs and UI.

**Changes from experiment 33:**

- MCP server name changed from `leaf_candidate` to `leaf_candidate_final`.
- The task called only `leaf_present` on one compatible page.

**Configuration:**

- MCP server name: `leaf_candidate_final`.
- Codex task: `01a05e70-8af3-74e3-bc77-eec5400761a6`.
- Page: `.tmp/adaptive-probe`.
- Tool: `leaf_present`.

**Expected outcomes:**

- The new identity launches a fresh MCP process and reads the current shared
resource.
- Codex's approved frame-domain set excludes Leaf's HTTP origin, so the app
assigns no nested localhost URL and immediately calls the read-only
`leaf_snapshot_refresh` tool.
- The presentation call and automatic refresh require no approval prompt.

## Findings

`leaf_present` completed once with zero approval prompts. On the task's first
view, desktop log lines 20769–20777 recorded the resource read, sandbox origin
`mcp-server-leaf-candidate-final`, committed widget frame, and running widget.
Line 20786 recorded one successful `mcpServer/tool/call` after app
initialization, matching the automatic `leaf_snapshot_refresh` fallback.

The server and task logs contain no `guest_load_failed`, `ERR_BLOCKED_BY_CSP`,
`replaceAll`, or nested localhost attempt. This confirms that the fresh
candidate avoided Codex's known CSP failure and started the read-only snapshot
path.

A screenshot captured 2.2 seconds after navigation shows the correct full-mode
shell (`Complete page · Draft · event 0`), no nested or broken frame, and the
status `This host did not approve the complete page frame. Opening the
comments-only snapshot…`. This visually confirms the capability-gate transition
before the snapshot content rendered. Another active app reclaimed focus a
second later, so this run still lacks a real-host screenshot of the final
snapshot. The successful refresh call in the logs and the repository's
real-Chromium tests cover that rendered fallback state. The screenshot remains
at `/tmp/leaf-experiment-34-final.png`; it is not copied into the repository.
10 changes: 5 additions & 5 deletions scripts/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,11 @@ published file is already loadable:
- Builds, where it is not, or where what ships is cut down to what the registry
declares: `highlight` and `pierre`, both cut to `$languages.names`, and
`plot`, bundled with d3 because Plot's published ESM leaves d3 as a bare
external import. `mcp-app` is the separate delivery-surface build: each tracked
shell under `scripts/mcp-app/` becomes one HTML blob containing the MCP Apps
SDK, its application code and styles, and the Leaf mark. The page shell then
frames the exact process-scoped origin declared by the resource. The separate
comments-only fallback lives as a self-contained authored asset.
external import. `mcp-app` is the separate delivery-surface build: the tracked
adaptive shell under `scripts/mcp-app/` becomes one HTML blob containing the MCP
Apps SDK, its application code and styles, and the Leaf mark. A complete-page
result frames the exact process-scoped origin declared by the resource; a
snapshot result renders inert authored markup in the same shell.

A bundle reproduces its tracked bytes exactly when every input it fetches is
pinned, which holds for the three copies and for `highlight`, so a clean
Expand Down
Loading
Loading