<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nguyen Hien</title>
    <description>The latest articles on DEV Community by Nguyen Hien (@cptrodgers).</description>
    <link>https://dev.to/cptrodgers</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3859748%2F8e400ae9-0927-4574-af06-6ee2b2271a86.jpg</url>
      <title>DEV Community: Nguyen Hien</title>
      <link>https://dev.to/cptrodgers</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cptrodgers"/>
    <language>en</language>
    <item>
      <title>MCP App CSP Explained: Why Your Widget Won't Render</title>
      <dc:creator>Nguyen Hien</dc:creator>
      <pubDate>Fri, 03 Apr 2026 16:16:39 +0000</pubDate>
      <link>https://dev.to/cptrodgers/mcp-app-csp-explained-why-your-widget-wont-render-9n1</link>
      <guid>https://dev.to/cptrodgers/mcp-app-csp-explained-why-your-widget-wont-render-9n1</guid>
      <description>&lt;p&gt;You built an MCP App. The tool works. The server returns data. But the widget renders as a blank iframe. &lt;/p&gt;

&lt;p&gt;You've hit the &lt;code&gt;#1 problem in MCP App development&lt;/code&gt;: &lt;strong&gt;Content Security Policy&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This post explains exactly how CSP works in MCP Apps, what the three domain arrays do, the mistakes that cause silent failures, and how to debug them. By the end, you'll never stare at a blank widget again.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sandbox model
&lt;/h2&gt;

&lt;p&gt;Every MCP App widget runs inside a sandboxed iframe. On ChatGPT, that iframe lives at a domain like &lt;code&gt;yourapp.web-sandbox.oaiusercontent.com&lt;/code&gt;. On Claude, it's computed from a hash of your server URL. On VS Code, it's host-controlled.&lt;/p&gt;

&lt;p&gt;The sandbox blocks &lt;strong&gt;everything&lt;/strong&gt; by default. No external API calls. No CDN images. No Google Fonts. No WebSocket connections. Nothing leaves the iframe unless you explicitly declare it.&lt;/p&gt;

&lt;p&gt;You declare allowed domains in &lt;code&gt;_meta.ui.csp&lt;/code&gt; on your MCP resource. The host reads this and sets the iframe's Content Security Policy. If a domain isn't declared, the browser blocks the request before it even happens.&lt;/p&gt;

&lt;p&gt;Here's what a declaration looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;_meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;csp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;connectDomains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;resourceDomains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://cdn.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple enough. But the devil is in knowing which array to put each domain in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three domain arrays
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;connectDomains&lt;/code&gt; — runtime connections
&lt;/h3&gt;

&lt;p&gt;Controls: &lt;code&gt;fetch()&lt;/code&gt;, &lt;code&gt;XMLHttpRequest&lt;/code&gt;, &lt;code&gt;WebSocket&lt;/code&gt;, &lt;code&gt;EventSource&lt;/code&gt;, &lt;code&gt;navigator.sendBeacon()&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Maps to the CSP &lt;code&gt;connect-src&lt;/code&gt; directive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use when:&lt;/strong&gt; your widget calls an API at runtime.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This fetch will be BLOCKED unless api.stripe.com is in connectDomains&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;charges&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.stripe.com/v1/charges&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Same for WebSockets&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wss://realtime.example.com/feed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;resourceDomains&lt;/code&gt; — static assets
&lt;/h3&gt;

&lt;p&gt;Controls: &lt;code&gt;&amp;lt;script src&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;link rel="stylesheet"&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;img src&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;audio&amp;gt;&lt;/code&gt;, &lt;code&gt;@font-face&lt;/code&gt;, CSS &lt;code&gt;url()&lt;/code&gt;, &lt;code&gt;@import&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Maps to CSP &lt;code&gt;script-src&lt;/code&gt;, &lt;code&gt;style-src&lt;/code&gt;, &lt;code&gt;img-src&lt;/code&gt;, &lt;code&gt;font-src&lt;/code&gt;, &lt;code&gt;media-src&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use when:&lt;/strong&gt; your widget loads assets from external CDNs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- These will be BLOCKED unless the domains are in resourceDomains --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.example.com/chart.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://fonts.googleapis.com/css2?family=Inter"&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://res.cloudinary.com/demo/image/upload/sample.jpg"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;frameDomains&lt;/code&gt; — nested iframes
&lt;/h3&gt;

&lt;p&gt;Controls: &lt;code&gt;&amp;lt;iframe src&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Maps to CSP &lt;code&gt;frame-src&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use when:&lt;/strong&gt; your widget embeds third-party content like YouTube videos, Google Maps, or Spotify players.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- BLOCKED unless youtube.com is in frameDomains --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;iframe&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://www.youtube.com/embed/abc123"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;frameDomains&lt;/code&gt;, nested iframes are blocked entirely. Note that ChatGPT reviews apps with &lt;code&gt;frameDomains&lt;/code&gt; more strictly — only use it when you actually embed iframes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five mistakes that break your widget
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Using &lt;code&gt;resourceDomains&lt;/code&gt; for API calls
&lt;/h3&gt;

&lt;p&gt;This is the most common mistake. Your widget calls &lt;code&gt;fetch()&lt;/code&gt; to an API, and you put the domain in &lt;code&gt;resourceDomains&lt;/code&gt; because "it's a resource." It isn't — &lt;code&gt;fetch()&lt;/code&gt; is a runtime connection.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Wrong: API domain in resourceDomains&lt;/span&gt;
&lt;span class="nx"&gt;csp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;resourceDomains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Correct: API domain in connectDomains&lt;/span&gt;
&lt;span class="nl"&gt;csp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;connectDomains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The rule:&lt;/strong&gt; if your JavaScript code calls it at runtime, it goes in &lt;code&gt;connectDomains&lt;/code&gt;. If an HTML tag loads it as a static asset, it goes in &lt;code&gt;resourceDomains&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Forgetting the font file domain
&lt;/h3&gt;

&lt;p&gt;Google Fonts is a two-domain system. The CSS is served from &lt;code&gt;fonts.googleapis.com&lt;/code&gt;, but the actual font files (&lt;code&gt;.woff2&lt;/code&gt;) come from &lt;code&gt;fonts.gstatic.com&lt;/code&gt;. If you only declare the first, the CSS loads but the fonts don't.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Wrong: CSS loads, fonts don't&lt;/span&gt;
&lt;span class="nx"&gt;csp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;resourceDomains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://fonts.googleapis.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Correct: both domains declared&lt;/span&gt;
&lt;span class="nl"&gt;csp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;resourceDomains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://fonts.googleapis.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://fonts.gstatic.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your widget will render with fallback system fonts — a subtle visual bug that's easy to miss during development but obvious to users.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Missing the WebSocket protocol
&lt;/h3&gt;

&lt;p&gt;WebSocket connections use &lt;code&gt;wss://&lt;/code&gt;, not &lt;code&gt;https://&lt;/code&gt;. If you declare the HTTPS version, the WebSocket connection still fails.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Wrong: wss:// connections are still blocked&lt;/span&gt;
&lt;span class="nx"&gt;csp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;connectDomains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://realtime.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Correct: use the wss:// scheme&lt;/span&gt;
&lt;span class="nl"&gt;csp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;connectDomains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wss://realtime.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Also correct: declare both if you use both&lt;/span&gt;
&lt;span class="nl"&gt;csp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;connectDomains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wss://realtime.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Services that need both arrays
&lt;/h3&gt;

&lt;p&gt;Some services serve both static assets AND API responses from the same or related domains. Mapbox is a classic example — it serves API responses (tile coordinates) and image tiles (actual map pictures) from the same origins.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Wrong: only connect, map tiles don't render&lt;/span&gt;
&lt;span class="nx"&gt;csp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;connectDomains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.mapbox.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Correct: both connect and resource&lt;/span&gt;
&lt;span class="nl"&gt;csp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;connectDomains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.mapbox.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;resourceDomains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.mapbox.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Other services that commonly need both: Cloudinary (API + image CDN), Firebase (API + hosting), Supabase (API + storage).&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Works in dev, breaks when published
&lt;/h3&gt;

&lt;p&gt;ChatGPT has a more relaxed CSP in developer mode. When you publish your app, stricter rules apply. Two things that catch people:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Missing &lt;code&gt;_meta.ui.domain&lt;/code&gt;.&lt;/strong&gt; Developer mode works without it. Published mode requires it — this is the domain ChatGPT uses to scope your widget's sandbox origin.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;_meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://myapp.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// required for published apps&lt;/span&gt;
    &lt;span class="na"&gt;csp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Missing &lt;code&gt;openai/widgetCSP&lt;/code&gt;.&lt;/strong&gt; Some published apps need the ChatGPT-specific CSP format alongside the standard &lt;code&gt;_meta.ui.csp&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;_meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;csp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;connectDomains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;resourceDomains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://cdn.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// ChatGPT compatibility layer&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;openai/widgetCSP&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;connect_domains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nx"&gt;resource_domains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://cdn.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the naming difference: &lt;code&gt;connectDomains&lt;/code&gt; (camelCase) in the standard spec vs &lt;code&gt;connect_domains&lt;/code&gt; (snake_case) in the ChatGPT extension.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to debug CSP violations
&lt;/h2&gt;

&lt;p&gt;When CSP blocks a request, the browser logs it to the console. Here's how to find it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open DevTools (&lt;code&gt;F12&lt;/code&gt; or &lt;code&gt;Cmd+Opt+I&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Go to the &lt;strong&gt;Console&lt;/strong&gt; tab&lt;/li&gt;
&lt;li&gt;Look for red errors starting with &lt;code&gt;Refused to&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The error message tells you exactly what was blocked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Refused to connect to 'https://api.example.com/data'
because it violates the following Content Security Policy directive:
"connect-src 'self'"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What was blocked:&lt;/strong&gt; &lt;code&gt;https://api.example.com/data&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Which directive:&lt;/strong&gt; &lt;code&gt;connect-src&lt;/code&gt; — you need &lt;code&gt;connectDomains&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Current policy:&lt;/strong&gt; only &lt;code&gt;'self'&lt;/code&gt; is allowed — the domain isn't declared&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For font issues:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Refused to load the font 'https://fonts.gstatic.com/s/inter/...'
because it violates the following Content Security Policy directive:
"font-src 'self'"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means &lt;code&gt;fonts.gstatic.com&lt;/code&gt; needs to be in &lt;code&gt;resourceDomains&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Debugging checklist
&lt;/h3&gt;

&lt;p&gt;When your widget is blank or partially broken:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open DevTools Console — look for &lt;code&gt;Refused to&lt;/code&gt; errors&lt;/li&gt;
&lt;li&gt;For each error, identify the directive (&lt;code&gt;connect-src&lt;/code&gt;, &lt;code&gt;font-src&lt;/code&gt;, &lt;code&gt;script-src&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;Map the directive to the right array:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;connect-src&lt;/code&gt; → &lt;strong&gt;connectDomains&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;script-src&lt;/code&gt;, &lt;code&gt;style-src&lt;/code&gt;, &lt;code&gt;img-src&lt;/code&gt;, &lt;code&gt;font-src&lt;/code&gt;, &lt;code&gt;media-src&lt;/code&gt; → &lt;strong&gt;resourceDomains&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;frame-src&lt;/code&gt; → &lt;strong&gt;frameDomains&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Add the blocked domain to the correct array&lt;/li&gt;
&lt;li&gt;Restart your MCP server and test again&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Copy-paste patterns
&lt;/h2&gt;

&lt;p&gt;Here are CSP declarations for common use cases:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API calls only:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;csp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;connectDomains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.yourbackend.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;CDN images:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;csp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;resourceDomains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://cdn.yourbackend.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Google Fonts:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;csp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;resourceDomains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://fonts.googleapis.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://fonts.gstatic.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Full stack — API + CDN + Fonts:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;csp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;connectDomains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.yourbackend.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nx"&gt;resourceDomains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://cdn.yourbackend.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://fonts.googleapis.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://fonts.gstatic.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Mapbox maps:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;csp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;connectDomains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.mapbox.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://events.mapbox.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nx"&gt;resourceDomains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.mapbox.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://cdn.mapbox.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Embedded YouTube:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;csp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;frameDomains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.youtube.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Other sandbox restrictions
&lt;/h2&gt;

&lt;p&gt;CSP isn't the only thing the sandbox blocks. These browser APIs are also restricted inside MCP App iframes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;localStorage&lt;/code&gt; / &lt;code&gt;sessionStorage&lt;/code&gt;&lt;/strong&gt; — may throw &lt;code&gt;SecurityError&lt;/code&gt;. Use in-memory state instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;eval()&lt;/code&gt; / &lt;code&gt;new Function()&lt;/code&gt;&lt;/strong&gt; — blocked by default. Some charting libraries use &lt;code&gt;eval()&lt;/code&gt; internally — check before picking a dependency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;window.open()&lt;/code&gt;&lt;/strong&gt; — blocked. Use the MCP Apps bridge for navigation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;document.cookie&lt;/code&gt;&lt;/strong&gt; — no cookies in sandboxed iframes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;navigator.clipboard&lt;/code&gt;&lt;/strong&gt; — blocked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;alert()&lt;/code&gt; / &lt;code&gt;confirm()&lt;/code&gt; / &lt;code&gt;prompt()&lt;/code&gt;&lt;/strong&gt; — blocked.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your widget depends on any of these, it will fail silently even if your CSP is perfect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Platform differences
&lt;/h2&gt;

&lt;p&gt;The MCP Apps spec is standard, but each host implements it differently:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;CSP source&lt;/th&gt;
&lt;th&gt;Widget domain&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ChatGPT&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;_meta.ui.csp&lt;/code&gt; + &lt;code&gt;openai/widgetCSP&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{domain}.web-sandbox.oaiusercontent.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Requires &lt;code&gt;_meta.ui.domain&lt;/code&gt; for published apps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Claude&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_meta.ui.csp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SHA-256 of MCP server URL&lt;/td&gt;
&lt;td&gt;Own sandbox model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;VS Code&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_meta.ui.csp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Host-controlled&lt;/td&gt;
&lt;td&gt;Had bugs with &lt;code&gt;resourceDomains&lt;/code&gt; mapping in older versions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you're building for multiple platforms, test on each. A widget that works on ChatGPT might fail on Claude or VS Code due to these subtle differences.&lt;/p&gt;

&lt;h2&gt;
  
  
  Skip the debugging entirely
&lt;/h2&gt;

&lt;p&gt;Getting CSP right by hand is tedious. Every time you add a new external dependency — a font, an analytics script, an API endpoint — you need to update &lt;code&gt;_meta.ui.csp&lt;/code&gt; and hope you picked the right array.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/cptrodgers/mcpr" rel="noopener noreferrer"&gt;&lt;strong&gt;MCPR&lt;/strong&gt;&lt;/a&gt; is an open-source MCP proxy that handles this for you. It sits between the AI client and your MCP server, reads your &lt;code&gt;_meta.ui.csp&lt;/code&gt; declarations, and injects the correct CSP headers automatically — so you declare once and it works across ChatGPT, Claude, and VS Code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo &lt;span class="nb"&gt;install &lt;/span&gt;mcpr
mcpr &lt;span class="nt"&gt;--config&lt;/span&gt; mcpr.toml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you don't want to self-host, &lt;a href="https://cloud.mcpr.app" rel="noopener noreferrer"&gt;&lt;strong&gt;MCPR Cloud&lt;/strong&gt;&lt;/a&gt; gives you a managed tunnel with a free subdomain. Claim yours at &lt;code&gt;cloud.mcpr.app&lt;/code&gt; and start proxying in minutes — CSP handled, auth included, every tool call observable.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/cptrodgers/mcpr" rel="noopener noreferrer"&gt;Star us on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/getting-started/quickstart"&gt;Get started with MCPR&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;connectDomains&lt;/code&gt; = &lt;code&gt;fetch()&lt;/code&gt;, WebSocket, XHR (runtime connections)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;resourceDomains&lt;/code&gt; = images, fonts, scripts, stylesheets (static assets)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;frameDomains&lt;/code&gt; = nested iframes (use sparingly)&lt;/li&gt;
&lt;li&gt;Debug with DevTools Console — look for "Refused to" errors&lt;/li&gt;
&lt;li&gt;Google Fonts needs both &lt;code&gt;fonts.googleapis.com&lt;/code&gt; AND &lt;code&gt;fonts.gstatic.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Test on every platform you ship to — they're all slightly different&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>mcp</category>
      <category>csp</category>
      <category>chatgpt</category>
    </item>
  </channel>
</rss>
