different-ai/openwork · error · McpAppHostError

invalid_resource_csp

invalid_resource_csp

Error message

MCP App CSP domain lists must contain at most 16 origins.

What it means

domainList parses a CSP domain array from an MCP App's resource _meta and enforces a hard cap of 16 origins. If the value is not an array or exceeds 16 entries, the host throws invalid_resource_csp to bound the size of content-security-policy allow-lists it will render.

Source

Thrown at apps/server/src/mcp-app-host.ts:114

  try {
    const url = new URL(value);
    if (url.username || url.password || url.pathname !== "/" || url.search || url.hash) return null;
    if (url.protocol === "https:") return url.origin;
    if (url.protocol === "http:" && isLoopbackHostname(url.hostname)) return url.origin;
  } catch {
    return null;
  }
  return null;
}

function isLoopbackHostname(hostname: string): boolean {
  return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "[::1]";
}

function domainList(value: unknown): string[] {
  if (!Array.isArray(value) || value.length > 16) {
    if (value === undefined) return [];
    throw new McpAppHostError("invalid_resource_csp", "MCP App CSP domain lists must contain at most 16 origins.");
  }
  const domains = value.map(safeDomain);
  if (domains.some((domain) => domain === null)) {
    throw new McpAppHostError("invalid_resource_csp", "MCP App CSP domains must be HTTPS origins (or loopback HTTP origins).");
  }
  return Array.from(new Set(domains as string[]));
}

function resourcePresentationMeta(value: unknown): { csp: McpAppCsp; prefersBorder: boolean } {
  const meta = isRecord(value) ? value : {};
  const ui = isRecord(meta.ui) ? meta.ui : {};
  const csp = isRecord(ui.csp) ? ui.csp : {};
  const permissions = isRecord(ui.permissions) ? ui.permissions : {};
  if (Object.keys(permissions).length > 0 || ui.domain !== undefined) {
    throw new McpAppHostError(
      "unsupported_resource_permissions",
      "This OpenWork host slice does not grant device permissions or dedicated sandbox origins.",
    );

View on GitHub (pinned to 2b7df46e8a)

Solutions

  1. Trim the domains array in the app's _meta.ui.csp to 16 or fewer HTTPS origins.
  2. Consolidate origins (use a shared CDN domain or wildcard-free minimal set).
  3. Ensure ui.csp.domains is a JSON array of strings, not a string.
  4. Split UI across multiple resources if more than 16 origins are truly required.

Example fix

// before
'"csp": { "connectDomains": ["https://a.com","https://b.com", /* 20 more */] }'
// after
'"csp": { "connectDomains": ["https://api.example.com", "https://cdn.example.com"] }'
Defensive patterns

Strategy: validation

Validate before calling

const domains = meta?.ui?.csp?.connectDomains
if (!Array.isArray(domains)) throw new Error('csp domains must be an array')
if (domains.length > 16) throw new Error(`max 16 origins, got ${domains.length}`)

Type guard

function isCspDomainList(v: unknown): v is `https://${string}`[] {
  return Array.isArray(v) && v.length <= 16 && v.every((d): d is `https://${string}` => typeof d === 'string' && d.startsWith('https://'))
}

Try / catch

try {
  const { csp } = resourcePresentationMeta(resource._meta)
} catch (e) {
  if (e instanceof McpAppHostError && e.code === 'invalid_resource_csp') {
    renderWithDefaultCsp(resource); warn('app CSP rejected, using defaults')
  } else throw e
}

Prevention

When it happens

Trigger: An MCP App resource declares ui.csp with a domains array (or non-array value) longer than 16 origins, or the field is present but not an array (e.g. a string).

Common situations: Server author allow-listing many CDN/API origins in one CSP entry; auto-generated CSP including every vendor domain; passing a comma-joined string instead of an array.

Related errors


AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01). Data as JSON: /api/errors/0771fd893b0205fd. Report an issue: GitHub.