Skip to content

mcp apps: browser e2e for the view's real URL - #4667

Open
chelojimenez wants to merge 1 commit into
mainfrom
feat/mcp-apps-view-origin-e2e
Open

chelojimenez wants to merge 1 commit into
mainfrom
feat/mcp-apps-view-origin-e2e

Conversation

@chelojimenez

@chelojimenez chelojimenez commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Why

The properties that justify mounting views by document.write (#4661) are all browser-level, and jsdom models none of them — its document.open() only clears child nodes, adopting no URL and enforcing no policy. Up to now they were verified by a throwaway Playwright script I ran by hand. This makes them a checked-in gate.

Completes Phase 1. Remaining in the plan: host-origin pinning (1B), then the _meta.ui.domain work.

The six cases

asserts
Real URL the view's own location.href is the proxy URL, on the sandbox origin (127.0.0.1) and not the app's (localhost), with no srcdoc attribute
Third-party referrer a fixture server records what the browser actually sent it
CSP after the write an undeclared host is refused; the declared fixture is not
Widget self-reload location.reload() inside the view restarts it instead of blanking
srcdoc path still renders, and reports it has no URL
Quirks mode doctype-less HTML → BackCompat, with a doctype control → CSS1Compat

One finding worth flagging. My first version of the referrer test asserted the fixture would see the view's full URL. It doesn't: the app sends Referrer-Policy: strict-origin-when-cross-origin, so a cross-origin request carries the origin only — http://127.0.0.1:5375/. That's correct and privacy-preserving, it's the granularity a referrer allowlist matches anyway (http://127.0.0.1:*/*), and the thing that changed is that the value is a real origin rather than absent. The test now pins that, plus the two regressions that would matter: it must not be empty, and must not be the app's origin — otherwise the origin we tell developers to allowlist would be the wrong one.

Scope choice

The harness mounts SandboxedIframe directly rather than driving the chat renderer against a live MCP connection. Every property above lives in the sandbox layer, and the renderer's lifecycle/applied plumbing and the Sandbox Stack origin chip already have component tests that drive them directly. Building a fixture MCP server plus a connection flow to re-assert them through the UI would be a lot of machinery for coverage that exists — the harness header says so explicitly.

Infrastructure notes for the reviewer

  • test:e2e:mcp-apps runs predev first. dev:app:default assumes the generated server bundles already exist; in this fresh worktree the dev server died on a missing codex-appserver-bridge.bundled.js and every test timed out on a page that rendered but never mounted. playwright.oauth-debugger.config.ts has the same latent dependency — it survives because a dev has usually run npm run dev first. Worth considering the same change there separately.
  • The default suite ignores this spec (playwright.config.ts testIgnore), because it boots a production server where /__e2e/* is not registered. Verified: --list on the default config matches 0 of these, and the oauth-debugger config still matches only its own.
  • mountMode is a harness query parameter, not a second dev server with VITE_MCPJAM_VIEW_MOUNT=srcdoc — one build, one browser, both mount paths.
  • viewFrame() deliberately does not match on URL. Once mounted, the view reports the same URL as its parent proxy — the property under test — so a URL match would return the proxy and pass even if the view never mounted. It walks the frame tree instead. (My first attempt reached through outer.contentDocument from the page, which is null cross-origin; that's what the first red run was.)
  • client/src/router.tsx: my lazy block matches the formatting of the identical OAuthDebuggerE2EHarness block directly above it. Prettier v3 would reformat both; I left the neighbour consistency rather than making two adjacent identical imports look different. Say the word if you'd rather I match prettier.

Verification

npm run test:e2e:mcp-apps — 6 passed (~17s). npm run typecheck:client clean; route-elements-coverage still green with the new route; new files are prettier-clean.

🤖 Generated with Claude Code


Note

Low Risk
Adds dev-only routes, e2e fixtures, and npm/Playwright config only; no production sandbox or proxy behavior changes in this diff.

Overview
Adds a checked-in browser e2e suite for MCP App view mounting behaviors that jsdom cannot model (proxy URL adoption, cross-origin Referer/Origin, CSP binding after document.write, widget location.reload(), srcdoc fallback, and quirks mode without a doctype).

A dev-only /__e2e/mcp-app-view harness mounts SandboxedIframe directly (query params for mount, doctype, fixture origin) and records mcpjam:view-mode / CSP violation messages on window.__mcpAppViewE2E. Playwright drives six cases against a local fixture HTTP server that logs incoming Referer and Origin.

CI wiring: new test:e2e:mcp-apps (runs predev then playwright.mcp-apps.config.ts on a dedicated dev port), and the default playwright.config.ts now ignores mcp-app-view-origin.spec.ts like the oauth-debugger suite.

Reviewed by Cursor Bugbot for commit 2878816. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Adds browser e2e coverage for MCP App views mounted by document.write, pinning the real-URL behavior that jsdom can't model. Six cases verify the view runs at the sandbox proxy's URL on 127.0.0.1 (never the app's localhost), a third-party fixture sees the sandbox origin as referrer (trimmed by strict-origin-when-cross-origin, which is the granularity a referrer allowlist matches anyway), the injected CSP still binds after the write, location.reload() restarts a widget instead of blanking it, the srcdoc fallback still renders and reports no URL, and doctype-less HTML lands in quirks mode (matching claude.ai).

Infrastructure

  • New playwright.mcp-apps.config.ts runs a dev server, since the /__e2e/mcp-app-view harness route exists only behind import.meta.env.DEV; the default production suite ignores this spec.
  • mountMode is a harness query parameter, so one browser build covers both mount paths against one server.
  • The harness mounts SandboxedIframe directly rather than driving the chat renderer; renderer applied plumbing and the origin chip already have component tests.
  • test:e2e:mcp-apps runs predev first, because the dev server dies in a fresh worktree on missing generated server bundles.

Written for commit 2878816. Summary will update on new commits.

Review in cubic

The properties that made views mountable by document.write are all
browser-level, and jsdom models none of them — its document.open() only
clears child nodes, adopting no URL and enforcing no policy. Until now
they were verified by a scratch Playwright script; this makes them a
checked-in gate.

Six cases, against a dev server (the harness route only exists under
import.meta.env.DEV):

- the view's own location.href is the sandbox proxy's URL, on the
  sandbox origin (127.0.0.1) and not the app's (localhost), with no
  srcdoc attribute;
- a third-party fixture records what the browser actually sent it.
  Referrer-Policy: strict-origin-when-cross-origin trims the path, so
  what arrives is the view's ORIGIN — which is the granularity a
  referrer allowlist matches, and the change from carrying nothing at
  all. Origin-based allowlists (WebKit strips Referer cross-origin) get
  the same value;
- the injected CSP still binds after the write: an undeclared host is
  refused while the declared fixture is not;
- a widget calling location.reload() restarts instead of blanking;
- the srcdoc mount path renders and reports it has no URL, so the
  origin chip never offers a value no provider accepts;
- doctype-less HTML renders in quirks mode, with a doctype control —
  the one behaviour change from the srcdoc era, pinned as the
  deliberate claude.ai-fidelity choice it is.

The harness mounts SandboxedIframe directly rather than driving the
chat renderer: every property above lives in the sandbox layer, and the
renderer's lifecycle/applied plumbing and the origin chip are already
covered by component tests that can drive them directly.

`test:e2e:mcp-apps` runs `predev` first — `dev:app:default` assumes the
generated server bundles exist, so in a fresh clone the dev server dies
on a missing codex-appserver bundle. The default (production-server)
suite ignores this spec, since /__e2e/* is not registered there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@cursor

cursor Bot commented Sep 3, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_96e70ced-4859-4a12-8740-62adc595ab59)

@chelojimenez

Copy link
Copy Markdown
Contributor Author

✅ Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
✅ Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@coderabbitai

coderabbitai Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: 983080eb-dce2-4fba-b524-d0444b8c17d4

📥 Commits

Reviewing files that changed from the base of the PR and between 05f4d8a and 2878816.

📒 Files selected for processing (7)
  • mcpjam-inspector/client/src/components/e2e/McpAppViewE2EHarness.tsx
  • mcpjam-inspector/client/src/router.tsx
  • mcpjam-inspector/e2e/fixtures/mcp-app-view-fixture-server.ts
  • mcpjam-inspector/e2e/mcp-app-view-origin.spec.ts
  • mcpjam-inspector/package.json
  • mcpjam-inspector/playwright.config.ts
  • mcpjam-inspector/playwright.mcp-apps.config.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 1 remains after this review.


Walkthrough

Adds a DEV-only MCP App view harness with configurable mount mode and doctype handling. Adds a fixture HTTP server and Playwright tests for sandbox URLs, request headers, CSP enforcement, reload behavior, srcdoc fallback, and quirks mode. Adds a dedicated Playwright configuration and npm script for running the suite.

Merge Risk: ⚪ Minimal · up to 28788

This adds browser coverage for MCP App sandbox mounting behavior without changing production runtime behavior. The dedicated development test path and reported passing checks leave no actionable merge-blocking risk.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Internal preview

Preview URL: https://mcp-inspector-pr-4667.up.railway.app
Deployed commit: a92d20a
PR head commit: 2878816
Backend target: staging fallback.
Health: ✅ Convex reachable
Access is employee-only in non-production environments.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 7 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="mcpjam-inspector/playwright.mcp-apps.config.ts">

<violation number="1" location="mcpjam-inspector/playwright.mcp-apps.config.ts:41">
P2: The webServer readiness check only polls the Vite client, which returns index.html for any path as soon as it is listening. The harness actually depends on the Hono server at 6475 (the /api proxy that serves /api/apps/mcp-apps/sandbox-proxy), started concurrently by dev:app:default, so tests can begin before the server is ready and fail on proxied requests. Poll the server or the proxied proxy path for readiness, e.g. a second webServer entry keyed on http://localhost:6475 or a probe of the sandbox-proxy route, so startup is confirmed before tests run.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

webServer: [
{
command: "npm run dev:app:default",
url: "http://localhost:5375/__e2e/mcp-app-view",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The webServer readiness check only polls the Vite client, which returns index.html for any path as soon as it is listening. The harness actually depends on the Hono server at 6475 (the /api proxy that serves /api/apps/mcp-apps/sandbox-proxy), started concurrently by dev:app:default, so tests can begin before the server is ready and fail on proxied requests. Poll the server or the proxied proxy path for readiness, e.g. a second webServer entry keyed on http://localhost:6475 or a probe of the sandbox-proxy route, so startup is confirmed before tests run.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At mcpjam-inspector/playwright.mcp-apps.config.ts, line 41:

<comment>The webServer readiness check only polls the Vite client, which returns index.html for any path as soon as it is listening. The harness actually depends on the Hono server at 6475 (the /api proxy that serves /api/apps/mcp-apps/sandbox-proxy), started concurrently by dev:app:default, so tests can begin before the server is ready and fail on proxied requests. Poll the server or the proxied proxy path for readiness, e.g. a second webServer entry keyed on http://localhost:6475 or a probe of the sandbox-proxy route, so startup is confirmed before tests run.</comment>

<file context>
@@ -0,0 +1,57 @@
+  webServer: [
+    {
+      command: "npm run dev:app:default",
+      url: "http://localhost:5375/__e2e/mcp-app-view",
+      cwd: packageRoot,
+      reuseExistingServer: !process.env.CI,
</file context>

This branch was successfully deployed

1 active deployment
preview-pr-4667 — 28788168 Deployed Sep 3, 2026 by chelojimenez via upsert-preview #17075
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant