vercel/ai · error · Error

Disallowed ui/open-link scheme: ${scheme}

Error message

Disallowed ui/open-link scheme: ${scheme}

What it means

This is a security allow-list on `ui/open-link`: the iframe may only ask the host to open `https:`, `http:`, or `mailto:` URLs. Any other scheme is rejected so untrusted apps cannot trigger `javascript:`, `data:`, `file:`, or custom-protocol navigations from the host page.

Source

Thrown at packages/react/src/mcp-apps/bridge.ts:98

/**
 * Validates `ui/open-link` params and allows only `https:`/`http:`/`mailto:`
 * URLs.
 */
function assertOpenLinkParams(params: unknown): { url: string } {
  if (!isJSONObject(params) || typeof params.url !== 'string') {
    throw new Error('Invalid ui/open-link params');
  }

  let scheme: string;
  try {
    scheme = new URL(params.url).protocol;
  } catch {
    throw new Error(`Invalid ui/open-link url: ${params.url}`);
  }

  if (scheme !== 'https:' && scheme !== 'http:' && scheme !== 'mailto:') {
    throw new Error(`Disallowed ui/open-link scheme: ${scheme}`);
  }

  return { url: params.url };
}

/**
 * Validates params for `ui/request-display-mode`.
 */
function assertDisplayModeParams(params: unknown): {
  mode: 'inline' | 'fullscreen' | 'pip';
} {
  if (
    !isJSONObject(params) ||
    (params.mode !== 'inline' &&
      params.mode !== 'fullscreen' &&
      params.mode !== 'pip')
  ) {
    throw new Error('Invalid ui/request-display-mode params');

View on GitHub (pinned to 69428b1f8b)

Solutions

  1. Use an `https://` (or `http:`/`mailto:`) URL instead of a custom scheme.
  2. If a custom protocol is genuinely needed, implement a host-side `openLink` wrapper and a dedicated allow-listed tool, not ui/open-link.
  3. Serve deep-link content via a regular web URL that redirects to the app scheme.
  4. Audit the app's link sources to ensure no untrusted `javascript:`/`data:` values can reach ui/open-link.

Example fix

// before
openLink({ url: 'javascript:void(window.print())' })
// after
openLink({ url: 'https://example.com/print' })
Defensive patterns

Strategy: validation

Validate before calling

// app-side, before requesting:
const protocol = new URL(url).protocol;
if (protocol !== 'https:' && protocol !== 'http:' && protocol !== 'mailto:') {
  throw new Error(`Scheme ${protocol} cannot be opened; use https/http/mailto`);
}

Type guard

function isAllowedLinkUrl(value: string): boolean {
  try {
    const p = new URL(value).protocol;
    return p === 'https:' || p === 'http:' || p === 'mailto:';
  } catch { return false; }
}

Try / catch

try {
  await openLink({ url });
} catch (error) {
  if (error instanceof Error && error.message.startsWith('Disallowed ui/open-link scheme')) {
    console.warn('Blocked non-web scheme; convert deep link to an https redirect url');
  }
}

Prevention

When it happens

Trigger: The iframe sends `ui/open-link` with a well-formed URL whose protocol is not in the allow-list, e.g. `javascript:alert(1)`, `data:text/html,...`, `vscode://...`, or `intent://...`.

Common situations: App tries to deep-link into a desktop app via custom scheme; malicious/compromised iframe attempting XSS via `javascript:` urls; app embedding `data:` URLs generated from content.

Related errors


AI-assisted analysis of vercel/ai@69428b1f8b (2026-08-30). Data as JSON: /api/errors/0e347f1c6aba3aa1. Report an issue: GitHub.