diff --git a/crates/goose-server/src/auth.rs b/crates/goose-server/src/auth.rs index a18c83972fdc..05e67f2eb276 100644 --- a/crates/goose-server/src/auth.rs +++ b/crates/goose-server/src/auth.rs @@ -10,7 +10,7 @@ pub async fn check_token( request: Request, next: Next, ) -> Result { - if request.uri().path() == "/status" { + if request.uri().path() == "/status" || request.uri().path() == "/mcp-ui-proxy" { return Ok(next.run(request).await); } let secret_key = request diff --git a/crates/goose-server/src/commands/agent.rs b/crates/goose-server/src/commands/agent.rs index 901667855a4c..666534395d20 100644 --- a/crates/goose-server/src/commands/agent.rs +++ b/crates/goose-server/src/commands/agent.rs @@ -49,7 +49,7 @@ pub async fn run() -> Result<()> { .allow_methods(Any) .allow_headers(Any); - let app = crate::routes::configure(app_state) + let app = crate::routes::configure(app_state, secret_key.clone()) .layer(middleware::from_fn_with_state( secret_key.clone(), check_token, diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs index b3f9aedb14f6..a75841f9cffe 100644 --- a/crates/goose-server/src/openapi.rs +++ b/crates/goose-server/src/openapi.rs @@ -326,6 +326,7 @@ derive_utoipa!(Icon as IconSchema); paths( super::routes::status::status, super::routes::status::diagnostics, + super::routes::mcp_ui_proxy::mcp_ui_proxy, super::routes::config_management::backup_config, super::routes::config_management::recover_config, super::routes::config_management::validate_config, diff --git a/crates/goose-server/src/routes/mcp_ui_proxy.rs b/crates/goose-server/src/routes/mcp_ui_proxy.rs new file mode 100644 index 000000000000..2489c8d98f16 --- /dev/null +++ b/crates/goose-server/src/routes/mcp_ui_proxy.rs @@ -0,0 +1,53 @@ +use axum::{ + extract::Query, + http::{header, StatusCode}, + response::{Html, IntoResponse, Response}, + routing::get, + Router, +}; +use serde::Deserialize; + +#[derive(Deserialize)] +struct ProxyQuery { + secret: String, +} + +const MCP_UI_PROXY_HTML: &str = include_str!("templates/mcp_ui_proxy.html"); + +#[utoipa::path( + get, + path = "/mcp-ui-proxy", + params( + ("secret" = String, Query, description = "Secret key for authentication") + ), + responses( + (status = 200, description = "MCP UI proxy HTML page", content_type = "text/html"), + (status = 401, description = "Unauthorized - invalid or missing secret"), + ) +)] +async fn mcp_ui_proxy( + axum::extract::State(secret_key): axum::extract::State, + Query(params): Query, +) -> Response { + if params.secret != secret_key { + return (StatusCode::UNAUTHORIZED, "Unauthorized").into_response(); + } + + ( + [ + (header::CONTENT_TYPE, "text/html; charset=utf-8"), + ( + header::HeaderName::from_static("referrer-policy"), + "no-referrer", + ), + ], + Html(MCP_UI_PROXY_HTML), + ) + .into_response() +} + +pub fn routes(secret_key: String) -> Router { + Router::new() + .route("/mcp-ui-proxy", get(mcp_ui_proxy)) + .with_state(secret_key) +} diff --git a/crates/goose-server/src/routes/mod.rs b/crates/goose-server/src/routes/mod.rs index 9ca3799e5d89..db2071451d8b 100644 --- a/crates/goose-server/src/routes/mod.rs +++ b/crates/goose-server/src/routes/mod.rs @@ -2,6 +2,7 @@ pub mod agent; pub mod audio; pub mod config_management; pub mod errors; +pub mod mcp_ui_proxy; pub mod recipe; pub mod recipe_utils; pub mod reply; @@ -16,7 +17,7 @@ use std::sync::Arc; use axum::Router; // Function to configure all routes -pub fn configure(state: Arc) -> Router { +pub fn configure(state: Arc, secret_key: String) -> Router { Router::new() .merge(status::routes()) .merge(reply::routes(state.clone())) @@ -27,4 +28,5 @@ pub fn configure(state: Arc) -> Router { .merge(session::routes(state.clone())) .merge(schedule::routes(state.clone())) .merge(setup::routes(state.clone())) + .merge(mcp_ui_proxy::routes(secret_key)) } diff --git a/crates/goose-server/src/routes/templates/mcp_ui_proxy.html b/crates/goose-server/src/routes/templates/mcp_ui_proxy.html new file mode 100644 index 000000000000..0cac94a889da --- /dev/null +++ b/crates/goose-server/src/routes/templates/mcp_ui_proxy.html @@ -0,0 +1,142 @@ + + + + + + + + MCP-UI Proxy + + + + + + diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index 8b87f06badb4..2f3a64a577d6 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -1042,6 +1042,33 @@ } } }, + "/mcp-ui-proxy": { + "get": { + "tags": [ + "super::routes::mcp_ui_proxy" + ], + "operationId": "mcp_ui_proxy", + "parameters": [ + { + "name": "secret", + "in": "query", + "description": "Secret key for authentication", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "MCP UI proxy HTML page" + }, + "401": { + "description": "Unauthorized - invalid or missing secret" + } + } + } + }, "/recipes/create": { "post": { "tags": [ diff --git a/ui/desktop/package-lock.json b/ui/desktop/package-lock.json index 431e285a04c9..334fa2dcd084 100644 --- a/ui/desktop/package-lock.json +++ b/ui/desktop/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@ai-sdk/openai": "^2.0.52", "@ai-sdk/ui-utils": "^1.2.11", - "@mcp-ui/client": "^5.13.0", + "@mcp-ui/client": "^5.14.1", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-dialog": "^1.1.15", @@ -3018,9 +3018,9 @@ } }, "node_modules/@mcp-ui/client": { - "version": "5.13.1", - "resolved": "https://registry.npmjs.org/@mcp-ui/client/-/client-5.13.1.tgz", - "integrity": "sha512-U0+kgdgmBRfqVM1MbqDZnaxBWboyDWQNmYrS72loL+XL9ZMtej5B7EThrPsE3wtwJftLURVagYqgDAnfSgKhXw==", + "version": "5.14.1", + "resolved": "https://registry.npmjs.org/@mcp-ui/client/-/client-5.14.1.tgz", + "integrity": "sha512-DHJ4H01L2oIiMdDzUrBErxYoli9Q3cQq5sXk3hhBQNqASbc55PtEhz6k0pOp7ykkj63MfxDKDmYXLw5jseY7/g==", "license": "Apache-2.0", "dependencies": { "@modelcontextprotocol/sdk": "*", diff --git a/ui/desktop/package.json b/ui/desktop/package.json index b7a799228767..db01a97d97dc 100644 --- a/ui/desktop/package.json +++ b/ui/desktop/package.json @@ -41,7 +41,7 @@ "dependencies": { "@ai-sdk/openai": "^2.0.52", "@ai-sdk/ui-utils": "^1.2.11", - "@mcp-ui/client": "^5.13.0", + "@mcp-ui/client": "^5.14.1", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-dialog": "^1.1.15", diff --git a/ui/desktop/src/api/sdk.gen.ts b/ui/desktop/src/api/sdk.gen.ts index 0634e71abc5b..f263e5b5a1c6 100644 --- a/ui/desktop/src/api/sdk.gen.ts +++ b/ui/desktop/src/api/sdk.gen.ts @@ -2,7 +2,7 @@ import type { Client, Options as Options2, TDataShape } from './client'; import { client } from './client.gen'; -import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, CheckProviderData, ConfirmPermissionData, ConfirmPermissionErrors, ConfirmPermissionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetToolsData, GetToolsErrors, GetToolsResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StatusData, StatusResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateRouterToolSelectorData, UpdateRouterToolSelectorErrors, UpdateRouterToolSelectorResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen'; +import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, CheckProviderData, ConfirmPermissionData, ConfirmPermissionErrors, ConfirmPermissionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetToolsData, GetToolsErrors, GetToolsResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StatusData, StatusResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateRouterToolSelectorData, UpdateRouterToolSelectorErrors, UpdateRouterToolSelectorResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen'; export type Options = Options2 & { /** @@ -310,6 +310,13 @@ export const startTetrateSetup = (options? }); }; +export const mcpUiProxy = (options: Options) => { + return (options.client ?? client).get({ + url: '/mcp-ui-proxy', + ...options + }); +}; + export const createRecipe = (options: Options) => { return (options.client ?? client).post({ url: '/recipes/create', diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index c1e9b62014ac..644dcdd5bb37 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -1725,6 +1725,32 @@ export type StartTetrateSetupResponses = { export type StartTetrateSetupResponse = StartTetrateSetupResponses[keyof StartTetrateSetupResponses]; +export type McpUiProxyData = { + body?: never; + path?: never; + query: { + /** + * Secret key for authentication + */ + secret: string; + }; + url: '/mcp-ui-proxy'; +}; + +export type McpUiProxyErrors = { + /** + * Unauthorized - invalid or missing secret + */ + 401: unknown; +}; + +export type McpUiProxyResponses = { + /** + * MCP UI proxy HTML page + */ + 200: unknown; +}; + export type CreateRecipeData = { body: CreateRecipeRequest; path?: never; diff --git a/ui/desktop/src/components/MCPUIResourceRenderer.tsx b/ui/desktop/src/components/MCPUIResourceRenderer.tsx index c73aab418b2c..b977c17701da 100644 --- a/ui/desktop/src/components/MCPUIResourceRenderer.tsx +++ b/ui/desktop/src/components/MCPUIResourceRenderer.tsx @@ -5,6 +5,7 @@ import { UIActionResultNotification, UIActionResultPrompt, UIActionResultToolCall, + UIActionResult, } from '@mcp-ui/client'; import { useState, useEffect } from 'react'; import { toast } from 'react-toastify'; @@ -15,41 +16,6 @@ interface MCPUIResourceRendererProps { appendPromptToChat?: (value: string) => void; } -type UISizeChange = { - type: 'ui-size-change'; - payload: { - height: number; - width: number; - }; -}; - -// Reserved message types from iframe to host -type UILifecycleIframeReady = { - type: 'ui-lifecycle-iframe-ready'; - payload?: Record; -}; - -type UIRequestData = { - type: 'ui-request-data'; - messageId: string; - payload: { - requestType: string; - params: Record; - }; -}; - -// We are creating a new type to support all reserved message types that may come from the iframe -// Not all reserved message types are currently exported by @mcp-ui/client -type ActionEventsFromIframe = - | UIActionResultIntent - | UIActionResultLink - | UIActionResultNotification - | UIActionResultPrompt - | UIActionResultToolCall - | UISizeChange - | UILifecycleIframeReady - | UIRequestData; - // More specific result types using discriminated unions type UIActionHandlerSuccess = { status: 'success'; @@ -126,20 +92,34 @@ export default function MCPUIResourceRenderer({ appendPromptToChat, }: MCPUIResourceRendererProps) { const [currentThemeValue, setCurrentThemeValue] = useState('light'); + const [proxyUrl, setProxyUrl] = useState(undefined); useEffect(() => { const theme = localStorage.getItem('theme') || 'light'; setCurrentThemeValue(theme); - console.log('[MCP-UI] Current theme value:', theme); + + const fetchProxyUrl = async () => { + try { + const baseUrl = await window.electron.getGoosedHostPort(); + const secretKey = await window.electron.getSecretKey(); + if (baseUrl && secretKey) { + setProxyUrl(`${baseUrl}/mcp-ui-proxy?secret=${encodeURIComponent(secretKey)}`); + } else { + console.error('Failed to get goosed host/port or secret key'); + } + } catch (error) { + console.error('Error fetching MCP-UI Proxy URL:', error); + } + }; + + fetchProxyUrl().catch(console.error); }, []); - const handleUIAction = async ( - actionEvent: ActionEventsFromIframe - ): Promise => { + const handleUIAction = async (actionEvent: UIActionResult): Promise => { // result to pass back to the MCP-UI let result: UIActionHandlerResult; - const handleToolCase = async ( + const handleToolAction = async ( actionEvent: UIActionResultToolCall ): Promise => { const { toolName, params } = actionEvent.payload; @@ -156,7 +136,7 @@ export default function MCPUIResourceRenderer({ }; }; - const handlePromptCase = async ( + const handlePromptAction = async ( actionEvent: UIActionResultPrompt ): Promise => { const { prompt } = actionEvent.payload; @@ -191,7 +171,9 @@ export default function MCPUIResourceRenderer({ }; }; - const handleLinkCase = async (actionEvent: UIActionResultLink) => { + const handleLinkAction = async ( + actionEvent: UIActionResultLink + ): Promise => { const { url } = actionEvent.payload; try { @@ -244,7 +226,7 @@ export default function MCPUIResourceRenderer({ } }; - const handleNotifyCase = async ( + const handleNotifyAction = async ( actionEvent: UIActionResultNotification ): Promise => { const { message } = actionEvent.payload; @@ -262,7 +244,7 @@ export default function MCPUIResourceRenderer({ }; }; - const handleIntentCase = async ( + const handleIntentAction = async ( actionEvent: UIActionResultIntent ): Promise => { toast.info( @@ -285,82 +267,31 @@ export default function MCPUIResourceRenderer({ }; }; - const handleSizeChangeCase = async ( - actionEvent: UISizeChange - ): Promise => { - return { - status: 'success' as const, - message: 'Size change handled', - data: actionEvent.payload, - }; - }; - - const handleIframeReadyCase = async ( - actionEvent: UILifecycleIframeReady - ): Promise => { - console.log('[MCP-UI] Iframe ready to receive messages'); - return { - status: 'success' as const, - message: 'Iframe is ready to receive messages', - data: actionEvent.payload, - }; - }; - - const handleRequestDataCase = async ( - actionEvent: UIRequestData - ): Promise => { - const { messageId, payload } = actionEvent; - const { requestType, params } = payload; - console.log('[MCP-UI] Data request received:', { messageId, requestType, params }); - return { - status: 'success' as const, - message: `Data request received: ${requestType}`, - data: { - messageId, - requestType, - params, - response: { status: 'acknowledged' }, - }, - }; - }; - try { switch (actionEvent.type) { case 'tool': - result = await handleToolCase(actionEvent); + result = await handleToolAction(actionEvent); break; case 'prompt': - result = await handlePromptCase(actionEvent); + result = await handlePromptAction(actionEvent); break; case 'link': - result = await handleLinkCase(actionEvent); + result = await handleLinkAction(actionEvent); break; case 'notify': - result = await handleNotifyCase(actionEvent); + result = await handleNotifyAction(actionEvent); break; case 'intent': - result = await handleIntentCase(actionEvent); - break; - - case 'ui-size-change': - result = await handleSizeChangeCase(actionEvent); - break; - - case 'ui-lifecycle-iframe-ready': - result = await handleIframeReadyCase(actionEvent); - break; - - case 'ui-request-data': - result = await handleRequestDataCase(actionEvent); + result = await handleIntentAction(actionEvent); break; default: { const _exhaustiveCheck: never = actionEvent; - console.error('Unhandled action type:', _exhaustiveCheck); + console.error('Unhandled MCP-UI action type:', _exhaustiveCheck); result = { status: 'error', error: { @@ -372,7 +303,7 @@ export default function MCPUIResourceRenderer({ } } } catch (error) { - console.error('[MCP-UI] Unexpected error:', error); + console.error('Unexpected error handling MCP-UI action:', error); result = { status: 'error', error: { @@ -383,12 +314,6 @@ export default function MCPUIResourceRenderer({ }; } - if (result.status === 'error') { - console.error('[MCP-UI] Action failed:', result); - } else { - console.log('[MCP-UI] Action succeeded:', result); - } - return result; }; @@ -398,19 +323,20 @@ export default function MCPUIResourceRenderer({