You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add a pure CSP-builder library at clients/web/src/lib/sandbox-csp.ts: validate app-supplied _meta.ui.csp requests, filter to safe sources, and wrap untrusted widget HTML in a fixed shell so the policy is always the first <head> child.
Why
The inspector plumbs _meta.ui.csp through today but never enforces it — a widget developer gets a misleading "works here". Enforcement needs a typed, unit-testable library: default-src 'none' catch-all, form-action 'none', source-allowlist filtering, attribute escaping. This issue is the standalone library only; wiring into the app bridge is a follow-up issue.
Reviewed clients/web/src/lib/sandbox-csp.ts on branch v2/1558-sandbox-csp (HEAD cb54de5).
Smoke method
sandbox-csp.ts is a pure, dependency-free library (its only import is a import type from @modelcontextprotocol/ext-apps/app-bridge), so no Playwright/DOM is needed. I wrote a standalone tsx script that imports the real exports (buildSandboxCspPolicy, approveCspSources, wrapSandboxedHtml, escapeHtmlAttr, SAFE_CSP_SOURCE) and fed representative app-supplied inputs: valid https/wss sources (incl. a port), a bare *, two injection/breakout attempts, and an img/<script> inner HTML. Real output below.
Real output
approveCspSources — drops the injection/breakout/javascript: sources (each warned), keeps the safe ones incl. the broad-but-syntactically-safe *:
Behaviour matches the documented contract exactly.
AGENTS.md compliance
Mantine / React: N/A — pure non-React library, no components or styling.
TypeScript: Clean. No any, no eslint-disable, no @ts-* suppressions anywhere in the source or test diff. Strongly typed against McpUiResourceCsp. The compile-time exhaustiveCspKeys<const T>() guard is a nice idiom — it forces a never (and a compile error) if the upstream McpUiResourceCsp ever gains a new domain key, so a new grant category can't be silently dropped by approveCspSources. (The one assertion in the test, as unknown as string[], is a deliberate double-assertion to exercise the non-array guard — not any.)
Coverage:sandbox-csp.ts reports 100 / 100 / 100 / 100 (Stmts / Branch / Funcs / Lines) via cd clients/web && npm run test:coverage, well above the ≥90 gate. Confirmed the PR adds src/lib/sandbox-csp.{ts,tsx} to the coverage include in vite.config.ts (listed by file, not a src/lib/** glob, so untested legacy siblings aren't pulled under the gate), so the file is gate-enforced — and the full coverage run exited 0.
Verdict
PASS. Smoke test produces correct, injection-safe output for every representative input; AGENTS.md compliant (Mantine N/A, no any/suppressions, 100% gate-enforced coverage). No violations found.
🔗 Implemented by PR #1588. Its code is already integrated into the Wave 1 rollup branch 1579-wave-1 (see rollup PR #1601 / verification #1600). #1588 currently targets v2/main and will auto-show as Merged once the rollup lands in v2/main. The Development panel doesn't link it now because the closing keyword is on a non-default base branch.
Activity
cliffhall commentedon Jul 2, 2026
Smoke test & AGENTS.md audit — PR #1588
Reviewed
clients/web/src/lib/sandbox-csp.tson branchv2/1558-sandbox-csp(HEADcb54de5).Smoke method
sandbox-csp.tsis a pure, dependency-free library (its only import is aimport typefrom@modelcontextprotocol/ext-apps/app-bridge), so no Playwright/DOM is needed. I wrote a standalonetsxscript that imports the real exports (buildSandboxCspPolicy,approveCspSources,wrapSandboxedHtml,escapeHtmlAttr,SAFE_CSP_SOURCE) and fed representative app-supplied inputs: validhttps/wsssources (incl. a port), a bare*, two injection/breakout attempts, and animg/<script>inner HTML. Real output below.Real output
approveCspSources— drops the injection/breakout/javascript:sources (each warned), keeps the safe ones incl. the broad-but-syntactically-safe*:buildSandboxCspPolicy— generated CSP string (resourceDomainsfeeds script/style/img/font/media; catch-alls locked to'none',base-uri 'self'):wrapSandboxedHtml— CSP<meta>is the literal first<head>child, ahead of the untrusted body bytes:A breakout policy value is attribute-encoded and cannot escape the
content="…"attribute:SAFE_CSP_SOURCEaccept/reject decisions:Behaviour matches the documented contract exactly.
AGENTS.md compliance
any, noeslint-disable, no@ts-*suppressions anywhere in the source or test diff. Strongly typed againstMcpUiResourceCsp. The compile-timeexhaustiveCspKeys<const T>()guard is a nice idiom — it forces anever(and a compile error) if the upstreamMcpUiResourceCspever gains a new domain key, so a new grant category can't be silently dropped byapproveCspSources. (The one assertion in the test,as unknown as string[], is a deliberate double-assertion to exercise the non-array guard — notany.)sandbox-csp.tsreports 100 / 100 / 100 / 100 (Stmts / Branch / Funcs / Lines) viacd clients/web && npm run test:coverage, well above the ≥90 gate. Confirmed the PR addssrc/lib/sandbox-csp.{ts,tsx}to the coverageincludeinvite.config.ts(listed by file, not asrc/lib/**glob, so untested legacy siblings aren't pulled under the gate), so the file is gate-enforced — and the full coverage run exited 0.Verdict
PASS. Smoke test produces correct, injection-safe output for every representative input; AGENTS.md compliant (Mantine N/A, no
any/suppressions, 100% gate-enforced coverage). No violations found.Custom smoke script (tsx, not committed)
cliffhall commentedon Jul 2, 2026
🔗 Implemented by PR #1588. Its code is already integrated into the Wave 1 rollup branch
1579-wave-1(see rollup PR #1601 / verification #1600). #1588 currently targetsv2/mainand will auto-show as Merged once the rollup lands inv2/main. The Development panel doesn't link it now because the closing keyword is on a non-default base branch.Merge pull request #1588 from modelcontextprotocol/v2/1558-sandbox-csp
feat(web): add sandbox CSP builder library (closes modelcontextprotoc…