Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Comprehensive Content Security Policy audit for MCP Apps in sandboxed iframes. Discovers all network origins, traces them to source, and generates CSP configuration for registerAppResource.
.claude/skills/a5c-ai-mcp-csp-investigation/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-03 | ✗→✓ | ▲ Improved | 1224% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 63% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 89% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 64% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 244% | 0% |
Perform exhaustive Content Security Policy audits for MCP Apps running in sandboxed iframes where all network requests fail SILENTLY without proper CSP declarations.
MCP Apps run in sandboxed iframes with no same-origin server. This means:
contents[] return from the registerAppResource read callbackThis skill provides a systematic methodology for discovering every network origin an app uses, tracing each to its source, and generating the correct CSP configuration.
script-src, style-src, img-src, font-src)connect-src)frame-src)registerAppResource read callbackbash# Build to produce final output files npm run build # Identify all output files find dist/ -type f \( -name "*.html" -o -name "*.js" -o -name "*.css" \)
bash# Search for URL patterns in build output grep -rEoh 'https?://[a-zA-Z0-9._-]+[a-zA-Z0-9._/-]*' dist/ | sort -u # Search for protocol-relative URLs grep -rEoh '//[a-zA-Z0-9._-]+\.[a-zA-Z]{2,}' dist/ | sort -u # Search for fetch/XHR patterns in source grep -rn 'fetch\|XMLHttpRequest\|axios\|\.get\(\|\.post\(' src/ # Search for WebSocket connections grep -rn 'new WebSocket\|wss://\|ws://' src/ # Search for dynamic imports and lazy loading grep -rn 'import(\|require(\|loadScript' src/
For each discovered origin, determine:
| Origin | Source Type | Environment | Category | |--------|-----------|-------------|----------| | https://cdn.example.com | Hardcoded constant | Universal | resourceDomains | | https://api.example.com | Environment variable (API_URL) | Conditional | connectDomains | | https://fonts.googleapis.com | Third-party library | Universal | resourceDomains | | wss://realtime.example.com | Conditional (feature flag) | Prod-only | connectDomains |
bash# Check node_modules for hidden network requests grep -rn 'fetch\|XMLHttpRequest\|beacon\|sendBeacon' node_modules/<lib>/dist/ 2>/dev/null # Common hidden request sources: # - Analytics (Google Analytics, Segment, Mixpanel) # - Error tracking (Sentry, Bugsnag, Datadog) # - Font loading (Google Fonts, Adobe Fonts) # - CDN fallbacks (jQuery CDN, unpkg) # - Map tiles (Mapbox, Google Maps, Leaflet)
The CSP object goes in the contents[] return from registerAppResource:
typescriptimport { registerAppResource, RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps'; registerAppResource(server, { uri: 'app:///my-app', name: 'My App', mimeType: RESOURCE_MIME_TYPE, async read() { return { contents: [{ uri: 'app:///my-app', mimeType: RESOURCE_MIME_TYPE, text: bundledHtml, // CSP configuration goes HERE, in contents[] resourceDomains: [ 'https://cdn.example.com', 'https://fonts.googleapis.com', 'https://fonts.gstatic.com', ], connectDomains: [ 'https://api.example.com', ...(process.env.NODE_ENV === 'development' ? ['http://localhost:3000'] : []), ], frameDomains: [], }], }; }, });
For every origin that depends on configuration or environment:
typescript// WRONG: Runtime URL uses config but CSP doesn't include it const apiUrl = process.env.API_URL || 'https://api.example.com'; fetch(apiUrl); // Works for default, fails for custom API_URL // RIGHT: Same config controls both runtime URL and CSP entry const apiUrl = process.env.API_URL || 'https://api.example.com'; // In registerAppResource: connectDomains: [apiUrl], // CSP matches runtime URL
contents[] from registerAppResource read callback, NOT in _meta on the tool.http://localhost:3000 needs CSP in dev mode.API_URL can be customized, the CSP must include whatever value it resolves to.resourceDomains, connectDomains, frameDomains), NOT snake_case.contents[] of registerAppResource read callbackjavascriptconst mcpCspInvestigationTask = defineTask({ name: 'mcp-csp-investigation', description: 'Perform CSP audit for MCP App in sandboxed iframe', inputs: { projectDir: { type: 'string', required: true }, buildCommand: { type: 'string', default: 'npm run build' }, outputDir: { type: 'string', default: 'dist' } }, outputs: { resourceDomains: { type: 'array' }, connectDomains: { type: 'array' }, frameDomains: { type: 'array' }, conditionalOrigins: { type: 'array' }, artifacts: { type: 'array' } }, async run(inputs, taskCtx) { return { kind: 'skill', title: `CSP investigation for ${inputs.projectDir}`, skill: { name: 'mcp-csp-investigation', context: { projectDir: inputs.projectDir, buildCommand: inputs.buildCommand, outputDir: inputs.outputDir, instructions: [ 'Build the application', 'Search all output for network origins', 'Trace each origin to source', 'Check third-party libraries for hidden requests', 'Categorize domains (resource, connect, frame)', 'Generate CSP configuration for registerAppResource', 'Verify conditional origins have matching CSP entries' ] } }, io: { inputJsonPath: `tasks/${taskCtx.effectId}/input.json`, outputJsonPath: `tasks/${taskCtx.effectId}/result.json` } }; } });
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 12,761 | 12,237 | -4% | 1 | 1 | 0% | 201 | 2,710 | +1248% | 0 | 0 | — |
case-02 | fail→fail | 3,606 | 5,140 | +43% | 1 | 1 | 0% | 551 | 2,678 | +386% | 0 | 0 | — |
case-03 | fail→pass | 8,916 | 10,927 | +23% | 1 | 1 | 0% | 346 | 4,582 | +1224% | 0 | 0 | — |
case-04 | fail→pass | 12,235 | 4,652 | -62% | 1 | 1 | 0% | 2,013 | 3,276 | +63% | 0 | 0 | — |
case-05 | fail→pass | 9,918 | 4,121 | -58% | 1 | 1 | 0% | 1,677 | 3,165 | +89% | 0 | 0 | — |
case-06 | fail→pass | 12,164 | 5,641 | -54% | 1 | 1 | 0% | 2,127 | 3,493 | +64% | 0 | 0 | — |
case-07 | fail→pass | 5,151 | 5,060 | -2% | 1 | 1 | 0% | 964 | 3,316 | +244% | 0 | 0 | — |
case-08 | fail→pass | 5,153 | 3,288 | -36% | 1 | 1 | 0% | 1,117 | 2,975 | +166% | 0 | 0 | — |
case-09 | fail→pass | 9,879 | 5,768 | -42% | 1 | 1 | 0% | 1,787 | 3,408 | +91% | 0 | 0 | — |
case-10 | fail→fail | 9,234 | 5,742 | -38% | 1 | 1 | 0% | 1,687 | 3,486 | +107% | 0 | 0 | — |
case-11 | pass→pass | 13,585 | 10,149 | -25% | 1 | 1 | 0% | 2,324 | 4,195 | +81% | 0 | 0 | — |
case-12 | fail→fail | 19,467 | 8,403 | -57% | 1 | 1 | 0% | 2,006 | 3,739 | +86% | 0 | 0 | — |
case-13 | fail→fail | 13,701 | 8,965 | -35% | 1 | 1 | 0% | 2,671 | 3,672 | +37% | 0 | 0 | — |
case-14 | fail→pass | 4,003 | 3,012 | -25% | 1 | 1 | 0% | 674 | 2,889 | +329% | 0 | 0 | — |
case-15 | fail→fail | 14,873 | 6,836 | -54% | 1 | 1 | 0% | 2,505 | 3,652 | +46% | 0 | 0 | — |
case-16 | fail→fail | 11,312 | 5,114 | -55% | 1 | 1 | 0% | 1,892 | 3,284 | +74% | 0 | 0 | — |
case-17 | fail→fail | 14,517 | 5,514 | -62% | 1 | 1 | 0% | 2,770 | 3,484 | +26% | 0 | 0 | — |
case-18 | fail→pass | 10,221 | 5,250 | -49% | 1 | 1 | 0% | 1,683 | 3,395 | +102% | 0 | 0 | — |
case-19 | fail→pass | 14,005 | 8,394 | -40% | 1 | 1 | 0% | 2,530 | 3,834 | +52% | 0 | 0 | — |
case-20 | fail→fail | 10,415 | 6,914 | -34% | 1 | 1 | 0% | 2,071 | 3,765 | +82% | 0 | 0 | — |
case-21 | fail→fail | 8,173 | 11,078 | +36% | 1 | 1 | 0% | 1,781 | 4,163 | +134% | 0 | 0 | — |
case-22 | fail→fail | 13,402 | 10,790 | -19% | 1 | 1 | 0% | 2,877 | 4,537 | +58% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 19 counted toward the lift figure. The other 3 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +45 percentage points is the difference between those two pass rates over the 19 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.