resolveMCPAllowlists now returns appsEnabled from the merged tenant-scoped config, so a
tenant/role/user override of mcpSettings.apps reaches the registry's per-request resolution and
callTool attaches no UI resource for users whose tenant disabled apps.
Authorize app-driven resource reads in the canonical (fully percent-decoded) space the server
resolves and reject any relative path segment, so a percent-encoded traversal such as %2e%2e%2f can
no longer match an advertised template. Exact resources/list matches are unaffected.
Trim narrating comments across the MCP Apps changes so the code is self-documenting.
Resolve mcpSettings.apps per request through the tenant-scoped allowlist resolver (inheriting the
YAML base when omitted) and consult it in callTool: when a tenant/role/user has apps disabled, the
tool result is returned with no UI resource attached, so those users no longer get a broken iframe
that the gated app endpoints reject. The OAuth-path connection advertises the resolved value.
Constrain query and query-continuation URI-template operators to their declared variable names
instead of the whole query string, so a template like file://items{?id} no longer authorizes
unrelated query parameters such as ?admin=true. The path-traversal guard still applies.
Move the MCP Apps per-endpoint validation and orchestration into packages/api as TypeScript
service functions (readAppResource, listAppResources, listAppResourceTemplates, callAppTool)
exported from @librechat/api, delegating through a structural manager interface to avoid a circular
import. The /api controllers become thin adapters; resolveAppContext, the sandbox file serve, and
the requireMCPAppsEnabled middleware stay in /api as request-bound glue.
Skip the server HTML fetch for resourceUri-only apps in read-only views (shared/search) and render
an "MCP Apps aren't viewable in shared conversations" placeholder instead of a failing iframe, so a
shared transcript never resolves app HTML from the viewer's MCP server. Inline apps still render.
Invalidate the advertised-resource cache on resources/list_changed by keying it on a new
resourceListVersion, so resources/read authorization tracks live add/remove of server resources
instead of staying fresh until reconnect.
Map RFC 6570 URI template operators to bounded patterns instead of a blanket wildcard, and reject a
template match whose resolved URI contains a path-traversal segment, so a query template such as
file://public{?id} can no longer authorize unrelated reads.
Return 400 rather than 500 for denied resource reads, and isolate invalid per-tool UI metadata so a
single malformed _meta.ui.resourceUri no longer aborts the whole server's tool-cache build.
Reimplement the MCP Apps ui-meta helpers (RESOURCE_MIME_TYPE, getToolUiResourceUri,
isToolVisibilityModelOnly, isToolVisibilityAppOnly) in packages/api/src/mcp/apps.ts so
@librechat/api no longer imports the ESM-only @modelcontextprotocol/ext-apps from its CommonJS
build. ext-apps remains a client-only dependency, removing the require(ESM) boundary that throws
ERR_REQUIRE_ESM on Node versions without synchronous require(esm) support.
Add an mcpSettings.apps toggle (enabled unless explicitly false). Thread enableApps through
connection creation so the io.modelcontextprotocol/ui capability is advertised only when apps are
enabled, and gate the resource and app-tool-call routes with a requireMCPAppsEnabled middleware.
Authorize app-driven resources/read against the resources and templates a server advertises, so a
sandboxed app cannot proxy arbitrary uris. ui:// resources stay allowed and the check fails closed.
Render MCP apps in shared and search transcripts display-only by withholding the host-bound bridge
handlers and capabilities in read-only views, so an embedded app cannot call tools or read
resources with the viewer's auth while the stored tool result still renders.
The host advertises serverResources, and the ext-apps bridge treats resources/read, resources/list, and resources/templates/list as one proxied set. Only the first two were wired, so an app that sent resources/templates/list received a method-not-found. Register an onlistresourcetemplates handler backed by a new MCPManager.listResourceTemplates and a /api/mcp/resources/templates/list route, mirroring the existing resources/list path. Tool listing is left out deliberately: the App Bridge has no app-to-host tools/list request, and serverTools covers only tool calls.
Make app follow-up requests fail closed when scoped config resolution errors. resolveConfigServers gains an opt-in throwOnError so the app path rejects instead of degrading to an empty set, which previously let a transient failure fall back to the base config for the same server name and proxy the iframe request to the wrong server.
Plumbs OAuth context into app follow-up requests. The app controllers now build a
flowManager and tokenMethods and pass them through readResource, listResources, and
appToolCall to getAppConnection and getConnection, so a cold-recreated connection
(idle timeout, restart, reload) for an OAuth-backed server reuses the user's stored
tokens instead of failing for lack of OAuth context.
Backs the advertised serverResources capability with resource listing. Apps that
feature-detect serverResources can call resources/list, which had no handler. A new
listResources manager method, a POST /api/mcp/resources/list route, and an
onlistresources bridge handler proxy listing the same way reads are proxied.
Makes synthetic and embedded app resource ids unique per result snapshot. The id now
mixes in the tool result content, _meta, and error state alongside the resourceUri,
structuredContent, and arguments, so repeated calls that differ only in those fields
no longer collide and overwrite earlier conversation resources.
Keeps app iframes laid out while waiting for size. The frame is rendered transparent
until a positive size event instead of display:none, with the loading state overlaid,
so an app whose initial auto-resize reports zero is not stuck behind the spinner.
Plumbs the request-scoped config and user credentials into the app endpoints so
config-sourced servers resolve and credentialed connections work even after the
original tool-call connection is gone. The readResource and app-tool-call
controllers now resolve configServers and the user's customUserVars and pass
them through to getAppConnection, which forwards configServers to
getServerConfig and customUserVars to both the connection factory and the header
refresh. Header refresh now runs for customUserVar configs when the route
supplied those vars, and is still skipped when they are absent so a live
connection's resolved headers are never clobbered with bare placeholders.
Prefers the resource item's own _meta.ui csp and permissions for embedded ui://
resources, falling back to tool-level metadata, so a resource that declares its
own connect or resource domains is not served the default restrictive policy.
Stops caching app-initiated resource reads. The five-minute cache now applies
only to the immutable app HTML fetch; bridge onreadresource calls bypass it so
dynamic resources are not served stale.
Advertises MCP Apps support during MCP initialize. The client now sends the
ext-apps capability (mimeTypes including text/html;profile=mcp-app) so servers
using the getUiCapability graceful-degradation path expose app-enhanced tools
rather than text-only fallbacks.
Bridges inline MCP App HTML. Server-bound resources now always render through
the sandbox bridge rather than a bare srcDoc iframe, and useAppBridge sends the
resource's inline text directly when present instead of a resources/read round
trip, so inline text/html;profile=mcp-app apps complete their App.connect
handshake and receive tool input and results. Bare srcDoc is kept only for
inline HTML with no server binding.
Forwards the complete tool result to apps. A shared buildAppToolResult always
produces a result for app-backed resources so ontoolresult fires even for empty
output, and it carries the tool result _meta the App Bridge forwards via
sendToolResult (the result is a full CallToolResult), which apps use to hydrate
component-only state.
Advertises the message capability. The bridge handles ui/message via onmessage
but omitted the matching host capability, so spec-compliant apps disabled
message actions; it now advertises the text message modality it supports.
Permits app reads of server resources. The resources/read proxy required the
ui:// scheme, which contradicts the serverResources capability the bridge
advertises, so it now accepts any resource URI and leaves authorization to the
MCP server.
Allows WebSocket origins in app CSP. The sandbox host allowlist dropped wss://
endpoints declared in csp.connectDomains; the pattern now permits ws and wss so
apps relying on live updates can connect.
Invalidates app-level tool metadata on reconnect. App-level connections can be
transparently recreated when a server config changes, so cached resourceUri and
visibility are now keyed to the connection that produced them and rebuilt when
it changes.
Validates open-link schemes before opening. A sandboxed app could send
ui/open-link with any string; onmessage now opens only http and https URLs and
ignores other schemes and malformed URLs, so apps cannot launch javascript: or
data: targets from the host page.
Decodes blob-backed app resources. resources/read may return HTML as a base64
blob rather than text per the MCP Apps spec, so fetchMCPResourceHtml decodes the
blob when text is absent instead of rendering a blank iframe.
Disambiguates embedded ui:// resource ids by payload. The embedded resource id
was hashed from only the template text or URI, so the same template returned by
multiple calls with different structuredContent collided and the conversation
resource map overwrote earlier entries. The id now mixes in the structured
content and tool arguments, matching the synthetic-resource path.
Allows a dedicated sandbox origin to be framed by the host. The MCP Apps spec
requires the host and sandbox to have different origins for web hosts, but the
sandbox route hardcoded same-origin framing. Framing stays same-origin by
default and an operator can list allowed host origins via
MCP_SANDBOX_FRAME_ANCESTORS for a cross-origin sandbox deployment.
Addresses security and correctness findings from a second review pass.
Sandbox hardening: trustedOrigin is now derived from document.referrer at
startup so notifyReady uses the known parent origin instead of the wildcard
'*'. toDomainList validates every entry against a strict host-pattern regex
before joining, preventing CSP injection via malicious server metadata.
serveMCPSandbox now sets Content-Security-Policy: frame-ancestors 'self' and
X-Frame-Options: SAMEORIGIN so the sandbox proxy cannot be framed by
third-party origins.
Server-side guards: appToolCall now validates that toolName is actually
registered on serverName before forwarding to tools/call. The
knownToolNamesCache is populated alongside modelOnlyToolCache in
populateToolCaches, scoped per user/server key. isModelOnlyTool was inlined
into appToolCall now that the single caching pass populates both sets.
readResource gained the updateUserLastActivity call so resource fetches also
prevent idle timeout. 500 responses now return generic messages; McpError
InvalidRequest (-32600) surfaces as 400 with the message.
Client: useAppBridge uses refs for onSizeChanged, toolArgs, and toolResult so
the stable bridge effect closure always reads current values without
triggering a remount. MCPAppView tracks timedOut separately from loaded so a
bridge failure after 10 s shows an error message instead of a blank iframe.
Added com_ui_mcp_app_failed_to_load translation key. Redundant
as string | undefined casts on toolName removed in ToolCall, MCPUIResource,
and UIResourceCarousel.