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
- Use an `https://` (or `http:`/`mailto:`) URL instead of a custom scheme.
- If a custom protocol is genuinely needed, implement a host-side `openLink` wrapper and a dedicated allow-listed tool, not ui/open-link.
- Serve deep-link content via a regular web URL that redirects to the app scheme.
- 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
- Only generate https/http/mailto links in app UI.
- Never pass raw user or model text directly as an open-link url (javascript:/data: risk).
- Replace custom deep-link schemes with web URLs that redirect.
- Sanitize link sources (CMS, LLM output) before opening.
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
- resources/read is limited to ui:// resources: ${params.uri}
- Invalid ui/open-link url: ${params.url}
- Tool is not app-visible: ${params.name}
- Invalid tools/call params
- Invalid resources/read params
AI-assisted analysis of vercel/ai@69428b1f8b (2026-08-30).
Data as JSON: /api/errors/0e347f1c6aba3aa1.
Report an issue: GitHub.