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

  1. Fix the server's tool _meta so ui.resourceUri starts with ui:// (e.g. ui://my-app/index.html).
  2. Serve the UI through the MCP server's resources (ui:// resource) rather than an external URL.
  3. If the tool is not meant to be an MCP App, remove the resourceUri from _meta entirely so toolUiResourceUri returns null.
  4. 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

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


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