mcpbeat Sign in

Canvas Webapp Agent Skill

>- Render a React/Vite (or any bundled) web app on the pi-dashboard canvas, which loads loopback URLs in a sandboxed opaque-origin iframe. Use when a surface, or a /live/<id> 500 ECONNREFUSED. Covers why Vite dev servers and non-CORS static servers fail there, and the static-build + CORS-server recipe that works.

1k tokens
context cost
the whole folder, loaded on every use
1
files
instructions only
0
copies elsewhere
how many repositories repackaged it
254
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/BlackBeltTechnology/pi-agent-dashboard --skill canvas-webapp

What it tells the agent to use

found in the instruction text
Bash runs shell commands — read the instruction before connecting

The instruction itself

6 sections, as written by the author

canvas-webapp — show a web app on the dashboard canvas

The dashboard opens a loopback canvas(kind:"url") target inside a

sandbox="allow-scripts" iframe with no allow-same-origin (opaque origin),

proxied under /live/<id>/ (LiveServerViewer.tsxlive-server-proxy). That

sandbox breaks two common serving strategies; this skill is the fix.

When to Use

Use when you must display a running web app / React / Vite / MUI mockup on the

pi-dashboard canvas via canvas(target:{kind:"url"|"server"}) and it shows up

blank white, an empty surface, or a /live/<id> 500 ECONNREFUSED.

Procedure

  • Do NOT point the canvas at a Vite DEV server. The dashboard proxies

loopback targets under /live/<id>/, but Vite dev emits ABSOLUTE asset paths

(/main.tsx, /@vite/client, and runtime fetches like /__schema.json) that

resolve against the dashboard root, not the proxy prefix → 404 → blank page.

  • Produce a STATIC production build with a RELATIVE base: a vite config with

base:'./', a dedicated index.html entry, and the runtime data

(schema/props) IMPORTED statically (no fetch of an absolute path). Run

npx vite build --config <config>.

  • Verify the built index.html references ./assets/... (relative). Copy

the entry html to index.html so the proxy root (/live/<id>/) serves it.

  • **Serve the dist with a tiny node static server that sets

Access-Control-Allow-Origin: *** (and `Cross-Origin-Resource-Policy:

cross-origin`) on every response. This is REQUIRED: the opaque-origin iframe

fetches <script type=module> in CORS mode with Origin: null, so without

ACAO:* the module is blocked → blank white even though the build is correct. A

plain python3 -m http.server does NOT set CORS and renders blank.

  • Point the canvas: `canvas(target:{kind:'url',

url:'http://127.0.0.1:<port>/'}, mode:'replace')`. Loopback is required (SSRF

gate); 127.0.0.1 is safest.

  • Self-verify by iframing your own harness before touching the canvas:

serve an HTML with `<iframe sandbox="allow-scripts allow-forms allow-popups"

src="http://127.0.0.1:<port>/">` on another port, open it in the browser tool,

and screenshot — this reproduces the exact dashboard sandbox.

Minimal CORS static server

// canvas-serve.mjs  —  node canvas-serve.mjs <port>   (serves ./canvas-dist/)
import { createServer } from "node:http";
import { readFile, stat } from "node:fs/promises";
import { extname, join, normalize } from "node:path";
const ROOT = new URL("./canvas-dist/", import.meta.url).pathname;
const PORT = Number(process.argv[2] ?? 5181);
const MIME = { ".html":"text/html;charset=utf-8", ".js":"text/javascript;charset=utf-8",
  ".css":"text/css;charset=utf-8", ".json":"application/json", ".woff":"font/woff",
  ".woff2":"font/woff2", ".svg":"image/svg+xml", ".png":"image/png", ".ico":"image/x-icon" };
createServer(async (req, res) => {
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
  let p = decodeURIComponent((req.url ?? "/").split("?")[0]);
  if (p.endsWith("/")) p += "index.html";
  const full = normalize(join(ROOT, p));
  if (!full.startsWith(ROOT)) { res.statusCode = 403; return res.end("forbidden"); }
  const s = await stat(full).catch(() => null);
  if (!s?.isFile()) { res.statusCode = 404; return res.end("not found"); }
  res.setHeader("Content-Type", MIME[extname(full)] ?? "application/octet-stream");
  res.end(await readFile(full));
}).listen(PORT, "127.0.0.1", () => console.log(`CORS static on http://127.0.0.1:${PORT}/`));

Pitfalls

  • bash kill %1 job control does NOT carry across separate Bash tool calls — a

dev server started in one call cannot be killed by %1 in another. Kill stale

servers by PID: lsof -tiTCP:<port> -sTCP:LISTEN | xargs kill -9.

  • Vite dev may bind IPv6 [::1]:<port> only, while the dashboard proxy dials

IPv4 127.0.0.1:<port> → the /live/<id> route returns 500

FST_REPLY_FROM_INTERNAL_SERVER_ERROR 'connect ECONNREFUSED 127.0.0.1:<port>'.

Another reason to avoid dev servers and bind static servers explicitly to

127.0.0.1.

  • canvas(target:{kind:'server', port}) produces a tap-to-open CHIP on desktop,

not an auto-opened view; kind:'url' with a loopback URL auto-opens via

openLiveTarget. Prefer kind:'url'.

  • The build succeeds and renders fine in a NORMAL browser tab yet is blank in the

dashboard — that difference is the sandbox/CORS issue, not a build bug. Don't

chase the build.

  • Non-loopback/remote URLs are refused by the dashboard SSRF gate

(validateLiveTarget).

Verification

  • The static server responds 200 with header Access-Control-Allow-Origin: *

(curl -D - -o /dev/null).

  • An <iframe sandbox="allow-scripts"> pointed at the server renders the app

(browser-tool screenshot), matching the dashboard's opaque-origin sandbox.

  • After canvas(kind:'url'), the user confirms the app is visible on the canvas

(not blank, no 500).

How to use it

Copy the folder

Take blackbelttechnology/canvas-webapp from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.

Install what it needs

The instructions reference npx. Without those the skill loads but fails at the first command.