Commit graph

2196 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
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
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
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
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
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
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
5eb1c2c107
🖇️ feat: Reference Selected Chat Text with Multi-Quote Popup (#13868)
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
* 🖇️ feat: Reference Selected Chat Text with Multi-Quote Popup

Add a ChatGPT/Codex-style quote feature: selecting text in any message shows
an 'Add to chat' popup that accumulates removable quote chips above the
composer. On submit, the excerpts are merged into the user message text as
Markdown blockquotes (counted in the user message token count, not a system
message) and persisted on the message so they render on the user bubble and
survive reload.

- packages/api: add getReferencedQuotes + mergeQuotedText helpers (blockquote merge, length/count caps) with unit tests
- BaseClient.sendMessage: temporarily merge req.body.quotes into userMessage.text before buildMessages, restore clean text, persist quotes array
- data-schemas + data-provider: add optional quotes field to message schema/type
- client: pendingQuotesByConvoId atom, QuoteButton selection popup, PendingQuoteChips composer row, MessageQuotes persistent display
- useChatFunctions: drain pending quotes onto the message, carry forward on regenerate
- add localization keys and component/integration tests

* 🧪 test: Add Playwright e2e for chat quote feature

Add e2e/specs/mock/quotes.spec.ts covering select -> 'Add to chat' popup ->
chip -> send -> persistent reference block -> reload, plus multi-select
accumulation and chip removal. Selection is driven programmatically (real DOM
Range + dispatched mouseup) to summon the popup deterministically.

Add data-testid hooks (add-to-chat-button, pending-quote-chips, message-quotes)
to the quote components for stable selectors.

* 🛡️ fix: Address Codex review on quote feature

- Run PII filter + OpenAI moderation over req.body.quotes (P1): quoted excerpts
  are merged into the model-facing user message, so they must clear the same
  filters; a crafted quotes payload could otherwise bypass them. Adds tests.
- Carry quotes through edit/save-and-submit replays (overrideQuotes in
  EditMessage), mirroring overrideManualSkills, so edited turns keep context.
- Hide the quote UI for Assistants endpoints (which bypass BaseClient merge),
  so users can't queue quotes the assistant never receives.
- Clear pending quote/skill queues by resolved conversationId in useClearStates,
  not the UI index, so queued-but-unsent selections don't linger in Recoil.
- Cap queued quotes client-side at 10 to match the backend QUOTE_MAX_COUNT, so
  the composer never shows more quotes than are actually sent.

* 🧵 fix: Durably re-merge quotes + Codex round 2

Address Codex's re-review of the quote feature:

- Durable history re-merge (per maintainer decision): quotes are no longer
  merged at request time and stripped; instead each user message's persisted
  message.quotes is merged into its formatted content in AgentClient.buildMessages
  (new prependQuotes helper) for current AND historical turns. The model
  receives the referenced context on every prompt and the token count stays
  consistent with what was persisted; stored text stays clean for display.
- Attach normalized quotes to the user message in handleStartMethods (before
  getReqData/onStart) so the optimistic bubble, resumable abort metadata, and
  saved row all carry them (fixes the abort-metadata gap).
- Skip the quote drain entirely for Assistants endpoints in useChatFunctions,
  leaving the pending atom intact (UI is already hidden there).
- Normalize req.body.quotes via getReferencedQuotes before moderation/PII so
  only the trimmed/truncated/capped excerpts the model will receive are checked.
- Tests: prependQuotes unit tests; BaseClient quote tests assert early
  attachment + clean text; e2e now verifies the model receives the merged
  blockquote on the current turn and re-merged from history on a later turn
  (new E2E_ASSERT_QUOTE mock marker).

* 🔗 fix: Quote share/memo/abort/PII gaps (Codex round 3)

- Shared links: include quotes in the anonymized projection + SharedMessage
  type (+test) so the /share view renders the same reference blocks as the
  owner, mirroring manualSkills/alwaysAppliedSkills.
- MessageRender memo: compare quotes length so a server/resume copy whose only
  change is the quote list re-renders (the block no longer goes stale/missing).
- Resumable job metadata: include quotes in the userMessage written to
  GenerationJobManager so a reload/reconnect mid-stream reconstructs the chips.
- PII + moderation: also scan the merged blockquote+text exactly as the model
  receives it, so a secret split across a quote and the typed body (each clean
  alone) is caught (+cross-boundary test).
- e2e: make quote-add robust against the auto-scroll-dismisses-selection race
  via a retried select+click helper.

* 🛑 fix: Keep quotes on aborted turn's request message (Codex round 4)

abortMiddleware reconstructs finalEvent.requestMessage from jobData.userMessage
but only copied ids + text; include quotes so a stopped quoted turn keeps its
MessageQuotes in the UI and a regenerate-before-reload still sends the
referenced context. Completes the resumable-metadata fix from the prior round.

* 🧮 fix: Quote recount + preliminary abort metadata (Codex round 5)

- Force a canonical token recount for messages carrying quotes in
  AgentClient.buildMessages, so a plain text-only Save edit (which recomputes
  tokenCount from text alone) can't leave a stale, quote-excluding count that
  undercounts context on later turns — recount from the quote-merged copy
  self-heals it.
- Seed normalized quotes into the preliminary userMessage metadata
  (getPreliminaryUserMessage), so an abort during init/tool-loading (before
  onStart) still reconstructs the stopped turn's MessageQuotes.

*  fix: Add getReferencedQuotes to controller test mocks (CI)

request.js's getPreliminaryUserMessage now calls getReferencedQuotes; the
agents controller specs mock @librechat/api wholesale, so the mock must export
it or the call throws and cascades. Added a faithful mock (normalize/cap,
null when empty) to request.resumeMetadata.spec.js and jobReplacement.spec.js.

* 📐 fix: Quotes in context projection + resumable metadata (Codex round 6)

- Context-usage projection (resolveContextProjection): select message.quotes,
  prepend them into the projected user text, and recount quoted messages so the
  context gauge counts the same prompt the model receives (a text-only Save edit
  no longer makes the gauge undercount / over-report remaining budget).
- Resumable job metadata: trackUserMessage (created-event rewrite) and abortJob
  (final requestMessage) now carry quotes; SerializableJobData.userMessage and
  CreatedEvent.message gained an optional quotes field. With the cross-replica
  created-event spread, stopping/reconnecting a quoted turn after the created
  event keeps its MessageQuotes.

* 💬 feat: Collapse multi-select quotes into one chip with hover popup

Composer feedback: the quote chip area now shows a single chip — the excerpt
text for one selection, or a collapsed "{n} selections" pill for multiple,
with a hover popup (HoverCard) listing every excerpt and a per-item remove. The
chip is taller (py-1.5/text-sm) to read less skinny. Adds com_ui_quote_selections
and com_ui_remove_all_quotes; updates unit + e2e tests (e2e drives the count via
a data-quote-count hook and exercises the hover popup).

*  fix: Make multi-selection quote popup keyboard accessible

The collapsed "{n} selections" pill used a HoverCard, which Radix only opens on
pointer hover — its interactive content was unreachable by keyboard. Replaced it
with a Popover: the trigger is a real button that opens on click / Enter / Space
(focus moves into the list, each excerpt's × is tab-navigable, Escape closes and
restores focus), with hover-open preserved for mouse via controlled open state +
a close grace period. Hover-initiated opens skip auto-focus so they don't pull
focus off the composer. Adds an e2e asserting keyboard open/close.

* 📐 fix: Clamp the Add-to-chat button within the viewport (Codex round 7)

The floating selection button positioned via translate(-50%,-100%) (bottom-center
anchor) but clamped top/left as if they were its top-left, so a selection near
the viewport top or sides could render the button partly/fully offscreen. Now it
measures the button (ref + useLayoutEffect) and computes an on-screen top-left —
clamping by the full width within side margins and flipping below the selection
when there's no room above — with no transform, and stays hidden until measured
so it never flashes at an unclamped spot.

* ↩️ fix: Restore pending quotes on early-abort draft (Codex round 8)

When a turn is stopped before the created event (e.g. during tool/MCP init), the
final handler restores requestMessage.text to the draft, but the pending-quote
atom was already drained on submit — so a retry sent no quotes. The abort
requestMessage now carries quotes (preliminary metadata + abort fixes), so the
three early-abort/no-response draft-restore paths in useEventHandlers now also
re-queue pendingQuotesByConvoId from requestMessage.quotes.

*  fix: Use Ariakit Popover for quote selections (keyboard focus)

The multi-selection popup used a hand-rolled Radix Popover with Popover.Anchor +
a manual button, so Radix had no trigger to return focus to — Escape dumped
focus to the page top. Refactored to Ariakit (the codebase's popover primitive,
per DropdownPopup/Fork): the `PopoverDisclosure` is the real trigger, so Escape
closes and returns focus to the composer instead of the top of the page. Keyboard
opens (Enter/Space) autofocus into the list and tab through each excerpt's remove;
hover opens for mouse with autofocus suppressed so it never pulls focus off the
composer. e2e asserts the keyboard open/navigate/Escape flow keeps focus on a
real control (never BODY).
2026-06-21 08:33:11 -04:00
Danny Avila
e515063ffe
🔗 feat: Snapshot Files for Shared-Link Attachments (#13740)
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
* 🔗 feat: Snapshot Files for Shared-Link Attachments

Shared-link viewers could read a shared conversation snapshot but not its
attachments: file preview/download still went through the owner-scoped file
ACL (the /api/files router sits behind requireJwtAuth + owner/agent checks),
so anonymous viewers got 401s and authenticated non-owners got 403s — the
repeated `[fileAccess] denied` warnings seen for the preview poller.

Capture an immutable per-share file snapshot (embedded on the SharedLink
document, referencing the original stored object — no byte copy) at share
create/update, and serve those files through new share-scoped routes
authorized by the existing shared-link view permission (public/ACL) plus
snapshot membership, never the owner's live file ACL.

- data-schemas: fileSnapshots on the share doc; capture in create/update;
  read-time rewrite of filepath/preview to /api/share/:id/files/:fileId;
  getSharedLinkFile + lazy backfillSharedLinkFiles for legacy links
- api: GET /api/share/:shareId/files/:file_id[/download|/preview]; route
  context added to fileAccess denial logs
- packages/api: isFileSnapshotEnabled resolver (env + yaml)
- data-provider: interface.sharedLinks.snapshotFiles (default on) + client
  endpoints/services
- client: ShareContext.shareId wired to Image, preview hook, and downloads
- config: SHARED_LINKS_SNAPSHOT_FILES env override (default on)

* 🔒 fix: Address Codex review on shared-link file snapshots

Triage of the Codex review on PR #13740 (2 P1, 7 P2 — all valid):

- P1 (cross-user access): scope the snapshot lookup to the sharing user's own
  files so a message referencing another user's file_id can't widen access.
- P1 (stored XSS): the inline share-file route now serves only safe preview
  types inline (raster images/pdf); everything else is forced to attachment with
  X-Content-Type-Options: nosniff.
- Stream shared downloads by default; redirect to a signed URL only on
  ?direct=true (blob/XHR callers work without bucket CORS).
- Read preview status live from the file record (always current for deferred
  previews) and stop embedding extracted text in the share doc (16MB-limit risk).
- Only lazily backfill when the fileSnapshots field is absent (legacy), not on
  every snapshot miss.
- Backfill legacy shares before rewriting message URLs, and gate URL rewriting
  to public shares so non-public (ACL) shares keep prior behavior (img/anchor
  can't carry the bearer token).
- Frontend: only route a download through the share path when the file was
  actually snapshotted (rewritten href / filepath), else fall back.

* 🔑 feat: Authorize shared-link files for non-public shares via cookie

Extends shared-link file access to non-public (ACL) shares (Codex finding 5).
`<img>`/anchor requests can't carry the bearer access token, so non-public
shares previously 401'd on file loads. Add an optional cookie-auth fallback on
the share file routes that resolves the viewer from the `refreshToken` cookie
(or signed `openid_user_id` cookie) — the same mechanism secure image links use
(validateImageRequest) — then let canAccessSharedLink run the viewer's ACL check.

- new middleware optionalShareFileAuth (+ unit spec); applied to the three
  share file routes after optionalJwtAuth
- URL rewriting in getSharedMessages is no longer gated to public shares (the
  route now authorizes header-less requests), so files work uniformly across
  public and non-public shares; revert the now-unused req.sharePublic plumbing

* 🔒 fix: Second Codex pass on shared-link file snapshots

Addresses the follow-up Codex findings on PR #13740:

- Don't snapshot transient text-source files: FileSources.text filepaths are
  Multer temp paths the upload route deletes, so they can't be streamed —
  removed from the streamable allowlist.
- Unset stale snapshots on a disabled-feature update: updateSharedLink now
  $unsets fileSnapshots when snapshotFiles is false, so an opted-out update
  can't keep serving file ids the update dropped.
- Load tenant config after share resolution: configMiddleware now runs after
  canAccessSharedLink (which enters the share's tenant ALS context), so
  per-tenant interface.sharedLinks.snapshotFiles overrides apply to anonymous
  public views.
- Return a clean 404 when the snapshotted object is gone: resolveShareFile now
  requires the live file record and 404s if it's been deleted/expired, instead
  of letting the stream error after headers are sent (ENOENT / 500).

(The re-flagged P1 about private-viewer rewriting was already fixed in the prior
commit's cookie-auth change.)

* 🔒 fix: Third Codex pass on shared-link file snapshots

Addresses the third Codex review pass on PR #13740:

- P1: keep shared previews/files pinned to the snapshotted version. Snapshot the
  small previewRevision; resolveShareFile 404s when the live file's revision no
  longer matches (file_id reused/overwritten by a later turn), so old links can't
  surface post-share content — covers both preview text and streamed bytes.
- Honor the toggle as a kill switch: resolveShareFile 404s when snapshotFiles is
  disabled, instead of only skipping backfill, so disabling stops serving
  already-snapshotted file URLs.
- Lazy-sweep orphaned 'pending' previews to 'failed' in the share preview route
  (mirrors the owner route) so the client poller reaches a terminal state.
- Resolve the cookie-fallback user in runAsSystem so strict tenant isolation
  doesn't throw before canAccessSharedLink establishes the share tenant context.

*  feat: Per-link "share files" checkbox for shared links

Add a checkbox to the share-link dialog (checked by default) letting the user
choose whether to include the conversation's files in the shared link, with
copy explaining images/files won't be visible to viewers otherwise. Opting out
skips snapshot creation/serving for that link.

- client: ShareButton renders the checkbox gated on the new
  startupConfig.sharedLinksSnapshotFilesEnabled flag; state threads through
  SharedLinkButton into the create/update mutations as `snapshotFiles`.
- data-provider: createSharedLink/updateSharedLink send `snapshotFiles` in the
  body; TStartupConfig gains `sharedLinksSnapshotFilesEnabled`.
- api: POST/PATCH /api/share compute snapshotFiles as
  isFileSnapshotEnabled(req.config) && body.snapshotFiles !== false (admin gate
  AND per-link opt-out); config.js exposes the effective enabled flag to clients.
- en locale: com_ui_share_files (+ _description).

* 🐛 fix: Make the "share files" opt-out actually hide files

Unchecking "share files" at creation didn't hide anything: the shared message
JSON still carried each file's original (e.g. static-served) path, and because
opting out only meant "no fileSnapshots field" — indistinguishable from a legacy
link — getSharedMessages would backfill snapshots on first view whenever the
admin feature was on, re-enabling files entirely.

Fix by persisting and honoring the per-link choice:
- Store `snapshotFiles` (boolean) on the SharedLink so opt-out is distinct from a
  legacy link; set it on create and update.
- getSharedMessages computes includeFiles = adminEnabled && link not opted out;
  when excluded it strips files/attachments from the payload (no original-path
  leak) and never backfills the opted-out link.
- Surface the stored choice via getSharedLink so the dialog checkbox reflects an
  existing link's actual setting instead of always defaulting to checked.

Note: changing the checkbox on an already-created link still applies only when
the link is refreshed (which regenerates the URL) — a UX follow-up.

* 🔒 fix: Close remaining shared-link file opt-out leaks (Codex)

Follow-up to the per-link opt-out, addressing the third Codex pass:

- Honor the opt-out on the file route too: getSharedLinkFile now returns the
  link's `optedOut` choice; resolveShareFile 404s (and never backfills) an
  opted-out link, so a direct /files/:id request can't re-create snapshots.
- Make read/serve viewer-independent: the gate no longer uses the viewer's
  resolved config (isFileSnapshotEnabled(req.config)) — it uses the link's stored
  choice plus a global env-only kill switch (isFileSnapshotKillSwitchActive). A
  viewer's own interface.sharedLinks.snapshotFiles can no longer hide a link's
  files. Create/update still use the creator's config to set the per-link choice.
- Neutralize render URLs for non-snapshotted files: applyShareFileRoute now
  strips filepath/preview for any file/attachment not in the snapshot, so the
  owner's original (e.g. static) path can't be loaded through the share.

* 🔒 fix: Harden shared-file version pinning and local path handling (Codex)

- Refuse reused/overwritten file snapshots more broadly: resolveShareFile now
  refuses to serve when either previewRevision OR `bytes` changed vs the
  snapshot. `bytes` catches non-office reused outputs (e.g. code-exec
  same-filename images that lack previewRevision) and is stable across S3 URL
  refresh and the pending->ready transition. Same-size content swaps remain a
  best-effort gap inherent to the no-byte-copy design.
- Strip cache-busting query strings before local streaming: code-output images
  add `?v=...` to filepath; the share route now splits it off so getLocalFileStream
  resolves the real filename instead of a literal `*.png?v=...` path.

* 💬 fix: Clarify that file-sharing changes apply on link refresh

For an already-created shared link, changing the "share files" checkbox only
takes effect when the link is refreshed (which regenerates the snapshot). Add a
note under the checkbox, shown only when a link already exists, so the behavior
isn't surprising: "Refresh the link to apply this change — files are snapshotted
when the link is refreshed."
2026-06-20 23:05:13 -04:00
Danny Avila
f76a5faa9e
📌 feat: Seed Default Pinned Tools and MCP Dropdown via Interface Config (#13865)
*  feat: Add `defaultPinnedTools` interface config for default tool & MCP pinning

Adds an `interface.defaultPinnedTools` string array letting admins pin tools and the MCP servers dropdown to the prompt bar by default for all users.

- Tool keys (artifacts, execute_code, web_search, file_search, skills) pin their badge via `useToolToggle`.

- The keyword `'mcp'` or a configured MCP server name pins the MCP dropdown via `useMCPSelect`.

- Only seeds initial state; a user's stored pin preference always wins. When unset, tools start unpinned and the MCP dropdown keeps its legacy default (pinned).

Unifies the approaches from #11646 (pinnedTools) and #9251 (defaultPinMcp) into one config key.

* 🐛 fix: Apply defaultPinnedTools pin once startupConfig resolves

On a cold load, useToolToggle can mount before useGetStartupConfig() resolves, so defaultPinned starts false and useLocalStorageAlt eagerly persists it; its init effect never re-runs for the later config-driven default. Fresh users would then miss the admin-configured default pin whenever startup config was not already cached.

Capture whether a pin preference existed before mount (pre-seed) and, once startupConfig arrives, apply the real default for users with no prior preference. Runs once and never overrides an existing stored pin, so the conservative behavior for existing users is preserved.

* 🐛 fix: Preserve pin clicks made before startupConfig resolves

The cold-load default-seeding effect captured the stored-pin state only at mount, so a pin toggled before startupConfig resolved was treated as no-preference and overwritten when the admin default applied.

Track explicit pin toggles via a ref (set through the returned setter) and skip the default application when the user has interacted in-session — in addition to the existing stored-preference guard.
2026-06-20 13:40:10 -04:00
Serhii Zghama
8824e8f918
🚪 fix: Gate Artifacts Toggle on Agent Capability Flag (#13665)
* fix: hide artifacts toggle when capability is disabled

The artifacts badge ignored the agent capabilities config, so a pinned
toggle stayed visible after the artifacts capability was turned off.
Gate the component on artifactsEnabled via useAgentCapabilities, matching
how Skills, FileSearch and CodeInterpreter already handle their capability.

* style: fix import order in Artifacts.tsx

* style: Sort mutation type imports

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2026-06-20 11:52:36 -04:00
Danny Avila
9dd0df9d61
🔑 feat: Surface User-Provided API Keys in Settings, Scoped to Reachable Endpoints (#13864)
Adds a "Provider API keys" entry under Settings → Data controls → API keys
that lists every endpoint requiring a user-provided credential and lets users
set or rotate its key via SetKeyDialog. This is always reachable, so keys can
be managed even when `interface.modelSelect` is hidden by `modelSpecs`.

The endpoint list is filtered the same way the mention popover and model
selector menu are:
- No modelSpecs → every user-provided endpoint.
- modelSpecs configured → limited to spec endpoints ∪ `modelSpecs.addedEndpoints`.
- agents reachable (with access) → expanded to the agents `allowedProviders`
  (all providers when unrestricted).

Reworks #13303 onto the registry-driven Settings dialog (#13722); the prior
standalone tab and the `APIKeys` directory are superseded (the latter also
collided with the agent `ApiKeys` feature from #13819).
2026-06-20 10:54:30 -04:00
Danny Avila
f8aa45d05e
🔚 feat: Add Bottom Terminus Node to Message Minimap Navigation (#13853)
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
*  feat: Add scroll-to-bottom terminus node to MessageNav

Append the chat's bottom (#messages-end) as a terminal rib in the message
minimap so it is reachable by click, drag-scrub, and the down chevron like
any message. Rendered as a distinct centered dot rather than a line rib, and
gated on the #messages-end sentinel actually existing.

Also clamp each rib's snap target to the container's max scroll so the down
chevron no longer stays stuck enabled at the bottom (the terminus can never
scroll its top to the container top).

* 🐛 fix: Scope MessageNav terminus to its own scroll container

The terminus rib stored the shared constant id 'messages-end', which is
rendered once per MessagesView. With multiple navs mounted, the global
document.getElementById lookups resolved the first chat's sentinel, breaking
the per-instance isolation guaranteed by the existing multi-instance tests.

Resolve the terminus via the nav's own scrollableRef container
(querySelector), leaving the globally-unique message ids on the fast
getElementById path. Adds a multi-instance test covering the terminus.
2026-06-19 14:10:59 -04:00
Danny Avila
e6fc232ed2
🌍 i18n: Update translation.json with latest translations (#13836) 2026-06-19 10:42:15 -04:00
Danny Avila
268fcbb78d
🕐 feat: Add promptCacheTtl model parameter for 1h/5m cache duration (#13835)
Some checks failed
Publish `librechat-data-provider` to NPM / pack (push) Waiting to run
Publish `librechat-data-provider` 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
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
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
Publish `@librechat/data-schemas` to NPM / pack (push) Has been cancelled
Publish `@librechat/data-schemas` to NPM / publish-npm (push) Has been cancelled
* 🕐 feat: Add promptCacheTtl model parameter for 1h/5m cache duration

Adds a user-configurable `promptCacheTtl` parameter (dropdown: 5m | 1h)
alongside the existing `promptCache` toggle for Anthropic, Bedrock, and
OpenRouter endpoints. Default is undefined so the agents SDK applies its
own default (1h), letting users opt down to the legacy 5m TTL.

- data-provider: schema, parameterSettings dropdown, types, bedrock picks
- data-schemas: convo/preset types + mongoose defaults
- api: thread promptCacheTtl into anthropic + openai(OpenRouter) llmConfig
- i18n: en translation keys for label/description/default placeholder
- tests: anthropic llm.spec coverage for set + unset cases

* 🔧 fix: Tie Bedrock promptCacheTtl to promptCache + thread OpenRouter TTL params (Codex review)

- bedrock.ts: clear promptCacheTtl whenever promptCache is off/unsupported,
  so an unsupported 1h is never sent on a non-caching Bedrock request
- openai/llm.ts: resolve promptCacheTtl through the same defaultParams/
  addParams/dropParams machinery as promptCache (via promptCacheTtlValue)
  so OpenRouter custom endpoints can configure/override/drop it
- tests: bedrock TTL-tied-to-promptCache cases; OpenRouter TTL default/add/drop

* 🎨 style: Sort imports in openai/llm.spec.ts (CI sort-imports)

*  test: Prove OpenRouter TTL-only selection honors promptCache default (Codex review)

OPENROUTER_DEFAULT_PARAMS injects promptCache:true into defaultParams, so a
TTL-only dropdown selection (promptCacheTtl set, promptCache switch untouched)
still resolves caching on and forwards the TTL. Add regression tests via the
real getOpenAIConfig entry point: TTL-only -> promptCache+TTL both set;
explicit promptCache:false -> both dropped.

* 🔖 chore: Bump librechat-data-provider to 0.8.506

* 🔧 fix: Drop Anthropic promptCacheTtl when promptCache is dropped (Codex review)

dropParams: ['promptCache'] deleted requestOptions.promptCache but left
promptCacheTtl behind, so the admin opt-out path could still carry a TTL
on a request with caching disabled. Clear the TTL alongside promptCache.
2026-06-18 16:36:43 -04:00
Danny Avila
2fcba914f7
🔗 fix: Surface Share Permissions Load Error as Alert Button With Tooltip (#13833)
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-18 13:37:49 -04:00
Marco Beretta
a468becf8c
🔑 feat: Agent API Keys management UI in Settings → Data controls (#13819)
* feat(client): add optional copyButton slot to SecretInput

* refactor: redesign Agent API Keys settings into a Data controls dialog

* chore: remove unused com_ui_last_used i18n key
2026-06-18 12:58:17 -04:00
Marco Beretta
9de3249e9c
🎛️ feat: Redesign Settings with Registry-Driven Dialog, Search, and Mobile Drill-In (#13722)
* i18n: add settings reorganization keys

* feat(settings): add tab/section types and tab metadata

* feat(settings): add useSettingsContext guard hook

* feat(settings): add pure settings search filter with tests

* feat(settings): extract selectors and add control wrappers

* feat(settings): add setting registry, memory and billing controls, integrity test

* feat(settings): add Section and Advanced disclosure with test

* feat(settings): add content pane with tab and search views

* feat(settings): add sidebar and dialog shell with tests

* refactor(settings): wire new dialog and remove superseded containers

* fix(settings): restore speech external engine option, escape-to-clear search, results a11y

- SpeechControls.tsx: read sttExternal/ttsExternal from useGetCustomConfigSpeechQuery
  instead of hardcoding false, so external engine options appear on qualifying deployments
- Sidebar: Escape clears search input when non-empty, stops propagation to avoid closing dialog
- Content: persistent aria-live="polite" wrapper covers both populated results and empty state
- context: useMemo on returned ctx object so Content's useMemo deps are referentially stable
- locales/README.md: update stale path from deleted General.tsx to Selectors.tsx

* refactor(settings): reorganize categories, remove advanced disclosure, add About

- Re-categorize settings into logical groups (username display -> Chat/Messages,
  keep-screen-awake -> Accessibility, fork/prompts surfaced into Chat sections)
- Dissolve thin Personalization tab; move Memory into Data & Privacy
- Remove the Advanced collapsible; all settings always visible, destructive
  actions grouped in an always-visible Danger zone
- Wire the new About tab into the registry-driven dialog
- Standardize spacing with bordered, evenly-divided section cards
- Use semantic text-text-* / border tokens so dark mode renders correctly
- Sync LangSelector language-loading indicator from dev

* feat(settings): move archived chats to the account menu

Add an Archived chats item to the account dropdown next to My Files,
opening the archived chats table in a modal. Removes it from the
settings dialog where it no longer fit the data/privacy grouping.

* feat(settings): polish About panel and use shared CopyButton

- Flatten the build-info into a single divided key/value list (drop the
  redundant inner card now that it sits inside a section card)
- Replace the hand-rolled copy button with the shared animated CopyButton
- Shorten the copied label so it fits the button without clipping

* fix(settings): set primary text color on setting rows for dark mode

Leaf control labels rendered without a text color and fell back to the
browser default (black), making them invisible on the dark panel. Set
text-text-primary on the section and search-results row containers so
labels inherit a visible color, matching the old container behavior.

* fix(settings): use visible icon for dialog close button

The plain multiplication-sign close button had no text color and was
invisible on the dark panel. Replace it with the lucide X icon using
text-text-secondary/hover:text-text-primary so it shows in both themes.

* fix(nav): drop focus ring on account menu items, use hover background only

The account-settings popover drew a 2px ring around the active menu item.
Remove that override so items show only the standard hover background,
consistent with every other menu.

* fix(settings): replace native search clear with a real X button

The settings search used type=search, whose native WebKit clear control
rendered as a blue X. Switch to a text input and add a real lucide X
clear button styled text-text-secondary, shown only when there's a query.

* fix(speech): disable dependent dropdowns and switches when STT/TTS is off

Add a disabled prop to the shared Dropdown component, then gate the
speech engine/voice/language dropdowns and the automatic-playback switch
on their parent toggle (speechToText / textToSpeech), matching the
controls that already disabled correctly.

* feat(settings): mobile drill-in navigation for settings tabs

On small screens the horizontal scrolling tab row is replaced with a
full-width vertical list (with chevrons); tapping a tab drills into its
content with a Back header. Searching shows results full-width. Desktop
keeps the side-by-side sidebar + content layout unchanged.

* chore(settings): remove orphaned i18n keys, fix import order and review notes

- Drop the i18n keys left unused after the refactor (old Commands/Balance/
  Personalization tab labels, the Speech simple/advanced labels, and the
  former About section headings)
- Sort imports in the rebased files the lint-staged hook never touched
- Guard the language fallback against an empty navigator.languages
- Import the RefObject type instead of leaning on the React namespace

* feat(settings): searchable language dropdown

Add an opt-in searchable mode to the shared Dropdown (Ariakit Select +
Combobox) and use it for the language selector, which has 40+ options.
The trigger styling is unchanged so it stays consistent with the other
settings rows; only the popover gains a filter input.

Accessibility: the filtered listbox is labeled, the empty state is moved
out of the listbox and announced via an aria-live status region, and the
decorative selected-state checkmark is hidden from assistive tech.

* fix(settings): restore guards dropped in dialog refactor

- Fall back to the General tab when the active tab becomes hidden
  (e.g. About when buildInfo is disabled) instead of rendering an
  empty panel.
- Normalize a deprecated/invalid engineTTS (e.g. 'edge') back to
  browser during speech init so read-aloud controls keep rendering.
- Hide the cloud browser voices toggle unless Browser TTS is active.

* test(e2e): match agent-creation toast exactly to avoid SR-announce collision

The agent builder spec asserted the creation toast with a non-exact
getByText, which also matched Radix Toast's transient role="status"
announce region ("Notification Successfully created ..."), causing a
strict-mode violation. Mirror the mcp spec by using { exact: true }.

* fix(settings): render the active panel as a tabpanel

Wrap the non-search settings body in Tabs.Content so the selected
panel gets role=tabpanel with Radix's id/aria-labelledby wiring,
resolving the aria-controls target on each tab trigger. Search
results stay a labeled live region (the tab list is hidden during
mobile search, so a tabpanel aria-labelledby would dangle).
2026-06-18 08:51:07 -04:00