different-ai/openwork · error · McpAppHostError
invalid_resource_uri
invalid_resource_uri
Error message
MCP App resource URI must use ui://.
What it means
toolUiResourceUri reads an MCP App's declared UI resource URI from tool._meta (either meta.ui.resourceUri or legacy meta['ui/resourceUri']). If a URI is present but does not use the required ui:// scheme, the host rejects it because MCP Apps are only rendered from ui:// resources.
Source
Thrown at apps/server/src/mcp-app-host.ts:90
if (!isRecord(value)) return {};
return Object.fromEntries(
Object.entries(value).filter((entry): entry is [string, string] => typeof entry[1] === "string"),
);
}
export function projectedMcpToolName(serverName: string, toolName: string): string {
const sanitize = (value: string) => value.replace(/[^a-zA-Z0-9_-]/g, "_");
return `${sanitize(serverName)}_${sanitize(toolName)}`;
}
export function toolUiResourceUri(tool: Partial<Tool>): string | null {
const meta = isRecord(tool._meta) ? tool._meta : {};
const ui = isRecord(meta.ui) ? meta.ui : {};
const nested = typeof ui.resourceUri === "string" ? ui.resourceUri : null;
const legacy = typeof meta["ui/resourceUri"] === "string" ? meta["ui/resourceUri"] : null;
const uri = nested ?? legacy;
if (!uri) return null;
if (!uri.startsWith("ui://")) throw new McpAppHostError("invalid_resource_uri", "MCP App resource URI must use ui://.");
return uri;
}
function safeDomain(value: unknown): string | null {
if (typeof value !== "string" || value.length > 2048) return null;
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]";View on GitHub (pinned to 2b7df46e8a)
Solutions
- Fix the server's tool _meta so ui.resourceUri starts with ui:// (e.g. ui://my-app/index.html).
- Serve the UI through the MCP server's resources (ui:// resource) rather than an external URL.
- If the tool is not meant to be an MCP App, remove the resourceUri from _meta entirely so toolUiResourceUri returns null.
- Update the MCP App SDK/server framework to a version that emits ui:// URIs.
Example fix
// before
'"_meta": { "ui": { "resourceUri": "https://cdn.example.com/app.html" } }'
// after
'"_meta": { "ui": { "resourceUri": "ui://example-app/index.html" } }' Defensive patterns
Strategy: validation
Validate before calling
function assertUiResourceUri(tool: { _meta?: unknown }): string | null {
const meta = (tool._meta ?? {}) as Record<string, unknown>
const ui = (meta.ui ?? {}) as Record<string, unknown>
const uri = typeof ui.resourceUri === 'string' ? ui.resourceUri : typeof meta['ui/resourceUri'] === 'string' ? (meta['ui/resourceUri'] as string) : null
if (uri && !uri.startsWith('ui://')) throw new Error(`resourceUri must start with ui://, got ${uri}`)
return uri
} Type guard
function hasValidUiUri(meta: unknown): meta is { ui: { resourceUri: `ui://${string}` } } {
const m = meta as Record<string, unknown>
const ui = m?.ui as Record<string, unknown> | undefined
const legacy = m?.['ui/resourceUri']
const uri = (ui?.resourceUri ?? legacy)
return typeof uri === 'string' && uri.startsWith('ui://')
} Try / catch
try {
const uri = host.toolUiResourceUri(tool)
} catch (e) {
if (e instanceof McpAppHostError && e.code === 'invalid_resource_uri') {
skipAppRendering(tool.name); reportBadToolMeta(tool.name)
} else throw e
} Prevention
- Emit ui.resourceUri with the ui:// scheme when authoring MCP App tools
- Serve app UIs as MCP resources, never plain http(s) URLs in _meta
- Test your server's tool metadata against an MCP App host before shipping
- Prefer the nested meta.ui.resourceUri form over the legacy key
When it happens
Trigger: Connecting to an MCP server whose tool metadata declares a resourceUri with http://, https://, file://, or any non-ui:// scheme; hand-written _meta with a typo like 'ui://' missing or a plain web URL.
Common situations: Server author pointed the app UI at a normal web page instead of packaging it as a ui:// MCP App resource; older or non-conforming MCP App tooling emitting legacy absolute URLs; malformed custom tool _meta.
Related errors
- invalid_resource_csp
- OpenWork Connect MCP metadata is invalid.
- OpenWork-managed OAuth requires a remote MCP URL.
- extensions.add_mcp_url_required
- protocol "${url.protocol}" is not allowed
AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01).
Data as JSON: /api/errors/fbbc8aa0159e94a1.
Report an issue: GitHub.