| Name | Last commit date | |
|---|---|---|
| .. | ||
| scripts | ||
| tests | ||
| .gitignore | ||
| package.json | ||
| playwright.config.ts | ||
| README.md | ||
MCP Apps tests (sunpeak)
Render and test executor's generative-UI MCP Apps (the create-artifact tool from the
mcp-apps-shell plugin) in a real MCP-Apps host, headlessly, in CI, with no
VM and no Claude/ChatGPT account.
sunpeak locally replicates the Claude
and ChatGPT app runtimes: it connects to an MCP server, calls a UI tool, mounts
the returned ui:// resource in a sandboxed iframe with the host bridge, and
hands the test a frame-scoped handle to the rendered component. Every test runs
against both host simulations. This is the lightweight successor to the earlier
MCPJam Playwright harness (which we drove by hand via fragile UI selectors).
Run
cd e2e/mcp-apps
npm install # also patches sunpeak (see below) and is isolated from the bun workspace
npm test # builds the shell, starts sunpeak + `executor mcp`, runs the specs
playwright.config.ts starts executor mcp over stdio (from source, so the
shell resource is served), so there is no daemon or HTTP token to manage. Specs
live in tests/; npm run test:headed and npm run report help when debugging.
A spec is tiny:
import { test, expect } from "sunpeak/test";
test("widget mounts", async ({ inspector }) => {
const result = await inspector.renderTool("create-artifact", { code: APP_SRC });
const app = result.app().frameLocator("iframe"); // see "extra iframe" below
await expect(app.locator('button:has-text("Increment")')).toBeVisible();
});
Two interop notes (both handled here)
-
sunpeak doesn't advertise the MCP-Apps UI client capability. Its inspector connects with
new Client({ name, version })and never declarescapabilities.extensions["io.modelcontextprotocol/ui"]. executor'screate-artifact(correctly, per the MCP-Apps spec) only mounts the widget inline when the host advertises it can rendertext/html;profile=mcp-app; otherwise it returns its browser fallback URL and nothing renders.scripts/patch-sunpeak.mjs(a postinstall) adds that capability to sunpeak's client. This is upstream-worthy: sunpeak should advertise it itself. -
executor's shell nests one extra iframe. The shell mounts the generated component in a nested
srcdociframe (shell-app->inner-renderer, the double-iframe sandbox), one level below sunpeak'sresult.app(). So tests useresult.app().frameLocator("iframe")to reach the component.
Why stdio + from source
create-artifact's shell resource (ui://executor/shell-tanstack-query.html) is
packages/hosts/mcp-apps-shell/dist/mcp-app.html. pretest builds it. Running
executor mcp from source serves it directly. (The compiled binary now ships
the shell too — see apps/cli/src/build.ts — but source is the cheaper path
for a test.)
Scope
sunpeak is a host simulation: it covers the protocol contract, the rendered
UI, and tool/theme/display-mode behavior. It does not catch real-host quirks
(OAuth, production CSP). For the shell component in isolation there is also the
faster in-repo unit test at
packages/hosts/mcp-apps-shell/src/shell/mcp-app.browser.test.ts.