Skip to content

✨ feat: MCP Apps Extension Support#11799

Open
KyleKincer wants to merge 5 commits intodanny-avila:mainfrom
KyleKincer:feat/mcp-apps
Open

✨ feat: MCP Apps Extension Support#11799
KyleKincer wants to merge 5 commits intodanny-avila:mainfrom
KyleKincer:feat/mcp-apps

Conversation

@KyleKincer
Copy link

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)

Copy link
Contributor

Copilot AI left a comment

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 danny-avila#10641

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@danny-avila
Copy link
Owner

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,

Copy link
Contributor

Copilot AI left a comment

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

Copilot AI Feb 18, 2026

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 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 '*';
Copy link

Copilot AI Feb 18, 2026

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(',')}`;
Copy link

Copilot AI Feb 18, 2026

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.
const fullHtml = injectIntoHead(html, cspMeta + SANDBOX_BASE_STYLE);

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

Copilot AI Feb 18, 2026

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 {
Copy link

Copilot AI Feb 18, 2026

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
Copy link

Copilot AI Feb 18, 2026

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' });
}
}
});
Copy link

Copilot AI Feb 18, 2026

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',
Copy link

Copilot AI Feb 18, 2026

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 danny-avila/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
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.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Enhancement]: Support for MCP Apps

2 participants

Comments