✨ feat: MCP Apps Extension Support - #11799
KyleKincer wants to merge 8 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR implements the MCP Apps extension, enabling MCP servers to render interactive HTML interfaces (forms, dashboards, visualizations) inline in the chat. The implementation delivers a complete end-to-end solution with backend proxy endpoints, metadata pipeline, sandboxed iframe rendering, and config-driven security controls.
Changes:
- Adds backend controller (
mcpApps.js) with resource/tool-call proxy endpoints, domain filtering, tool visibility enforcement, and config-gating - Extends MCP package with metadata pipeline (
parsers.ts), tool visibility filtering (MCPServerInspector.ts), and manager methods for resource/tool operations - Implements frontend MCPApp module with dual bridge architecture (custom JSON-RPC + optional SDK adapter), two-layer sandboxed iframe rendering, fullscreen support, and state preservation
Reviewed changes
Copilot reviewed 35 out of 36 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
packages/data-provider/src/types/assistants.ts |
Adds mcp_app tool type enum |
packages/data-provider/src/schemas.ts |
Defines MCPAppArtifact type and attachment metadata |
packages/data-provider/src/config.ts |
Adds mcpSettings.apps and appSettings configuration schema |
packages/api/src/mcp/types/index.ts |
Adds MCPAppArtifact import and _meta field to LCFunctionTool |
packages/api/src/mcp/registry/__tests__/MCPServerInspector.test.ts |
Tests tool visibility filtering (model vs app-only tools) |
packages/api/src/mcp/registry/MCPServerInspector.ts |
Implements getAllToolFunctions() returning separate modelTools and allTools |
packages/api/src/mcp/parsers.ts |
Creates mcp_app artifacts when toolUiMeta.resourceUri is present |
packages/api/src/mcp/connection.ts |
Advertises io.modelcontextprotocol/ui capability during handshake |
packages/api/src/mcp/__tests__/parsers.test.ts |
Tests MCP app artifact creation and merging with other artifacts |
packages/api/src/mcp/__tests__/MCPManager.test.ts |
Tests metadata fallback tiers and tools/list optimization |
packages/api/src/mcp/MCPManager.ts |
Adds readResource(), appToolCall(), getAllToolsForServer() with three-tier metadata fallback |
package-lock.json |
Adds @modelcontextprotocol/ext-apps and platform-specific dependencies |
librechat.example.yaml |
Documents MCP Apps configuration options |
client/src/components/Chat/Messages/Content/__tests__/ToolCall.test.tsx |
Tests MCP app attachment stability across re-renders |
client/src/components/Chat/Messages/Content/ToolCall.tsx |
Integrates MCPAppInline with stable ref to preserve app state |
client/src/components/Chat/Messages/Content/MCPApp/mcpAppUtils.ts |
Implements CSP/permissions builders and backend proxy fetch functions |
client/src/components/Chat/Messages/Content/MCPApp/mcpAppTheme.ts |
Provides host context with theme, display mode, and container dimensions |
client/src/components/Chat/Messages/Content/MCPApp/index.ts |
Exports MCPApp components and bridge |
client/src/components/Chat/Messages/Content/MCPApp/createMCPAppBridge.ts |
Factory for custom bridge or SDK adapter based on config |
client/src/components/Chat/Messages/Content/MCPApp/__tests__/MCPAppContainer.test.tsx |
Tests fullscreen lifecycle, state preservation, and geometry tracking |
client/src/components/Chat/Messages/Content/MCPApp/__tests__/MCPAppBridge.test.ts |
Tests JSON-RPC protocol, display modes, and context updates |
client/src/components/Chat/Messages/Content/MCPApp/MCPAppInline.tsx |
Fetches HTML resource and delegates to MCPAppContainer |
client/src/components/Chat/Messages/Content/MCPApp/MCPAppFullscreen.tsx |
Renders fullscreen portal with close button and escape key handler |
client/src/components/Chat/Messages/Content/MCPApp/MCPAppContainer.tsx |
Manages iframe lifecycle, bridge, portal rendering, and display modes |
client/src/components/Chat/Messages/Content/MCPApp/MCPAppBridgeSDKAdapter.ts |
Wraps @modelcontextprotocol/ext-apps SDK with LibreChat backend proxies |
client/src/components/Chat/Messages/Content/MCPApp/MCPAppBridge.ts |
Custom JSON-RPC bridge with tool/resource proxying and display mode handling |
client/src/components/Chat/Messages/Content/ContentParts.tsx |
Uses stable keys for tool call parts to prevent unnecessary re-renders |
client/src/components/Chat/Input/ChatForm.tsx |
Adds z-50 to ensure composer stays above inline MCP apps |
client/src/components/Chat/Header.tsx |
Changes z-index from z-10 to z-30 for proper layering |
client/src/components/Chat/ChatView.tsx |
Adds data-chat-view-root attribute for portal targeting |
client/public/mcp-sandbox.html |
Two-layer sandbox proxy with CSP injection and message validation |
client/package.json |
Adds @modelcontextprotocol/ext-apps dependency |
api/server/routes/mcp.js |
Wires /resources/read, /app-tool-call, and /sandbox endpoints |
api/server/controllers/mcpApps.test.js |
Tests domain filtering, visibility enforcement, and config gating |
api/server/controllers/mcpApps.js |
Implements resource read, tool call proxy, and sandbox serving with security controls |
api/server/controllers/agents/callbacks.js |
Gates mcp_app artifact processing on mcpSettings.apps config |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Implements MCP Apps extension support, enabling MCP servers to render interactive HTML interfaces (forms, dashboards, visualizations) inline in the chat via sandboxed iframes with a bidirectional JSON-RPC bridge. Closes LibreChat-AI#10641 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
914254e to
1050679
Compare
|
Thanks for the PR @KyleKincer I may not get to reviewing this week but I would greatly appreciate if you could point me to a few MCP servers and their respective configs to test with when I get the chance, |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 44 out of 45 changed files in this pull request and generated 8 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } catch (error) { | ||
| console.error('[MCPAppContainer] Failed to bootstrap opaque sandbox source:', error); | ||
| if (active) { | ||
| setSandboxSrc(SANDBOX_ENDPOINT); |
There was a problem hiding this comment.
The sandboxSrc initialization falls back to SANDBOX_ENDPOINT (direct iframe src) on error, which bypasses the opaque-origin blob URL security mechanism. An attacker who can intercept or poison the sandbox HTML could gain same-origin access. Consider failing closed (showing error UI) rather than falling back to a less secure loading method.
| setSandboxSrc(SANDBOX_ENDPOINT); | |
| // Fail closed: do not fall back to direct SANDBOX_ENDPOINT iframe src. | |
| setSandboxSrc('about:blank'); |
| try { | ||
| if (document.referrer) { | ||
| return new URL(document.referrer).origin; | ||
| } | ||
| } catch { | ||
| // Fall back to wildcard when referrer is unavailable. | ||
| } | ||
| return '*'; |
There was a problem hiding this comment.
The wildcard origin '*' is used when document.referrer is unavailable. While this is documented as a fallback, it bypasses origin validation entirely. Consider requiring explicit configuration of allowed origins rather than falling back to wildcard, or at minimum add a warning log when wildcard is used so administrators are aware of the reduced security posture.
| try { | |
| if (document.referrer) { | |
| return new URL(document.referrer).origin; | |
| } | |
| } catch { | |
| // Fall back to wildcard when referrer is unavailable. | |
| } | |
| return '*'; | |
| let origin = '*'; | |
| try { | |
| if (document.referrer) { | |
| origin = new URL(document.referrer).origin; | |
| } else { | |
| console.warn( | |
| '[MCP Sandbox] document.referrer is empty; using wildcard parent origin "*". ' + | |
| 'This reduces origin validation strictness.' | |
| ); | |
| } | |
| } catch (err) { | |
| console.warn( | |
| '[MCP Sandbox] Failed to parse document.referrer; using wildcard parent origin "*". ' + | |
| 'This reduces origin validation strictness.', | |
| err | |
| ); | |
| } | |
| return origin; |
| return `part-${messageId}-tool-${toolCallId}`; | ||
| } | ||
| } | ||
|
|
||
| if (part.type === ContentTypes.TEXT && part.tool_call_ids != null && part.tool_call_ids.length > 0) { | ||
| return `part-${messageId}-tool-links-${part.tool_call_ids.join(',')}`; |
There was a problem hiding this comment.
The getPartKey function generates stable keys based on toolCallId, which is good for preventing remounts. However, if two tool calls somehow have the same ID within a message (edge case), they would share the same key causing React reconciliation issues. Consider adding the part index as a secondary key component to guarantee uniqueness: part-${messageId}-tool-${toolCallId}-${idx}
| return `part-${messageId}-tool-${toolCallId}`; | |
| } | |
| } | |
| if (part.type === ContentTypes.TEXT && part.tool_call_ids != null && part.tool_call_ids.length > 0) { | |
| return `part-${messageId}-tool-links-${part.tool_call_ids.join(',')}`; | |
| return `part-${messageId}-tool-${toolCallId}-${idx}`; | |
| } | |
| } | |
| if (part.type === ContentTypes.TEXT && part.tool_call_ids != null && part.tool_call_ids.length > 0) { | |
| return `part-${messageId}-tool-links-${part.tool_call_ids.join(',')}-${idx}`; |
| const fullHtml = injectIntoHead(html, cspMeta + SANDBOX_BASE_STYLE); | ||
|
|
||
| const blob = new Blob([fullHtml], { type: 'text/html' }); | ||
| innerFrame.src = URL.createObjectURL(blob); |
There was a problem hiding this comment.
The opaque blob URL created for the inner iframe is never revoked with URL.revokeObjectURL(). This can lead to memory leaks if many MCP apps are loaded during a session. Consider storing the blob URL ref and revoking it in the iframe cleanup (when innerFrame is removed or on window unload).
| * @param {object} res - Express response | ||
| */ | ||
| const serveMCPSandbox = async (req, res) => { | ||
| try { |
There was a problem hiding this comment.
The sandbox endpoint lacks authentication but accesses req.user?.role to determine if apps are enabled. When req.user is undefined (unauthenticated requests), getMCPAppsConfig will use a default role, which could bypass intended access controls. This endpoint should either require authentication or explicitly handle the anonymous case with a documented default behavior.
| try { | |
| try { | |
| // Require authentication to avoid relying on a default role for anonymous users | |
| if (!req.user) { | |
| return res.status(401).json({ error: 'Unauthorized' }); | |
| } |
| if (params.oauthTokens) { | ||
| this.oauthTokens = params.oauthTokens; | ||
| } | ||
| const enableApps = params.enableApps !== false; // default true |
There was a problem hiding this comment.
The enableApps parameter defaults to true in MCPConnection but is never actually set based on the mcpSettings.apps configuration. This means connections will always advertise MCP Apps capability regardless of the apps configuration setting. The MCPConnectionFactory should pass the enableApps flag from the application configuration when creating connections.
| return res.sendFile(sandboxPath, (error) => { | ||
| if (error) { | ||
| logger.error('[serveMCPSandbox] Error:', error); | ||
| if (!res.headersSent) { | ||
| res.status(500).json({ error: 'Failed to load MCP sandbox' }); | ||
| } | ||
| } | ||
| }); |
There was a problem hiding this comment.
The error handler sendFile callback checks res.headersSent before sending an error response, but then always tries to send JSON. If headers are already sent (e.g., partial file transfer), attempting to send JSON will cause an error. Consider logging only when headers are sent, or ensure the error path cannot be reached after headers are sent.
| onSubmit={methods.handleSubmit(submitMessage)} | ||
| className={cn( | ||
| 'mx-auto flex w-full flex-row gap-3 transition-[max-width] duration-300 sm:px-2', | ||
| 'relative z-50 mx-auto flex w-full flex-row gap-3 transition-[max-width] duration-300 sm:px-2', |
There was a problem hiding this comment.
The z-index value 50 for the chat form may conflict with the fullscreen MCP app which uses z-index 2147483647 (max safe integer). While this specific case works, consider documenting the z-index layering strategy or using CSS custom properties to centralize z-index management across components that can overlap (header=30, form=50, fullscreen=max).
Standalone server for testing LibreChat's MCP Apps host implementation (PR LibreChat-AI/LibreChat#11799). Exercises all host bridge methods including tools/call, resources/read, ui/message, display modes, model context, and teardown. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Hey @danny-avila — I put together a test server you can use to exercise the full MCP Apps host implementation: https://github.com/KyleKincer/librechat-mcp-apps-test-server It's a single MCP server that registers an interactive HTML app as a resource. When the model calls To run it: git clone https://github.com/KyleKincer/librechat-mcp-apps-test-server.git
cd librechat-mcp-apps-test-server
npm install
npm startThen add this to mcpSettings:
apps: true
mcpServers:
mcp_apps_integration:
type: streamable-http
url: http://localhost:3099/mcpRestart the backend, and in chat ask the model to call |
|
Great work on this overall, the architecture is solid and the test coverage is thorough. A couple of things need to be addressed before this can merge though. CRITICALs The biggest one is the sandbox fallback in // Don't do this:
setSandboxSrc(SANDBOX_ENDPOINT);
// Do this:
setSandboxSrc('about:blank');
// + render an error stateSecond: Third: if (!uri.startsWith('ui://')) {
return res.status(400).json({ error: 'uri must use the ui:// scheme' });
}MAJORs Dead ternary in const role = message.role === 'user' ? 'user' : 'user';Both branches are identical. This needs to actually validate the role.
Blob URL leak in The
Minors
|
|
On top of this, there is a merge conflict and ESLint issue, please address them as well. |
|
Thanks for the detailed pass @danny-avila — I addressed all items in your review and pushed the updates. Highlights:
Validation run:
|
|
hello @danny-avila , anything I can do to help keep this one moving? |
# Conflicts: # api/server/controllers/agents/client.js # client/package.json # client/src/components/Chat/ChatView.tsx # client/src/components/Chat/Header.tsx # client/src/components/Chat/Messages/Content/ContentParts.tsx # client/src/components/Share/ShareMessagesProvider.tsx # client/src/hooks/Chat/useChatHelpers.ts # package-lock.json # packages/api/src/mcp/ConnectionsRepository.ts # packages/api/src/mcp/MCPConnectionFactory.ts # packages/api/src/mcp/MCPManager.ts # packages/api/src/mcp/UserConnectionManager.ts # packages/api/src/mcp/connection.ts # packages/api/src/mcp/parsers.ts # packages/api/src/mcp/registry/MCPServerInspector.ts # packages/api/src/mcp/types/index.ts
|
Hit a release-blocker bug while testing this PR end-to-end. Posting in case it helps. Symptom: authenticated users see "Failed to load the secure MCP app sandbox" — no MCP App ever renders. Console shows: Server log confirms Root cause: the client fetches the sandbox template with raw fetch("/api/mcp/sandbox", { method: "GET", credentials: "same-origin", cache: "no-store" })
Repro:
Fix options:
I went with (b) locally to keep testing and the rest of the surface (bridge, |
Adds an "Upstream context and related work" section pointing at relevant work on danny-avila/LibreChat: - PR LibreChat-AI#11799 + Issue LibreChat-AI#10641: direct overlap with this plan's MCP Apps work; lists the 10 axes on which v8 intentionally diverges (self-contained HTML, no direct browser networking, one proxy per instance, hop-specific relay validation, manifest-hash approval, etc.). - Issue LibreChat-AI#11997: the only upstream artifact for MCP Tasks (no PR yet); plan's Tasks work is greenfield. - PR LibreChat-AI#12850: 307/308 redirect handling and credential stripping is merged into dev but NOT in HEAD 738003b. Earlier revisions of this plan claimed main already had it; corrected. Phase 0 now tracks the upstream merge or ports the work if it slips. - PR LibreChat-AI#12535, LibreChat-AI#12853, LibreChat-AI#12910, Issue LibreChat-AI#12802: adjacent transport reliability and OAuth hygiene worth tracking. - PRs already in HEAD listed for context (LibreChat-AI#12782, LibreChat-AI#12763, LibreChat-AI#12755, LibreChat-AI#12745, LibreChat-AI#12812). - Notably absent upstream: session-id reuse correctness, header consistency, 404 → re-init, per-user token scoping, outstanding-task revalidation, legacy renderer retirement. References section reorganized into Specs / Upstream LibreChat / MDN subsections. https://claude.ai/code/session_011NZqb4xN9QcXpdY2LCtnuH
…s Hardened split) Restructures the implementation strategy after the eighth review: - Stop parallel-building. Phase 1 inherits upstream PR LibreChat-AI#11799 as the Apps Preview substrate (per-instance outer iframes, same-server binding, /api/mcp/sandbox auth, ui:// URI validation, capability gating, mcp_app artifact, test server, stableMCPAppRef) and verifies these with regression tests rather than re-implementing them. - Split Apps phasing into: * Phase 2P (Apps Preview, ~1-2w): adopt LibreChat-AI#11799 substantially as-is on chat surface only; consolidate to single ACL; truthful HostContext; payload truncation; rate limits; fullscreen behind appSettings.allowFullscreen; capability advertise behind MCP_APPS_PREVIEW_ENABLED. * Phase 2H (Apps Hardened GA, +2-3w later release): layer the v8 security delta - dedicated MCP_SANDBOX_ORIGIN, explicit-target-origin transport, hop-specific relay + proxy-stamped nonce, folded-in ui/initialize probe, connect-src 'none', self-contained HTML, MCPAppLaunchManifest review, MCPAppInstance write path, full UIResourceRenderer retirement across all surfaces - behind MCP_APPS_HARDENED_ENABLED. - Narrow first Apps release to live chat only. Share, search, and plugin-rendered surfaces fall back to text in both Preview and Hardened GA. Cross-surface support is post-Hardened-GA. - Defer MCPAppInstance full-conversation remount to Hardened GA. Preview relies on upstream stableMCPAppRef for parent- re-render survival. - Accept upstream fullscreen support behind appSettings.allowFullscreen (default OFF per server) instead of stripping in delta. - Trim Tasks v1 to status-first jobs panel. Progress bars deferred until upstream PR LibreChat-AI#12535 lands; reuse rather than rebuild. Tasks v1 is independent of Apps tracks. - Phase 0 starts from dev (inheriting PRs LibreChat-AI#12850, LibreChat-AI#12853, LibreChat-AI#12910) rather than older main HEAD; remaining work is session reuse, header consistency, basic 404 -> re-init, per-user token scoping, authContextHash. - Three independent feature flags: MCP_APPS_PREVIEW_ENABLED, MCP_APPS_HARDENED_ENABLED, MCP_TASKS_ENABLED. Old MCP_APPS_ENABLED becomes a deprecated alias for Preview with a startup warning. - Default-deny sandboxPermissions on the legacy renderer applies even when both Apps flags are off. Updated changelog header, Closed go/no-go decisions (added 19/20/21), Carried-forward list with track tags, current-state table with track markers, phases (Phase 0/1/2P/2H/4), test matrix (P/H/A tags), risks, and effort summary (~3-4w for Preview+Tasks; full v1 ~5-7w). References section unchanged. https://claude.ai/code/session_011NZqb4xN9QcXpdY2LCtnuH
…ntextHash, lazy remount) Closes the residual ambiguity in v10 and treats reality where the v9/v10 "clean inheritance from LibreChat-AI#11799" framing was too optimistic. Decisions changed: - Outer-sandbox topology is unified across modes. Both Preview and Hardened serve the outer proxy from MCP_SANDBOX_ORIGIN; v10's Preview-on-data-URL / Hardened-on-MCP_SANDBOX_ORIGIN split is gone. The split between modes is policy only. - PR LibreChat-AI#11799 is a local patchset, not a clean inheritance. End-to-end testing on LibreChat-AI#11799 (May 2 2026) flagged a 401 authenticated-bootstrap bug; Phase 1 must fix it locally and pass an authenticated end-to-end Playwright test before Preview can ship. - authContextHash binds to stable identity only. v10's headerAuthFingerprint over the live bearer-token bytes is removed; OAuth refresh would have orphaned in-flight tasks. New shape: { userId, mcpServerId, credentialSource, oauthSubject, apiKeyFingerprint, configRevision }. - Remount is lazy by visibility, not eager on conversation open. IntersectionObserver hydrates app placeholders only when they enter the viewport (default 200 px margin). Long threads no longer pay N iframes / N resources/read / N initialize probes up front. - resources/read accepts contents[] arrays. The host picks the matching (uri, mimeType) entry, logs extras, ignores them. v10's blanket multi-content rejection is gone. - Generated Apps schemas are vendored, not just package- pinned. Vendored TypeScript types + JSON Schemas live in packages/data-provider/src/mcp-apps/schemas/ for the stable 2026-01-26 spec; package upgrades and schema upgrades are independent. - Bundle size: cite ext-apps@1.7.1 ~377 KB minified explicitly; CI-enforced 8 KB compressed budget on default chat entry when no app is mounted. - Shared in-process Tasks poller dedup is documented as node-local. Operators in horizontally scaled deployments use sticky sessions for globally-once polling, or wait for a post-v1 distributed coordination layer. - input_required UI copy is a named "Unsupported lifecycle" state, not "failed" / "error". Operator docs ship a named section for it. - v6-v9 changelog narratives collapsed to a brief revision log table. v10 and v11 changelogs remain inline. Updates also flow through current-state table, decisions list (decision 4 + 18 reworked), Phase 1 (now ~1.5-2 weeks, inherits LibreChat-AI#11799 plus required local patches), Phase 2P (Preview release on the unified topology), Phase 2H (policy-only delta now ~2-2.5 weeks since topology landed in Phase 1), test matrix (OAuth-refresh invariance test, contents[] selection test, node-local dedup test), risks (inheritance fragility, vendor schema drift, lazy remount hides errors at scroll), and effort summary. R1 (Apps Preview) is now ~3-4 weeks. Full v1 (R1 + R2 + R3) is ~7-8.5 weeks, slightly longer than v10's estimate because the Phase 1 expansion accounts for the LibreChat-AI#11799 local patchset work that v10 underestimated. https://claude.ai/code/session_011NZqb4xN9QcXpdY2LCtnuH
|
Hey folks! How is it going? This is something we require in our organisation, and I would love to take this forward or push any changes that might help fast-track this feature. Thanks a lot for working on this, @KyleKincer |
|
Great work! Would love this to land, we have a lot of use cases already build in our MCP servers for this. |
Integrate ~398 upstream commits onto KyleKincer's MCP Apps branch (originally PR LibreChat-AI#11799). The bulk of the conflict resolution is in the MCP connection and registry layer, where main introduced `allowedAddresses` on the same call sites Kyle's feature threads `enableApps` through — both are now carried together (createMCPServersRegistry, MCPServerInspector, the connection factory, etc.). Affected test signatures are updated to match. Kyle's commits are preserved as the first parent for attribution.
|
Thanks for this work, @KyleKincer — MCP Apps support is a great addition, and the bridge/sandbox design here is solid. Since the PR has been quiet for a while and Branch: main...karamqubsi:feat/mcp-apps — your original commits are preserved as the base (first parent) for attribution; my changes sit on top as focused commits. What's in it, on top of your work:
Happy to open this as a new PR that continues your work, or to rebase onto your branch if you'd prefer to keep it under #11799 — whatever you and @danny-avila prefer. Let me know how you'd like to proceed. |
|
Thanks @KyleKincer / @karamqubsi !! @danny-avila any chance of getting / finishing the PR on this soon. We really could use this feature our MCP server uses this extension (MCP Apps) a lot. 🙏 |
|
I shall be stalking this feature |
|
FYI #13831 |
MCP Apps break when LibreChat is served under a base path (+ sandbox fetch isn't authenticated)While testing this branch behind a reverse proxy that serves LibreChat under a sub-path (code-server's 1. Client calls hardcode root-absolute
|
Summary
Closes #10641
How it works
When an MCP server tool declares
_meta.ui.resourceUri, LibreChat:mcp_appartifact attachment after the tool call completesThe existing MCP tool path (tools without
_meta.ui) is unchanged.Changes
Backend (
api/)mcpApps.js):readMCPResource,appToolCall,serveMCPSandboxwith role-based config gating, domain filtering, and tool visibility enforcementmcp.js):POST /resources/readandPOST /app-tool-callbehind JWT + MCP permission middleware;GET /sandboxis intentionally public (config-gated in controller) so the iframe can loadmcp_appartifact processing onmcpSettings.appsconfig; supports both streaming and non-streaming response modesMCP package (
packages/api/src/mcp/)parsers.ts): Createsmcp_appartifacts whentoolUiMeta.resourceUriis present on a toolMCPServerInspector.ts):getAllToolFunctions()now returns separatemodelToolsandallToolssets, respecting_meta.ui.visibility('model'|'app')MCPManager.ts): AddsreadResource(),appToolCall(),getAllToolsForServer(); reduces per-call metadata overhead with three-tier fallback (call result > cached config > live tools/list)connection.ts): Advertisesio.modelcontextprotocol/uicapability during MCP handshake when apps are enabledtypes/index.ts,schemas.ts):MCPAppArtifact,Tools.mcp_appenum,LCFunctionTool._metawith visibilityFrontend (
client/)MCPApp/):MCPAppInlinefetches HTML and delegates toMCPAppContainer, which manages the iframe lifecycle, bridge, portal rendering, geometry tracking, and display mode (inline/fullscreen)@modelcontextprotocol/ext-apps(toggle with?mcpBridge=sdkor localStorage)mcp-sandbox.html): Two-layer iframe architecture — outer frame validates/forwards postMessage traffic, inner frame runs app code in a restricted sandbox with injected CSPmcp_appattachment rendering viastableMCPAppRef, preserving app state across parent re-rendersChatView.tsx,Header.tsx): Uniquedata-chat-view-rootattribute for portal targetingConfiguration (
librechat.yaml)Security
sandbox-*methods; inner iframe created from blob URL withsandbox="allow-scripts"onlyallowedConnectDomains, filtered againstblockedDomains(wildcard support)visibilityincluding'app'; model-only tools are rejected with 403Test plan
mcpApps.test.js): domain filtering, visibility enforcement, sandbox serving, config gating