# MCP App Audit

> Audit and rewrite existing MCP Apps for host compatibility. Review apps for CSP violations, external CDN dependencies, eval usage, blocked permission APIs, and provide specific rewrite patterns. WHEN: 'audit my MCP App', 'review MCP app compatibility', 'why does my app break in VS Code', 'rewrite for VS Code', 'fix CSP errors', 'make my MCP app work everywhere', 'check host compatibility'.

- Skill: `dc995/mcp-app-audit` (Agent Skill)
- Install (CLI): `npx skillmds@latest add dc995/mcp-app-audit`
- Raw SKILL.md: https://api.skillmd.com/api/skills/dc995/mcp-app-audit/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: dc995 (https://skillmd.com/u/dc995)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/dc995/mcp-app-audit

---


# MCP App Audit

Review existing MCP Apps for host compatibility issues and get specific rewrite guidance.

## When to Use

- You have an MCP App that works in one host but not another
- You're evaluating whether a third-party example app will work in VS Code
- You want to make an existing app multi-host compatible
- You're reviewing upstream ext-apps examples before adopting them

## Audit Process

### Step 1: Scan for Red Flags

Check these files in the app:

**`mcp-app.html`** — look for:
- [ ] `<script src="https://...">` — external CDN scripts (**BLOCKED** in VS Code)
- [ ] `<link href="https://...">` — external CDN stylesheets (**BLOCKED** in VS Code)
- [ ] `<iframe src="https://...">` — nested iframes (**BLOCKED** in VS Code)

**`src/mcp-app.ts` (or .tsx)** — look for:
- [ ] `eval(...)` — dynamic code execution (**BLOCKED** in VS Code)
- [ ] `new Function(...)` — dynamic code execution (**BLOCKED** in VS Code)
- [ ] `setTimeout("string", ...)` — string-form setTimeout (**BLOCKED** in VS Code)
- [ ] `fetch("https://external-domain...")` — external API calls (**BLOCKED** in VS Code)
- [ ] `navigator.mediaDevices.getUserMedia` — microphone/camera (**BLOCKED** in VS Code)
- [ ] `navigator.geolocation` — geolocation (**BLOCKED** in VS Code)
- [ ] `new SpeechRecognition()` — Web Speech API (**BLOCKED** in VS Code)

**`package.json`** — look for:
- [ ] SDKs whose canonical install is a CDN `<script>` tag (hosted map/geo SDKs, vendor visualization SDKs) — they won't load under CSP, and many also fetch assets from vendor domains at runtime
- [ ] Libraries known to compile strings to code internally (MVVM/data-binding or eval-based template engines)

**`server.ts`** — look for:
- [ ] `_meta.ui.csp.connectDomains` — declared but VS Code ignores these
- [ ] Missing `RESOURCE_MIME_TYPE` on resource registration
- [ ] Tools without text content fallback for non-UI hosts
- [ ] New code adopts deprecated `sampling/createMessage`, Roots, or MCP Logging
  instead of the modern alternatives in `mcp-app-build/mcp-v2.md`.
- [ ] A legacy `server.server.createMessage(...)` / `elicitInput(...)` /
  `resources/subscribe` path uses a stateless v1 transport
  (`sessionIdGenerator: undefined`) — the reverse request times out (`-32001`).
  Keep the stateful transport inside the legacy adapter and ship a Display-Frame
  fallback. This rule does not apply to modern MRTR.
- [ ] Streamable HTTP without Origin validation, authentication or request/session limits
- [ ] Server proxy tools accepting arbitrary URLs (SSRF)
- [ ] OAuth callbacks without single-use `state` + PKCE bound to the initiating session

**Host/UI bridge** — look for:
- [ ] Same-origin `srcdoc` combined with `allow-scripts allow-same-origin`
- [ ] `postMessage` handlers that do not verify `event.source` and validate JSON-RPC
- [ ] Tool/model content assigned to `innerHTML` without sanitization
- [ ] Untrusted tool/UI text passed to model context as instructions
- [ ] MCP App UI calls a sampling tool, but VS Code workspace settings lack an
  exact `chat.mcp.serverSampling` entry with `allowedOutsideChat: true`
- [ ] Cancellation-shaped sampling result is treated as success or overwrites
  user/session state

### Step 2: Classify Each Finding

| Finding | Category | Severity |
|---|---|---|
| External `<script src>` | [CDN] | Breaking in VS Code |
| External `fetch()` from UI | [NETWORK] | Breaking in VS Code |
| `eval()` / `new Function()` | [CSP] | Breaking in VS Code |
| `getUserMedia` / geolocation | [PERMISSION] | Breaking in VS Code |
| Library uses eval internally | [RENDERING] | Breaking in VS Code |
| No text fallback in tool result | [PROTOCOL] | Degraded in non-UI hosts |
| CSP relaxation in `_meta` | [CSP] | Cosmetic (VS Code ignores it) |
| Legacy sampling/elicitation on stateless v1 transport | [TRANSPORT] | Breaking — server→client request times out (-32001) |
| Modern request requires hidden session state | [PROTOCOL] | Breaking — use MRTR or an explicit application handle |
| VS Code sampling capability without per-server authorization | [PERMISSION] | Degraded — host resolves sampling as cancelled |
| Sampling cancellation applied as model output | [PROTOCOL] | Breaking — corrupts user/session state |
| Same-origin active iframe content | [SECURITY] | Critical — host-origin compromise |
| Arbitrary server-side URL fetch | [SECURITY] | Critical — SSRF/data exfiltration |
| Missing Origin/session controls | [SECURITY] | Critical for network-reachable servers |

### Step 3: Apply Rewrite Patterns

## Rewrite Patterns

### Pattern A: CDN Script → npm Bundle

**Before** (broken in VS Code):
```html
<!-- mcp-app.html -->
<script src="https://cdn.vendor.example/sdk/v3/sdk.min.js"></script>
```

**After** (compatible with the validated host set):
```bash
npm install the-library  # an npm-bundleable equivalent
```
```typescript
// src/mcp-app.ts
import { thing } from "the-library";
```
```typescript
// vite.config.ts — vite-plugin-singlefile bundles everything into one HTML
```

**Key**: the library must be fully functional when npm-installed. SDKs that fetch assets from vendor domains at runtime (map tiles, fonts, sprites, style JSON) cannot be fixed this way — find an alternative SDK or route those fetches through the server.

### Pattern B: External Fetch → Server Proxy

**Before** (broken in VS Code):
```typescript
// src/mcp-app.ts
const data = await fetch("https://api.example.com/search?q=" + query);
```

**After** (compatible with the validated host set):
```typescript
// server.ts — add app-only tool
registerAppTool(server, "search", {
  inputSchema: { query: z.string() },
  _meta: { ui: { resourceUri, visibility: ["app"] } },
}, async ({ query }) => {
  const resp = await fetch(`https://api.example.com/search?q=${encodeURIComponent(query)}`, {
    signal: AbortSignal.timeout(10_000),
    redirect: "error",
  });
  if (!resp.ok) throw new Error(`Upstream search failed: ${resp.status}`);
  return {
    content: [{
      type: "text",
      text: JSON.stringify(await readJsonWithLimit(resp, 1_000_000)),
    }],
  };
});

// src/mcp-app.ts
const result = await app.callServerTool({ name: "search", arguments: { query } });
```

For user-selected destinations, add the SSRF controls from
`mcp-app-security/server-security.md`; never substitute `fetch(userUrl)`.

### Pattern C: eval/new Function → Data-Driven Rendering

**Before** (broken in VS Code):
```typescript
// Model sends code string
app.ontoolinput = (params) => {
  const fn = new Function("scene", "camera", params.code);
  fn(scene, camera);
};
```

**After** (compatible with the validated host set):
```typescript
// Define structured input schema
inputSchema: {
  chart: z.object({
    type: z.enum(["bar", "pie", "scatter"]),
    data: z.array(z.object({ name: z.string(), value: z.number() })),
  }),
}

// Pre-built renderers
const RENDERERS = { bar: renderBarChart, pie: renderPieChart, scatter: renderScatter };

app.ontoolinput = (params) => {
  const renderer = RENDERERS[params.chart.type];
  renderer(canvas, params.chart.data);
};
```

### Pattern D: Permission API → Fallback UI

**Before** (broken in VS Code):
```typescript
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
startTranscription(stream);
```

**After** (compatible with the validated host set):
```typescript
const caps = app.getHostCapabilities();
if (caps?.sandbox?.permissions?.microphone) {
  try {
    const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
    startTranscription(stream);
  } catch (error) {
    logPermissionFallback(error);
    showTextInput();
  }
} else {
  showTextInput(); // Fallback for VS Code
}
```

### Pattern E: Third-Party Library Eval → Disable Features

**Before** (CesiumJS with Knockout widgets — breaks in VS Code):
```typescript
const viewer = new Cesium.Viewer("cesiumContainer");
```

**After** (disable Knockout-dependent widgets):
```typescript
const viewer = new Cesium.Viewer("cesiumContainer", {
  selectionIndicator: false,  // Knockout → new Function() → CSP violation
  infoBox: false,             // Knockout → new Function() → CSP violation
  geocoder: false,            // Ion-dependent + Knockout
});
```

## SDK Compatibility Reference

Reason about the **mechanism**, not the brand. Classify any library by how it
loads and renders, then apply the matching verdict.

| Library mechanism | VS Code | Issue | Fix |
|---|---|---|---|
| Loads from a vendor CDN `<script>` + fetches assets (tiles/fonts/styles) from vendor domains at runtime | **No** | CDN script + runtime `connect-src` both blocked | Use an npm-bundleable equivalent, or proxy runtime fetches through the server |
| GL/worker renderer that compiles styles/shaders via eval or blob-eval workers | **No** | eval blocked under CSP | Use a non-eval renderer |
| MVVM/data-binding lib that compiles binding strings via `new Function()` | **No** | string-to-code blocked | Lit, Preact, or vanilla |
| Framework with eval-based template compilation | **No** | string-to-code blocked | A precompiled/AOT framework, React, or vanilla |
| Large engine that bundles eval-using widgets but has a non-eval core | **Partial** | Only the widgets violate CSP | Disable the eval-using widgets (see Pattern E) |
| npm-bundleable renderer using canvas/WebGL/SVG, no runtime vendor fetch | **Yes** | — | — |
| Map/charting lib whose tiles/images load as `<img>` and that bundles cleanly | **Yes** | `img-src` is permissive | — |
| Data-driven 3D / charting (structured input → pre-built renderer) | **Yes** | No eval | — |
| Web-component / view libs that template without eval | **Yes** | — | — |
| Preact | **Yes** | — | — |
| Solid | **Yes** | — | — |

## Output

After audit, produce:
1. **Compatibility report**: red/yellow/green per finding
2. **Rewrite plan**: specific patterns to apply for each red finding
3. **Host support declaration**: which hosts the app can target after rewrites
4. **HIT entry**: if a new constraint was discovered, record via HIT process
5. **Security report**: invoke `mcp-app-security` for host, transport, auth,
   external-fetch or model-context findings

