diff --git a/api/server/controllers/mcpApps.js b/api/server/controllers/mcpApps.js index ca9738fe78..7568a1b6da 100644 --- a/api/server/controllers/mcpApps.js +++ b/api/server/controllers/mcpApps.js @@ -17,8 +17,11 @@ const readMCPResource = async (req, res) => { if (!serverName || !uri) { return res.status(400).json({ error: 'serverName and uri are required' }); } - if (typeof uri !== 'string' || !uri.startsWith('ui://')) { - return res.status(400).json({ error: 'uri must use the ui:// scheme' }); + // The serverResources capability lets an app read any resource the connected MCP server + // exposes (ui:// templates plus supporting data such as file:// or custom schemes), so the + // proxy only requires a non-empty string and leaves resource authorization to the server. + if (typeof uri !== 'string' || uri.length === 0) { + return res.status(400).json({ error: 'uri must be a non-empty string' }); } const mcpManager = getMCPManager(); diff --git a/client/public/mcp-sandbox.html b/client/public/mcp-sandbox.html index a6e87b0122..f2e5483dab 100644 --- a/client/public/mcp-sandbox.html +++ b/client/public/mcp-sandbox.html @@ -167,9 +167,9 @@ ].join('; '); } - // Only permit host patterns: optional scheme, optional wildcard subdomain prefix, - // hostname characters, optional port. Rejects CSP keywords and injection attempts. - const SAFE_HOST_RE = /^(?:https?:\/\/)?(?:\*\.)?[a-zA-Z0-9][a-zA-Z0-9\-.]*(?::\d{1,5})?$/; + // Only permit host patterns: optional http(s)/ws(s) scheme, optional wildcard subdomain + // prefix, hostname characters, optional port. Rejects CSP keywords and injection attempts. + const SAFE_HOST_RE = /^(?:(?:https?|wss?):\/\/)?(?:\*\.)?[a-zA-Z0-9][a-zA-Z0-9\-.]*(?::\d{1,5})?$/; function toDomainList(value) { if (!Array.isArray(value)) return ''; diff --git a/client/src/components/Chat/Messages/Content/ToolCall.tsx b/client/src/components/Chat/Messages/Content/ToolCall.tsx index ba134f96c7..a5c6b96e6e 100644 --- a/client/src/components/Chat/Messages/Content/ToolCall.tsx +++ b/client/src/components/Chat/Messages/Content/ToolCall.tsx @@ -10,10 +10,10 @@ import { actionDomainSeparator, } from 'librechat-data-provider'; import type { TAttachment, UIResource } from 'librechat-data-provider'; +import { getMCPSandboxUrl, buildAppToolResult } from '~/utils/mcpApps'; import { useLocalize, useProgress, useExpandCollapse } from '~/hooks'; import { ToolIcon, getToolIconType, isError } from './ToolOutput'; import { useMCPIconMap, useAppBridge } from '~/hooks/MCP'; -import { getMCPSandboxUrl } from '~/utils/mcpApps'; import { AttachmentGroup } from './Parts'; import ToolCallInfo from './ToolCallInfo'; import ProgressText from './ProgressText'; @@ -50,17 +50,7 @@ const MCPAppView = React.memo(function MCPAppView({ } }, [args]); - const toolResult = useMemo(() => { - const sc = app.structuredContent as Record | undefined | null; - const content = (app.content as [] | undefined) ?? []; - if ((!sc || typeof sc !== 'object' || Array.isArray(sc)) && content.length === 0) - return undefined; - return { - content, - ...(sc && typeof sc === 'object' && !Array.isArray(sc) ? { structuredContent: sc } : {}), - ...(app.isError === true ? { isError: true } : {}), - }; - }, [app.structuredContent, app.content, app.isError]); + const toolResult = useMemo(() => buildAppToolResult(app), [app]); const handleSizeChanged = useCallback((params: { height?: number; width?: number }) => { if (params.height && params.height > 0) { @@ -71,7 +61,8 @@ const MCPAppView = React.memo(function MCPAppView({ useAppBridge(iframeRef, app, toolArgs, toolResult, handleSizeChanged); - if (app.text && (app.mimeType ?? 'text/html').includes('html')) { + const isAppBacked = !!(app.toolName && app.serverName); + if (!isAppBacked && app.text && (app.mimeType ?? 'text/html').includes('html')) { return (