Commit graph

4622 commits

Author SHA1 Message Date
Dustin Healy
8f10ef4b1f feat(mcp): dedicated-origin allow-same-origin, live host-context, configurable CSP, theme vars
Grant allow-same-origin to the sandbox inner frame only when the sandbox runs on a dedicated origin
(parentOrigin differs from the sandbox origin), matching the spec dedicated-origin model so
storage-backed apps work there while same-origin deployments stay isolated from the host.

Push host-context updates to a live app: a MutationObserver on the document theme class sends
sendHostContextChange with the new theme and derived style tokens when the user toggles light or
dark while an app is open.

Provide the standardized MCP Apps CSS theme variables (a mapped subset of LibreChat tokens) in the
initial hostContext and on theme change.

Add an opt-in strict CSP (VITE_MCP_SANDBOX_STRICT_CSP) that drops unsafe-eval, wasm-unsafe-eval,
blob:, and data: from the sandbox script-src, threaded to the sandbox via a strictCsp query param.
2026-06-30 17:43:42 -07:00
Dustin Healy
0f708c2eb8 fix(mcp): harden app CSP, fail closed on auth resolution, and rate-limit resource reads
Render non-app (no profile=mcp-app) ui:// HTML inert: the static srcDoc iframes in ToolCall,
MCPUIResource, and UIResourceCarousel now use sandbox="" so scripts and forms run only through the
CSP-applying sandbox proxy. Make the proxy's meta CSP unbypassable by wrapping any document whose
markup precedes <head>, so nothing untrusted is parsed before the policy takes effect.

Fail closed in resolveAppContext when MCP auth-value resolution throws, logging and rejecting rather
than proceeding with unresolved or stale credentials. Validate each MCP_SANDBOX_FRAME_ANCESTORS
token against a scheme://host[:port] pattern so a stray ";" cannot inject an extra CSP directive.

Rate-limit the app resource endpoints (resources/read, list, templates/list) per user, and correct
AppToolResult.content from an empty-tuple type to unknown[]. Add controller tests for the
frame-ancestors validation and the auth fail-closed path.
2026-06-30 17:30:56 -07:00
Dustin Healy
b24eee648e refactor(mcp): advertise UI capability at host level and fix tool-visibility scoping
Advertise io.modelcontextprotocol/ui unconditionally on every MCP connection instead of gating it on
a per-connection appsEnabled value, and remove the now-unused enableApps plumbing from the
connection factory, inspector, and connection-building call sites. Per SEP-1865 the capability is a
per-session statement of host rendering ability, not a per-user setting; gating it per request on a
shared connection pool was both spec-incorrect and unable to support tenant opt-in. Per-tenant apps
enablement stays enforced downstream (callTool UI-resource attachment plus the app endpoints), which
now works for both opt-in and opt-out.

Replace the exact-match visibility helpers with includes-based semantics: a tool is hidden from the
model when an explicit visibility array omits "model", and denied to app callers when it omits
"app". An empty or future-scoped array such as [] or ["model","internal"] is now handled correctly,
and the both-scopes default applies only when visibility is absent. Rename the cache to reflect that
it holds app-hidden tools.
2026-06-30 17:16:58 -07:00
Dustin Healy
ea5600a4b1 docs(mcp): clarify the UI capability is a host-level, per-session setting
Note at the advertising site that io.modelcontextprotocol/ui tracks the instance-wide apps setting
and per-tenant policy is enforced downstream, so the rationale lives with the code.
2026-06-30 08:48:53 -07:00
Dustin Healy
d87c12b7ec fix(mcp): advertise UI capability at host level and align app-resource handling
Per the MCP Apps spec (SEP-1865) the io.modelcontextprotocol/ui capability is negotiated once per
client-server session and declares the host's rendering ability, not a per-user preference;
per-tenant apps policy belongs in the host's downstream layer. Advertise the capability from the
global apps setting at every connection-building path instead of the per-request resolved value, so
a shared serverName-keyed connection no longer has its capability fixed by whichever scope opened it
first. Per-tenant apps-enabled stays enforced downstream in callTool resource attachment and the app
endpoints.

Classify only text/html;profile=mcp-app resources as app-backed so the bridge metadata matches the
client isMcpAppResource check; a plain text/html ui:// resource renders as a static srcDoc on both
sides instead of carrying dead tool-result metadata.

Redact the tool-result _meta (resultMeta) from ui_resources during share serialization so
model-hidden metadata cannot ride into a public shared transcript.

Resolve app follow-up requests through the originating tool call's connection path rather than
forcing a user-scoped connection, which the app-level guard rejects for shared servers.
2026-06-29 14:43:52 -07:00
Dustin Healy
2905c1563b fix(mcp): honor per-request apps flag in discovery and tighten app resource routing
Use the appsEnabled value resolved by resolveAllowlists in the tool-discovery connection path so
an OAuth/reinitialize fallback discovery does not advertise the UI extension for a user whose
effective config disabled apps.

Constrain simple URI-template expansions to exclude query delimiters so a value like q={q} cannot
match q=foo&admin=true and authorize an undeclared parameter on app resources/read.

Route app requests that carry a config-tier override through a request-scoped connection so iframe
reads and tool calls reach the overridden server instead of the cached base app connection.
2026-06-29 12:09:16 -07:00
Dustin Healy
624a6d8f4b fix(mcp): gate apps per-request on app connections and embedded UI resources
Resolve the allowlist-derived appsEnabled value when creating app-level connections in
ConnectionsRepository so a tenant/role/user override that toggles apps is honored instead of the
boot YAML default.

Gate ui:// resources embedded in tool results on the same per-request setting so a disabled scope
renders them as plain resource text rather than a sandboxed app, resolving appsEnabled lazily only
when a result actually carries a renderable UI resource.

Fail closed in canonicalizeUri when a URI does not stabilize within the decode cap so traversal
encoded more deeply than the cap cannot satisfy a template guard a fully-decoding server resolves
as a parent-directory path.
2026-06-29 11:07:55 -07:00
Dustin Healy
1a70dce24b fix(mcp): advertise apps per-request for user connections and tear down navigated sandbox frames
User connections now advertise the io.modelcontextprotocol/ui capability using the per-request
appsEnabled resolved from resolveAllowlists rather than the static base flag, so a tenant/role/user
override of mcpSettings.apps is honored at capability negotiation.

The sandbox proxy treats inner-frame navigation as a teardown signal: after the initial blob load,
any further load means the allow-scripts app navigated its own frame, so the proxy marks it
navigated, revokes the blob, removes the frame, and gates both forwarding paths on that flag. This
stops proxied host responses from reaching a navigated page and stops a navigated page from relaying
messages to the host.
2026-06-29 10:37:39 -07:00
Dustin Healy
87341c67c0 fix(mcp): carry apps flag through the request resolver and canonicalize resource-read auth
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.
2026-06-29 00:52:58 -07:00
Dustin Healy
f101d73f72 fix(mcp): resolve apps per request, tighten resource templates, extract app controller
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.
2026-06-28 23:52:37 -07:00
Dustin Healy
e459984f21 test(mcp): wire read-only context into MCP app component tests
The read-only placeholder makes app-backed resourceUri-only resources render a placeholder instead
of the bridge iframe when no MessagesViewProvider is present, which broke component tests that
render these components standalone. Mock useIsMessagesViewReadOnly to interactive in the affected
suites and add a regression test asserting the placeholder renders (and the iframe does not) in a
read-only view.
2026-06-28 23:17:30 -07:00
Dustin Healy
eedaf1a054 fix(mcp): tighten MCP Apps read-only views and resource-read authorization
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.
2026-06-28 22:58:21 -07:00
Dustin Healy
ea75afc99a fix(mcp): harden MCP Apps host security and CJS compatibility
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.
2026-06-28 21:56:28 -07:00
Dustin Healy
2f650687d6 chore(mcp): loosen csp safety so threejs mcp apps official demo server can run 2026-06-25 22:56:54 -07:00
Dustin Healy
20afb27961 fix(mcp): sync api lockfile, scope resource ids, and time out stuck app spinners
Record @modelcontextprotocol/ext-apps in the packages/api peerDependencies entry of the lockfile so a clean npm ci (the Dockerfiles' install path) validates the workspace metadata instead of failing before the build. Regenerated in a linux/amd64 container so platform optional deps are preserved.

Include serverName and toolName in deriveResourceId so two MCP servers that share a ui:// app shell and produce the same result no longer collide on one resourceId in the conversation-wide map and route a marker to the wrong server.

Give marker-rendered apps and carousel cards the same 10s load timeout the tool-call view already has, so an app whose iframe never completes the bridge handshake shows a failure state instead of a permanent loading spinner.
2026-06-25 15:09:39 -07:00
Dustin Healy
acc0befd0b fix(mcp): proxy resource templates and fail closed on app config resolution
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.
2026-06-25 14:42:14 -07:00
Dustin Healy
f31dacac70 fix(mcp): harden app bridge lifecycle and gate ui:// rendering to HTML
Guard the app bridge against stale-resource races. A resourceId switch or unmount while the iframe is still loading or bridge.connect() is pending now cancels the in-flight handleLoad, removes the load listener, and closes the orphaned bridge, so a single iframe can no longer end up with two bridges issuing duplicate sandbox and tool requests.

Build the sandbox URL from apiBaseUrl() and let new URL() resolve it against the window origin. apiBaseUrl() already reflects an absolute or non-root base href, so the extra origin prefix produced a malformed URL that broke every app iframe.

Emit a renderable ui:// marker only for HTML resources, since MCP Apps defines text/html;profile=mcp-app as the sole renderable type; json and remote-dom payloads fall through to plain resource text instead of a marker the host cannot render. Hide the iframe when an app sends request-teardown so a torn-down app no longer leaves a blank frame mounted.
2026-06-25 13:57:39 -07:00
Dustin Healy
39b338cac7 refactor(mcp): drop ref-sync effects and cache app HTML via react-query
Replace the per-prop ref-sync effects in useAppBridge with render-time ref assignments, leaving a single effect for the imperative AppBridge lifecycle. The refs still shield the once-per-resource bridge from unstable callbacks like ask, matching the latest-value-ref idiom used elsewhere in the app.

Route the immutable app HTML fetch through queryClient.fetchQuery keyed on QueryKeys.mcpAppResourceHtml so it shares the standard React Query cache and in-flight dedup, then remove the bespoke Map cache from mcpApps. readMCPResource and fetchMCPResourceHtml no longer thread a userId that was only ever a cache key and never sent to the server.
2026-06-25 13:12:13 -07:00
Dustin Healy
30611cb790 fix(mcp): harden app rendering and tool-cache invalidation
Guard the sandbox-ready handler so the proxy's repeated ready signals fetch and post the resource only once, and reveal apps on the initialized handshake so tools with auto-resize disabled are no longer stuck behind the loading spinner. Broaden the inline srcDoc fallback to any text-bearing resource so non-HTML ui:// markers still render.

Stop advertising an MCP App for tool calls whose config the app routes reject (OBO, Graph-token, and runtime body-placeholder servers), since the iframe could never fetch its HTML or run follow-up calls. Skip caching an empty tools/list result so a transient fetch failure is re-fetched instead of disabling apps until reconnect, and fold a tools/list_changed version counter into the cache freshness stamp so live tool changes invalidate a cache that createdAt alone would miss.
2026-06-25 11:54:17 -07:00
Dustin Healy
4261adcd5d fix(mcp): keep app tool calls alive on progress and always send tool input
Sets resetTimeoutOnProgress on app-initiated tool calls so a long-running tool
that keeps streaming MCP progress is not aborted at the fixed connection timeout,
matching the model-initiated callTool path.

Always sends the tool-input notification before the result, even when the tool
has no arguments. MCP Apps expect ui tool-input exactly once before the result,
so apps that initialize from ontoolinput no longer stay blank for no-argument
tools.
2026-06-25 11:09:43 -07:00
Dustin Healy
c9f5441b0d fix(mcp): gate app-bridge on the mcp-app profile and decode blob HTML as UTF-8
Routes only genuine MCP Apps through the App Bridge. Now that formatToolContent
stamps serverName and toolName onto every ui:// resource, a plain text/html
resource was treated as bridge-backed and stayed hidden because it never runs the
handshake or emits a size event. An isMcpAppResource helper now requires the
text/html;profile=mcp-app profile (alongside the server binding); plain HTML
resources fall back to the srcDoc path across MCPAppView, MCPAppCard, and
MCPUIResource.

Decodes blob-backed app HTML as UTF-8. atob alone yields a Latin-1 string, so a
blob template with non-ASCII content rendered as mojibake; the base64 bytes are
now decoded with TextDecoder before injection.
2026-06-25 10:07:00 -07:00
Dustin Healy
ff39323fff fix(mcp): OAuth-aware app connections, list proxy, unique result ids, laid-out iframes
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.
2026-06-25 07:12:46 -07:00
Dustin Healy
39a06f43f4 fix(mcp): restrict sandbox form submissions to the declared egress
form-action does not fall back to default-src, so with the inner iframe created
allow-forms a script could submit a hidden form to any origin and bypass the
connectDomains deny-by-default egress policy. The generated sandbox CSP now sets
form-action to the same declared connect allowlist ('none' when none is set).
2026-06-24 08:30:06 -07:00
Dustin Healy
251b18b9e9 fix(mcp): resolve config and credential context for app follow-up requests
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.
2026-06-24 08:13:32 -07:00
Dustin Healy
228627750a fix(mcp): bridge inline apps, forward full results, and close review gaps
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.
2026-06-24 00:14:32 -07:00
Dustin Healy
de28930ddf fix(mcp): resolve Codex review on the app-bridge follow-ups
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.
2026-06-23 23:29:16 -07:00
Dustin Healy
b664cce8ca fix(mcp): close follow-up Codex findings on the app-bridge fixes
Preserves resolved auth headers for non-DB servers that declare customUserVars.
getAppConnection re-ran processMCPEnv without customUserVars and overwrote the
headers the original tool call resolved, so iframe-initiated calls sent bare
placeholders. Header refresh now also skips when the config has customUserVars,
leaving the connection's resolved headers intact.

Forwards the tool result to embedded ui:// resources. When a tool returns an
inline ui:// resource item rather than declaring it via resourceUri, the
synthetic path already attached structuredContent and content but the embedded
path did not, so the app bridge initialized without result data. Both paths now
carry it.

Stops reusing UI metadata across request-scoped servers. The resourceUri and
visibility caches keyed on serverName:userId could serve a later request the
first request's metadata when the upstream varies by body context, so
request-scoped servers now build their tool metadata fresh per call.
2026-06-23 21:57:21 -07:00
Dustin Healy
ae0e98e4c9 fix(mcp): address Codex round-3 findings declined in error
Re-verification against source showed five previously declined Codex threads
had merit, so this implements the fixes rather than the incorrect push-backs.

Restores rendering of inline ui:// text resources attached to a tool call.
The selector in ToolCall excluded text-bearing resources and ToolCallInfo no
longer received attachments, so an inline resource vanished unless the model
echoed a marker. MCPAppView now renders every renderable attachment and routes
text resources through its srcDoc branch.

Wires metadata cache invalidation. clearResourceUriCache had no caller, so the
resourceUri, modelOnly and knownToolNames caches were never cleared. Tearing
down a user connection now evicts that server/user cache entry.

Threads the original registry config through the iframe follow-up RPCs.
readResource and appToolCall share a getAppConnection helper that resolves the
server config, hands it to getConnection so config-source servers resolve,
refreshes headers for non-DB-sourced servers, and rejects configs that still
need request body placeholders the app context cannot supply.

Completes the host side of the App Bridge so migrated mcp-ui apps keep working:
advertises serverResources and sets onreadresource for lazy resource loads, and
wires onmessage so ui/message (the standardized replacement for mcp-ui prompt
and intent actions) reaches the conversation.

Honors a dedicated sandbox origin. getMCPSandboxUrl passes the parent origin
to the sandbox, which the proxy reads from the parentOrigin query param so the
handshake targets LibreChat instead of the sandbox's own origin.
2026-06-23 21:42:17 -07:00
Dustin Healy
1f9d038291 fix(mcp): propagate isError to MCPAppView toolResult
MCPAppView in ToolCall.tsx was the only app renderer that didn't include
isError in the toolResult memo; MCPUIResource and UIResourceCarousel both
did. Aligns the three renderers so the bridge receives the error flag when
a failed MCP tool still returns app content.
2026-06-23 20:31:44 -07:00
Dustin Healy
cc45641d7e fix(mcp): address Codex P1/P2 findings — visibility filter, header clobber, base path, inline text
App-only tools (visibility: ['app']) were not filtered in MCPServerInspector.getToolFunctions,
so initializeMCPs → getAppToolFunctions → mergeAppTools was silently exposing them to the LLM
tool cache at startup, bypassing the filter that updateMCPServerTools correctly applies.
Applied the same visibility guard that updateMCPServerTools uses.

appToolCall was calling processMCPEnv without customUserVars for DB-sourced servers, then
setRequestHeaders — overwriting the connection's already-correctly-resolved headers with
unresolved {{MCP_API_KEY}} placeholders. Skipped the re-resolve for DB-sourced servers
since the connection carries valid headers from the original callTool setup.

callMCPAppTool and readMCPResource used hardcoded /api/... paths without the apiBaseUrl()
prefix; subdirectory deployments would miss those routes. apiBaseUrl was already imported for
getMCPSandboxUrl — extended it to both API calls for consistency.

MCPAppCard (carousel) and MCPAppView (ToolCall) both checked toolName && serverName first
when deciding to use the app bridge, but parsers.ts now sets those fields on all UIResources
including inline ui:// resources with text content. Resources with text were therefore silently
routed through the app bridge instead of being rendered directly as srcDoc iframes.
Added !resource.text / !app.text guard so inline HTML resources take the correct path.
2026-06-23 20:27:30 -07:00
Dustin Healy
d143bb10cc fix(mcp): address Codex P2 findings — resource ID collision, isError flag, sandbox base path
Resource ID for synthetic app UIResources now includes toolArgs in the
hash, so repeated calls to the same tool with different arguments
produce distinct IDs instead of overwriting each other in the
conversation resource map.

isError from MCPToolCallResponse is now stored on UIResource and
forwarded in the toolResult sent to the app via sendToolResult, so
app renderers can distinguish failed tool executions.

getMCPSandboxUrl now prepends apiBaseUrl() so the outer iframe src
resolves correctly in deployments served under a base path.
2026-06-23 19:46:47 -07:00
Dustin Healy
30efc5e42f fix(docker): copy mcp-sandbox.html from client/public not client/dist
vite.config.ts sets publicDir to false during builds so public/ files
are not emitted to dist/. The COPY instruction must reference the source
path in the client-build stage, not the build output path.
2026-06-23 19:39:24 -07:00
Dustin Healy
5fb406b450 fix(ci): update UIResourceCarousel dimensions test and sort Mention.tsx imports
The carousel card container was changed to use dynamic height (defaulting
to 360px) rather than a fixed minHeight. The test expected minHeight which
no longer exists. Also removes the animationDelay assertion which jsdom
does not verify via toHaveStyle.

Mention.tsx had a pre-existing import-sort drift that the CI sort-check
detected; sorted to clear the gate.
2026-06-23 19:37:17 -07:00
Dustin Healy
3de0512e21 feat: rate-limit /api/mcp/app-tool-call per user
Adds mcpAppToolCallLimiter (60 req/min per user) to prevent DoS
amplification through the MCP app iframe tool-call proxy. Follows the
same express-rate-limit + limiterCache + logViolation pattern as
toolCallLimiter.
2026-06-23 19:22:25 -07:00
Dustin Healy
3816864392 fix(mcp): address Codex P1/P2 findings — CSP, permissions, toolArgs propagation
Six findings from the Codex review pass on ac2812ba2:

Apply restrictive default CSP when _meta.ui.csp is omitted: buildCspMeta
now uses an empty object fallback so sandboxed apps without explicit CSP
declarations still get default-src 'none' / connect-src 'none' rather than
running with no Content-Security-Policy at all.

Add media-src to buildCspPolicy: resourceDomains now covers audio and video
loads; omitting it previously caused default-src 'none' to block media even
when the server declared approved CDN domains.

Propagate toolArgs through UIResource so inline \ui{} marker renders call
sendToolInput: callTool passes toolArguments into formatToolContent metadata,
parsers stores it on both explicit and synthetic UIResources, and MCPUIResource
and MCPAppCard now forward it to useAppBridge instead of always passing
undefined.

Update outer iframe allow attribute with resolved permissions from
resources/read: the sandboxready handler now re-applies buildAllowAttribute
with the fetched permissions before sendSandboxResourceReady, so
camera/mic/geo permissions declared only in _meta.ui are not blocked at
the browser permission-policy boundary.

Guard appToolCall against Graph API token placeholder servers: uses
mcpOptionsContainGraphTokenPlaceholder to detect unresolvable
{{LIBRECHAT_GRAPH_ACCESS_TOKEN}} placeholders and throws InvalidRequest
with a clear message, matching the existing OBO guard pattern.

Honor app-reported heights in UIResourceCarousel cards: MCPAppCard now
accepts an onHeightChange callback; UIResourceCarousel tracks per-card
dynamic heights and applies them to the outer card container instead of
the fixed 360px value.
2026-06-23 19:06:40 -07:00
Dustin Healy
ac2812ba2f fix(mcp): use window.location.origin as trusted sandbox origin
The previous approach derived trustedOrigin from document.referrer at
startup and fell back to '*' when referrer was empty, with a lazy-set
from the first incoming message as a further fallback. Both paths leave
a window where notifyReady broadcasts to all frames or the origin can
be set by an untrusted first message.

The sandbox is always served same-origin with LibreChat (/api/mcp/sandbox),
so window.location.origin is always the exact parent origin. This replaces
the referrer parse and lazy-set entirely: trustedOrigin is a const set at
parse time, notifyReady uses it directly, and the message handler rejects
any message whose origin does not match without fallback.
2026-06-23 18:47:28 -07:00
Dustin Healy
90b2d7a1ab fix(mcp): harden sandbox security and fix stale closures, error states
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.
2026-06-23 18:35:27 -07:00
Dustin Healy
d65c228cea fix(mcp): address second round of Codex review findings
Fixes 13 correctness issues flagged in the second Codex review pass on the
feat/mcp-apps-support branch.

Core server-side changes: resource URI and model-only-tool caches are now
scoped per user/server key so OAuth and user-sourced servers with differing
tool lists cannot cross-contaminate each other. The model-only visibility check
in appToolCall now blocks iframe-initiated calls to tools declared as
visibility: ['model']. appToolCall also runs processMCPEnv to resolve runtime
env/user vars and set request headers before forwarding to tools/call, and
throws for servers that require per-call OBO token minting (unsupported in this
path). parsers.ts now includes structuredContent in the synthetic resourceId
hash to guarantee uniqueness across repeated same-app calls with different
results, skips the early-return guard when a synthetic app resource is present,
appends the ui{} marker to the synthetic text block, and forwards the raw
content array alongside structuredContent so text/image-only app results are
not silently dropped.

Client-side changes: fetchMCPResourceHtml now returns the full _meta.ui from
the resources/read content item so CSP and permissions come from the canonical
location in the spec rather than the tool descriptor. useAppBridge falls back
to the resource-level values when the read result carries no overrides.
The sandbox retry interval clears when sandbox-resource-ready arrives, fixing
the race where the ready notification arrived before the transport was
connected. The size-change handler in MCPUIResource and UIResourceCarousel now
applies the reported height to the wrapper element, and MCPUIResource's iframe
style uses height: 100% so inline apps are not clipped. The carousel loading
placeholder now uses the localized key. Dockerfile.multi copies the sandbox
from client/dist (the Vite output) rather than the source tree, which is the
only path present in the multi-stage runtime image. baseUriDomains from the
CSP config are now honoured in buildCspPolicy instead of always emitting
base-uri 'self'. serverResources was removed from the AppBridge capabilities
advertisement because no resource handlers are registered on the bridge.
2026-06-23 18:18:51 -07:00
Dustin Healy
4da55e8178 fix(mcp): refresh user activity on app tool calls; scope resource cache by user
appToolCall was missing the updateUserLastActivity call that callTool
includes, so app interactions from an iframe would not reset the idle timer
on user-scoped MCP connections.

The client-side resourceCache in mcpApps.ts was keyed only on
serverName:uri, meaning a second user logging in within the 5-minute TTL
could receive cached HTML from the previous user's session. The key now
includes the userId, threaded from the Recoil user atom via useAppBridge.
2026-06-23 17:07:16 -07:00
Dustin Healy
b1fa8221ef fix(mcp): fix three correctness issues from Codex review
parsers.ts: the early-return guard for empty content was firing before the
synthetic UIResource block, so tools returning only structuredContent (no
content items) never produced an app resource. Now the guard skips the early
return when a synthetic app is expected. Also, the synthetic block was not
appending the \ui{resourceId} marker to the text, leaving the LLM without an
ID to place; fixed by mirroring the marker lines that the non-synthetic
resource handler already emits.

ToolCall / MCPUIResource: "Loading interactive view..." was a hardcoded
string; replaced with com_ui_loading_interactive_view from the localization
layer in both components, per project convention.
2026-06-23 16:39:58 -07:00
Dustin Healy
c3b002cf18 test(client): update ToolCall tests for refactored attachment rendering
ToolCallInfo no longer receives attachments since MCPUIResource handles UI resource
rendering now. Update tests to verify AttachmentGroup receives attachments instead.
2026-06-23 16:06:08 -07:00
Dustin Healy
d78fc0b5a9 fix: restore attachments prop to ToolCallInfo in ToolCall
Accidentally dropped during MCPAppView refactor; the prop was in origin/main.
2026-06-23 16:01:05 -07:00
Dustin Healy
5e844a614a fix(ci): add @modelcontextprotocol/ext-apps to jest transformIgnorePatterns and fix import sort
The package ships ESM-only; adding it to the babel-jest transform allow list
lets all three jest configs (api, packages/api, and config which inherits api)
process it correctly. Also sort imports in ToolCallInfo.tsx and
MCPUIResourceCarousel.test.tsx which the CI sort-imports check flagged.
2026-06-23 15:46:38 -07:00
Dustin Healy
ec60cd479d feat(mcp-apps): spec compliance pass on AppBridge implementation
Add hostContext (theme/platform/locale/timeZone/displayMode) to AppBridge
constructor, declare serverResources capability, wire requestteardown and
loggingmessage event listeners, fix buildAllowAttribute placement (set as
iframe allow attribute, not sandbox token), add isToolVisibilityAppOnly
inline check in tool cache builder, and remove dead handleUIAction code
that used the old @mcp-ui/client intent/tool/prompt action API.

Also guard MCPUIResource against useOptionalMessagesConversation returning
undefined outside a provider context.
2026-06-23 14:16:10 -07:00
Dustin Healy
f2f3c18ca4 refactor: replace @mcp-ui/client with @modelcontextprotocol/ext-apps/app-bridge
Switch from the community-maintained @mcp-ui/client to the official Anthropic SDK for MCP app rendering.
Introduces useAppBridge hook that drives AppBridge + PostMessageTransport directly, giving us full
control over the spec protocol and proper propagation of _meta.ui.csp and _meta.ui.permissions from
tool definitions to the sandbox iframe.

Removes useMCPAppCallbacks and the AppRenderer dependency; all three render sites (ToolCall, UIResourceCarousel,
MCPUIResource) now use a plain iframe with useAppBridge. The existing mcp-sandbox.html proxy is unchanged.
2026-06-23 13:55:56 -07:00
Dustin Healy
4481e80111 chore: regenerate package-lock.json on Linux x64 against current dev deps 2026-06-22 00:20:01 -07:00
Dustin Healy
3865bf5177 fix: resolve UIResource index signature type errors for @mcp-ui/client v7 2026-06-22 00:06:44 -07:00
Dustin Healy
2b88a47460 feat: MCP Apps support (squashed for rebase) 2026-06-21 23:55:17 -07:00
Danny Avila
465cb6e394
👐 a11y: Bump @ariakit/react, Improve a11y of Token Usage, Archived Chats, Reduce Table Layout Shifts (#13874)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Publish `@librechat/client` to NPM / pack (push) Waiting to run
Publish `@librechat/client` to NPM / publish-npm (push) Blocked by required conditions
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
* chore: Update `@ariakit/react` and `@ariakit/react-core` dependencies to v0.4.29 and v0.4.26 respectively, and add new `@ariakit/components`, `@ariakit/react-components`, `@ariakit/react-store`, and `@ariakit/react-utils` packages to package-lock.json and package.json files.

* fix: restore keyboard navigation for Tools dropdown submenus

Compose the Artifacts and MCP submenu triggers as a `MenuButton` that
receives the parent `MenuItem`'s props/ref directly, instead of nesting a
`MenuItem` inside the submenu's own provider and placing the ref on a
wrapper div. This registers the focusable trigger with the parent menu
store so arrow-key navigation reaches the items, which fully broke under
Ariakit 0.4.29.

* fix: Improve keyboard navigation for TokenUsageIndicator popover

Refactor the TokenUsageIndicator component to enhance keyboard accessibility. The popover now maintains focus on the gauge trigger, ensuring that the Escape key closes the popover without shifting focus to the non-interactive panel. Additionally, the autoFocusOnShow property is set to false to prevent unwanted focus behavior when the popover is displayed.

* fix: Stabilize focus and layout shift in Archived Chats dialog

Anchor dialog focus to the content element so rapid tabbing during the
virtualized table's loading state no longer escapes to the page's top
focus guard, and stabilize the columns memo to keep the focus trap intact.
Reserve a fixed height and stable scrollbar gutter, and drop the redundant
nested scroll wrapper in the shared DataTable to eliminate load-time
layout shift.

* fix: Add stable scrollbar gutter to SharedLinks DataTable

Enhance the layout stability of the SharedLinks component by adding a "scrollbar-gutter-stable" class to the DataTable. This change aims to prevent layout shifts during loading, improving the overall user experience.

* fix: Enhance keyboard accessibility and focus management in TokenUsageIndicator

Refactor the TokenUsageIndicator component to improve keyboard navigation and focus behavior. Introduced a useRef hook for the disclosure button to ensure focus remains on the gauge trigger when the popover is opened. Updated the popover's finalFocus property to return focus to the trigger on close, enhancing the overall user experience for keyboard users.
2026-06-21 12:53:24 -04:00
Danny Avila
1505fd5262
📦 chore: Bump @librechat/agents to v3.2.44
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
2026-06-21 08:39:10 -04:00