Skip to content

✨ feat: MCP Apps Extension Support - #11799

Open
KyleKincer wants to merge 8 commits into
LibreChat-AI:mainfrom
KyleKincer:feat/mcp-apps
Open

KyleKincer wants to merge 8 commits into
LibreChat-AI:mainfrom
KyleKincer:feat/mcp-apps

Conversation

@KyleKincer

Copy link
Copy Markdown

Summary

  • Implements MCP Apps extension support, enabling MCP servers to render interactive HTML interfaces (forms, dashboards, visualizations) inline in the chat
  • Adds backend resource/tool-call proxy endpoints, metadata pipeline through the MCP package, sandboxed iframe rendering on the client, and config-driven security controls
  • Delivers the approach from #11581

Closes #10641

How it works

When an MCP server tool declares _meta.ui.resourceUri, LibreChat:

  1. Preserves that metadata through the tool discovery and call pipeline
  2. Creates an mcp_app artifact attachment after the tool call completes
  3. Fetches the declared HTML resource via a host-side proxy endpoint
  4. Renders it in a two-layer sandboxed iframe with dynamic CSP
  5. Provides a JSON-RPC bridge so the iframe can call tools and receive results

The existing MCP tool path (tools without _meta.ui) is unchanged.

Changes

Backend (api/)

  • New controller (mcpApps.js): readMCPResource, appToolCall, serveMCPSandbox with role-based config gating, domain filtering, and tool visibility enforcement
  • Route wiring (mcp.js): POST /resources/read and POST /app-tool-call behind JWT + MCP permission middleware; GET /sandbox is intentionally public (config-gated in controller) so the iframe can load
  • Agent callbacks: Gates mcp_app artifact processing on mcpSettings.apps config; supports both streaming and non-streaming response modes

MCP package (packages/api/src/mcp/)

  • Metadata pipeline (parsers.ts): Creates mcp_app artifacts when toolUiMeta.resourceUri is present on a tool
  • Tool visibility (MCPServerInspector.ts): getAllToolFunctions() now returns separate modelTools and allTools sets, respecting _meta.ui.visibility ('model' | 'app')
  • Manager (MCPManager.ts): Adds readResource(), appToolCall(), getAllToolsForServer(); reduces per-call metadata overhead with three-tier fallback (call result > cached config > live tools/list)
  • Connection (connection.ts): Advertises io.modelcontextprotocol/ui capability during MCP handshake when apps are enabled
  • Types (types/index.ts, schemas.ts): MCPAppArtifact, Tools.mcp_app enum, LCFunctionTool._meta with visibility

Frontend (client/)

  • New module (MCPApp/): MCPAppInline fetches HTML and delegates to MCPAppContainer, which manages the iframe lifecycle, bridge, portal rendering, geometry tracking, and display mode (inline/fullscreen)
  • Dual bridge: Custom JSON-RPC bridge (default) + optional SDK adapter via @modelcontextprotocol/ext-apps (toggle with ?mcpBridge=sdk or localStorage)
  • Sandbox (mcp-sandbox.html): Two-layer iframe architecture — outer frame validates/forwards postMessage traffic, inner frame runs app code in a restricted sandbox with injected CSP
  • ToolCall integration: Stable mcp_app attachment rendering via stableMCPAppRef, preserving app state across parent re-renders
  • Portal cleanup (ChatView.tsx, Header.tsx): Unique data-chat-view-root attribute for portal targeting

Configuration (librechat.yaml)

mcpSettings:
  apps: true                        # enable/disable MCP Apps
  appSettings:
    allowedConnectDomains: []       # additional CSP connect-src domains
    blockedDomains: []              # domain blocklist (supports wildcards)
    maxHeight: 800                  # iframe height cap (100-2000px)
    allowFullscreen: true           # allow fullscreen display mode

Security

  • Two-layer sandbox: Outer iframe validates messages and blocks sandbox-* methods; inner iframe created from blob URL with sandbox="allow-scripts" only
  • Dynamic CSP: Built from server-declared domains, merged with admin allowedConnectDomains, filtered against blockedDomains (wildcard support)
  • Tool visibility enforcement: App iframe can only call tools with visibility including 'app'; model-only tools are rejected with 403
  • Auth boundary: Resource read and tool call endpoints require JWT + MCP permission; sandbox HTML endpoint is public but config-gated

Test plan

  • Backend controller tests (mcpApps.test.js): domain filtering, visibility enforcement, sandbox serving, config gating
  • MCP parsers tests: app artifact creation and metadata flow
  • MCPManager tests: metadata fallback tiers, tool discovery
  • MCPServerInspector tests: visibility filtering, model vs app tool separation
  • MCPAppBridge tests: protocol lifecycle, display modes, context updates
  • MCPAppContainer tests: bridge persistence, state preservation, geometry, fullscreen
  • ToolCall tests: attachment stability, mount/unmount behavior
  • Full CI suite
  • Manual E2E with MCP App server (e.g., test_mcp_servers)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

KyleKincer and others added 2 commits February 14, 2026 18:01
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>
@danny-avila

Copy link
Copy Markdown
Collaborator

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,

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copilot AI Feb 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
setSandboxSrc(SANDBOX_ENDPOINT);
// Fail closed: do not fall back to direct SANDBOX_ENDPOINT iframe src.
setSandboxSrc('about:blank');

Copilot uses AI. Check for mistakes.
Comment thread client/public/mcp-sandbox.html Outdated
Comment on lines +20 to +27
try {
if (document.referrer) {
return new URL(document.referrer).origin;
}
} catch {
// Fall back to wildcard when referrer is unavailable.
}
return '*';

Copilot AI Feb 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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;

Copilot uses AI. Check for mistakes.
Comment on lines +66 to +71
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(',')}`;

Copilot AI Feb 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}

Suggested change
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}`;

Copilot uses AI. Check for mistakes.
Comment thread client/public/mcp-sandbox.html Outdated
const fullHtml = injectIntoHead(html, cspMeta + SANDBOX_BASE_STYLE);

const blob = new Blob([fullHtml], { type: 'text/html' });
innerFrame.src = URL.createObjectURL(blob);

Copilot AI Feb 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
* @param {object} res - Express response
*/
const serveMCPSandbox = async (req, res) => {
try {

Copilot AI Feb 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
try {
try {
// Require authentication to avoid relying on a default role for anonymous users
if (!req.user) {
return res.status(401).json({ error: 'Unauthorized' });
}

Copilot uses AI. Check for mistakes.
if (params.oauthTokens) {
this.oauthTokens = params.oauthTokens;
}
const enableApps = params.enableApps !== false; // default true

Copilot AI Feb 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +260 to +267
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' });
}
}
});

Copilot AI Feb 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
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',

Copilot AI Feb 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
KyleKincer added a commit to KyleKincer/librechat-mcp-apps-test-server that referenced this pull request Feb 18, 2026
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>
@KyleKincer

Copy link
Copy Markdown
Author

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 launch_apps_testbed, the app renders with buttons for every host bridge method (tools/call, resources/read, ui/message, display modes, model context, etc.) and logs all the events so you can see the full round-trip.

To run it:

git clone https://github.com/KyleKincer/librechat-mcp-apps-test-server.git
cd librechat-mcp-apps-test-server
npm install
npm start

Then add this to librechat.yaml:

mcpSettings:
  apps: true

mcpServers:
  mcp_apps_integration:
    type: streamable-http
    url: http://localhost:3099/mcp

Restart the backend, and in chat ask the model to call launch_apps_testbed. The README has a full checklist of what each button tests.

@danny-avila

Copy link
Copy Markdown
Collaborator

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 MCPAppContainer.tsx. When the fetch to /api/mcp/sandbox fails, the code falls back to setting sandboxSrc = SANDBOX_ENDPOINT (a direct HTTPS URL). The outer iframe has sandbox="allow-scripts allow-same-origin", the entire security model depends on that src being an opaque data: URL so the origin is null. The moment it loads from /api/mcp/sandbox instead, it runs in LibreChat's own origin with allow-same-origin, which means full access to cookies, localStorage, and the DOM. Network errors are not exotic edge cases. This needs to fail closed: set sandboxSrc to 'about:blank' and show an error UI instead.

// Don't do this:
setSandboxSrc(SANDBOX_ENDPOINT);

// Do this:
setSandboxSrc('about:blank');
// + render an error state

Second: enableApps in MCPConnection defaults to true and nothing ever passes false. When an admin sets mcpSettings.apps: false, the endpoint guards in mcpApps.js correctly block requests, but the io.modelcontextprotocol/ui capability is still advertised to every server during the handshake. The config flag needs to be threaded through MCPConnectionFactory and MCPServersInitializer when creating connections.

Third: readMCPResource in mcpApps.js accepts any URI, there's no check that it starts with ui://. This endpoint was designed as a UI-resource proxy, but as written it proxies any resource on any MCP server the user has access to. If a server exposes config:// or secret:// resources, they're fully readable through this endpoint. Add a scheme check:

if (!uri.startsWith('ui://')) {
  return res.status(400).json({ error: 'uri must use the ui:// scheme' });
}

MAJORs

Dead ternary in MCPAppBridge.ts in the ui/message handler:

const role = message.role === 'user' ? 'user' : 'user';

Both branches are identical. This needs to actually validate the role.

getAllToolsForServer calls listTools() on the live MCP connection on every single bridge tool call to validate visibility. That doubles the round-trip latency of every interactive app action. Tool visibility is static between reconnects, it needs to be cached.

Blob URL leak in mcp-sandbox.html: URL.createObjectURL is called in createInnerFrame each time but revokeObjectURL is never called on the old URL. Revoke it before replacing the frame.

The tools/call cancellation path in MCPAppBridge.ts sends a ui/notifications/tool-cancelled notification AND then re-throws, causing the outer catch to also send a -32603 error response for the same request ID. JSON-RPC 2.0 requires exactly one response per request. The inner catch should break instead of re-throwing on cancellation.

/api/mcp/sandbox has no auth middleware. getMCPAppsConfig(req.user?.role) with req.user = undefined resolves the default config and serves the file to anyone. Add requireJwtAuth to that route.

MCPAppFullscreen.tsx is exported but never used outside the module. The fullscreen logic is fully implemented inside MCPAppContainer.tsx. Delete it.

Minors

  • nextRpcId in mcpAppUtils.ts is exported but never imported anywhere. Dead code.
  • inlineRect state tracks top and left but only width is ever consumed. The two unused fields cause state updates on every scroll/resize event.
  • Version strings '1.0.0' / '1.2.3' are hardcoded in MCPAppBridge.ts and connection.ts. They should read from a shared constant.
  • resourceMeta is in the dependency array of the sendContextUpdate effect and compared by reference. During streaming, if a parent creates a new object with the same shape, this fires a redundant postMessage to the iframe on every token.
  • Missing newline at end of MCPApp/index.ts.

@danny-avila

Copy link
Copy Markdown
Collaborator

On top of this, there is a merge conflict and ESLint issue, please address them as well.

@KyleKincer

Copy link
Copy Markdown
Author

Thanks for the detailed pass @danny-avila — I addressed all items in your review and pushed the updates.

Highlights:

  • Fixed the sandbox fail-open path (now fails closed with an error UI) and added auth protection for /api/mcp/sandbox.
  • Enforced ui:// scheme validation in resources/read.
  • Threaded mcpSettings.apps through MCP connection creation so UI capability advertising is disabled when apps are off.
  • Added caching for getAllToolsForServer to avoid repeated listTools() calls.
  • Fixed tools/call cancellation to avoid double JSON-RPC responses, validated ui/message role properly, and fixed sandbox blob URL cleanup.
  • Removed dead/unused MCP app code (MCPAppFullscreen, nextRpcId, unused inline rect fields), moved protocol/version strings to shared constants, and fixed the merge conflict + ESLint issues.

Validation run:

  • Targeted lint on changed files: clean (no errors).
  • API tests: mcpApps.test.js, initializeMCPs.spec.js passed.
  • Client MCP bridge tests passed.
  • Packages API MCP tests (MCPConnectionFactory, connection.capabilities, MCPManager) passed.

@KyleKincer

Copy link
Copy Markdown
Author

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
@sinjun-jeff

Copy link
Copy Markdown

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:

[MCPAppContainer] Failed to bootstrap opaque sandbox source: Error: Failed to load MCP sandbox template (401)

Server log confirms GET /api/mcp/sandbox returning 401 to a logged-in user, even though that same session can successfully list tools, call tools, etc.

Root cause: the client fetches the sandbox template with raw fetch():

fetch("/api/mcp/sandbox", { method: "GET", credentials: "same-origin", cache: "no-store" })

credentials: "same-origin" sends cookies — but auth is JWT in the Authorization header, attached by an axios request interceptor that fetch() bypasses. So the request hits the route without an Authorization header, requireJwtAuth returns 401, sandbox never loads.

Repro:

  1. Build this PR
  2. Register a new local user
  3. Configure any streamable-http MCP server with an MCP App resource
  4. Ask the model to call the launching tool
  5. Iframe area shows the error; network tab shows the 401

Fix options:

  • (a) Route the sandbox fetch through the existing axios client so the JWT interceptor fires
  • (b) Drop requireJwtAuth from the /sandbox route — it serves a static template HTML with no user data; the actual app HTML comes from the MCP server, not this endpoint

I went with (b) locally to keep testing and the rest of the surface (bridge, ui/initialize, ui/notifications/*, tools/call from the iframe, ui/message, server-side actions triggered by in-app forms) all worked great. Strong work overall — would love to see this land.

mbiskach pushed a commit to mbiskach/LibreChat that referenced this pull request May 3, 2026
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
mbiskach pushed a commit to mbiskach/LibreChat that referenced this pull request May 3, 2026
…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
mbiskach pushed a commit to mbiskach/LibreChat that referenced this pull request May 3, 2026
…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
@thealphadollar

Copy link
Copy Markdown

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

@RemcoE33

Copy link
Copy Markdown

Great work! Would love this to land, we have a lot of use cases already build in our MCP servers for this.

karamqubsi added a commit to karamqubsi/LibreChat that referenced this pull request Jun 2, 2026
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.
@karamqubsi

Copy link
Copy Markdown

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 main has moved on considerably, I took the liberty of bringing it up to date and addressing the open review feedback on a branch, in case it's useful for getting this over the line.

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:

  • Merged current main (~398 commits of drift). Most conflicts were in the MCP connection/registry layer, where main added allowedAddresses to the same call sites this PR threads enableApps through — both are now carried together, with the affected test signatures updated.
  • Fixed the 401 on the sandbox template fetch (the open review comment) — it now loads via the shared request client so the auth token is attached.
  • Reworked the inline iframe to render in normal document flow with a CSS-only fullscreen toggle. The previous fixed-position / scroll-observer approach drifted against newer main's layout (the app rendered above the message text); this also preserves the live bridge/app state across the fullscreen transition.
  • Theme variables now use the spec-compliant MCP-UI style names (the CSS custom properties the host exposes so an app matches the active light/dark theme).
  • SDK-derived adapter types (dropped any), and i18n — routed the remaining hardcoded UI strings through useLocalize() + added the locale keys.
  • Build is green and the backend + client test suites pass. Verified end-to-end against a real MCP App server that renders an interactive app.

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.

@josegomez

josegomez commented Jun 17, 2026 •

Copy link
Copy Markdown

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. 🙏
Happy to support any way I can.

@RedR25

RedR25 commented Jun 22, 2026

Copy link
Copy Markdown

I shall be stalking this feature

@rba100

rba100 commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

FYI #13831
Seems like there's another MCP Apps implementation PR.

@waltmayf

Copy link
Copy Markdown

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 /proxy/<port>/), the MCP App never renders. Two distinct root causes, both at head c510f355:

1. Client calls hardcode root-absolute /api/... paths and drop BASE_URL

Every endpoint in packages/data-provider/src/api-endpoints.ts is built as `${BASE_URL}/api/...`, where BASE_URL is derived from the page's <base href>. The MCP Apps calls skip that prefix:

  • client/src/components/Chat/Messages/Content/MCPApp/mcpAppUtils.ts:121 — request.post('/api/mcp/resources/read', …)
  • client/src/components/Chat/Messages/Content/MCPApp/mcpAppUtils.ts:134 — request.post('/api/mcp/app-tool-call', …)
  • client/src/components/Chat/Messages/Content/MCPApp/MCPAppContainer.tsx:22 — const SANDBOX_ENDPOINT = '/api/mcp/sandbox'

Under a prefix deployment the leading slash resolves against the origin root, so the request bypasses the /proxy/<port> prefix and hits the fronting proxy instead of LibreChat. In my setup the proxy answered Unsupported method POST for resources/read, and the sandbox iframe failed to bootstrap. At a domain-root deployment BASE_URL === '', so this is invisible — which is probably why it slipped through.

Fix: prepend the base URL, e.g. request.post(`${apiBaseUrl()}/api/mcp/resources/read`, …) (apiBaseUrl is already exported from librechat-data-provider), or add builders to the mcp object in api-endpoints.ts.

2. Sandbox template fetch has no Authorization header

MCPAppContainer.tsx:193 loads the sandbox template with a raw fetch(SANDBOX_ENDPOINT, { credentials: 'same-origin' }) and no bearer token. The GET /api/mcp/sandbox route is behind requireJwtAuth, and the JWT strategy only extracts the token from the Authorization: Bearer header (ExtractJwt.fromAuthHeaderAsBearerToken() in api/strategies/jwtStrategy.js) — there is no cookie fallback. So the fetch returns 401 and the container shows "Failed to load the secure MCP app sandbox." This is independent of #1: it fails even at a domain-root deployment once auth is required.

Fix: route it through the shared axios request helper so the bearer token (and 401-refresh) are applied, e.g. const html = await request.get<string>(`${apiBaseUrl()}/api/mcp/sandbox`, { responseType: 'text' }) — which also resolves #1 for the sandbox.

Verification

Reproduced behind a code-server /proxy/<port> reverse proxy (a streamable-http MCP server plus a local stdio ui:// demo). With the three paths corrected to use apiBaseUrl(), the calls target LibreChat instead of the proxy; routing the sandbox fetch through request supplies the bearer token. I also updated the MCPAppContainer sandbox test to assert the base-path-prefixed URL (/proxy/3080/api/mcp/sandbox) and mock the authenticated request.get; the suite passes. Happy to open a PR against the branch if that's useful.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Enhancement]: Support for MCP Apps