From a828eacbba7c65378e252ca1b2bd29ff979f277e Mon Sep 17 00:00:00 2001 From: Andrew Harvard Date: Thu, 30 Oct 2025 13:39:19 -0400 Subject: [PATCH 01/52] upgrade @mcp-ui/client --- ui/desktop/package-lock.json | 8 ++++---- ui/desktop/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ui/desktop/package-lock.json b/ui/desktop/package-lock.json index 008375acf13a..9bece8e56d6f 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 ee19c0290888..10377a166df7 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", From 02b3d7c24ee2d066e6cb1704e476107cfa4d955d Mon Sep 17 00:00:00 2001 From: Andrew Harvard Date: Thu, 30 Oct 2025 13:40:50 -0400 Subject: [PATCH 02/52] declare supported content types --- ui/desktop/src/components/MCPUIResourceRenderer.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/desktop/src/components/MCPUIResourceRenderer.tsx b/ui/desktop/src/components/MCPUIResourceRenderer.tsx index c73aab418b2c..19083441de27 100644 --- a/ui/desktop/src/components/MCPUIResourceRenderer.tsx +++ b/ui/desktop/src/components/MCPUIResourceRenderer.tsx @@ -398,6 +398,7 @@ export default function MCPUIResourceRenderer({ Date: Thu, 30 Oct 2025 13:43:54 -0400 Subject: [PATCH 03/52] simplify ui action handling --- .../src/components/MCPUIResourceRenderer.tsx | 120 ++---------------- 1 file changed, 14 insertions(+), 106 deletions(-) diff --git a/ui/desktop/src/components/MCPUIResourceRenderer.tsx b/ui/desktop/src/components/MCPUIResourceRenderer.tsx index 19083441de27..71215e2403af 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'; @@ -133,13 +99,11 @@ export default function MCPUIResourceRenderer({ console.log('[MCP-UI] Current theme value:', theme); }, []); - 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 +120,7 @@ export default function MCPUIResourceRenderer({ }; }; - const handlePromptCase = async ( + const handlePromptAction = async ( actionEvent: UIActionResultPrompt ): Promise => { const { prompt } = actionEvent.payload; @@ -191,7 +155,7 @@ export default function MCPUIResourceRenderer({ }; }; - const handleLinkCase = async (actionEvent: UIActionResultLink) => { + const handleLinkAction = async (actionEvent: UIActionResultLink) => { const { url } = actionEvent.payload; try { @@ -244,7 +208,7 @@ export default function MCPUIResourceRenderer({ } }; - const handleNotifyCase = async ( + const handleNotifyAction = async ( actionEvent: UIActionResultNotification ): Promise => { const { message } = actionEvent.payload; @@ -262,7 +226,7 @@ export default function MCPUIResourceRenderer({ }; }; - const handleIntentCase = async ( + const handleIntentAction = async ( actionEvent: UIActionResultIntent ): Promise => { toast.info( @@ -285,77 +249,27 @@ 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' }, - }, - }; - }; - + console.log('ACTION EVENT:', actionEvent); 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: { @@ -372,7 +286,7 @@ export default function MCPUIResourceRenderer({ } } } catch (error) { - console.error('[MCP-UI] Unexpected error:', error); + console.error('Unexpected error:', error); result = { status: 'error', error: { @@ -383,12 +297,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; }; From be436f7f078b973ab95b7d66bfafc7bb47c3fd42 Mon Sep 17 00:00:00 2001 From: Andrew Harvard Date: Thu, 30 Oct 2025 17:10:02 -0400 Subject: [PATCH 04/52] set up self-host proxy for UIResourceRenderer --- .../src/components/MCPUIResourceRenderer.tsx | 20 ++- ui/desktop/src/main.ts | 57 ++++++++ ui/desktop/src/preload.ts | 2 + ui/desktop/static/mcp-ui-proxy.html | 136 ++++++++++++++++++ 4 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 ui/desktop/static/mcp-ui-proxy.html diff --git a/ui/desktop/src/components/MCPUIResourceRenderer.tsx b/ui/desktop/src/components/MCPUIResourceRenderer.tsx index 71215e2403af..cc9001fda4fe 100644 --- a/ui/desktop/src/components/MCPUIResourceRenderer.tsx +++ b/ui/desktop/src/components/MCPUIResourceRenderer.tsx @@ -92,11 +92,27 @@ 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); + + // Fetch the MCP proxy URL from the main process + const fetchProxyUrl = async () => { + try { + const url = await window.electron.getMcpUIProxyUrl(); + if (url) { + setProxyUrl(url); + } else { + console.error('Failed to get proxy URL'); + } + } catch (error) { + console.error('Error fetching proxy URL:', error); + } + }; + + fetchProxyUrl(); }, []); const handleUIAction = async (actionEvent: UIActionResult): Promise => { @@ -312,7 +328,6 @@ export default function MCPUIResourceRenderer({ height: true, width: false, // set to false to allow for responsive design }, - sandboxPermissions: 'allow-forms', // enabled for experimentation, is spread into underlying iframe defaults iframeRenderData: { // iframeRenderData allows us to pass data down to MCP-UIs // MPC-UIs might find stuff like host and theme for conditional rendering @@ -320,6 +335,7 @@ export default function MCPUIResourceRenderer({ host: 'goose', theme: currentThemeValue, }, + proxy: proxyUrl, // refer to https://mcpui.dev/guide/client/using-a-proxy }} /> diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index c13d472d4b4e..956e2ab78951 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -52,6 +52,8 @@ import { Recipe } from './recipe'; import './utils/recipeHash'; import { Client, createClient, createConfig } from './api/client'; import installExtension, { REACT_DEVELOPER_TOOLS } from 'electron-devtools-installer'; +import express from 'express'; +import http from 'node:http'; // Updater functions (moved here to keep updates.ts minimal for release replacement) function shouldSetupUpdater(): boolean { @@ -1024,6 +1026,13 @@ ipcMain.handle('get-secret-key', () => { return SERVER_SECRET; }); +ipcMain.handle('get-mcp-ui-proxy-url', () => { + if (mcpUIProxyServerPort) { + return `http://localhost:${mcpUIProxyServerPort}/mcp-ui-proxy.html`; + } + return undefined; +}); + ipcMain.handle('get-goosed-host-port', async (event) => { const windowId = BrowserWindow.fromWebContents(event.sender)?.id; if (!windowId) { @@ -1642,10 +1651,51 @@ const registerGlobalHotkey = (accelerator: string) => { } }; +// HTTP server for serving MCP proxy files +let mcpUIProxyServerPort: number | null = null; +let mcpUIProxyServer: http.Server | null = null; + +async function startMcpUIProxyServer(): Promise { + return new Promise((resolve, reject) => { + const expressApp = express(); + const staticPath = path.join(__dirname, '../../static'); + + // Serve static files from the static directory + expressApp.use(express.static(staticPath)); + + // Create HTTP server + mcpUIProxyServer = http.createServer(expressApp); + + // Listen on a dynamic port (0 = let the OS choose an available port) + mcpUIProxyServer.listen(0, 'localhost', () => { + const address = mcpUIProxyServer?.address(); + if (address && typeof address === 'object') { + mcpUIProxyServerPort = address.port; + log.info(`MCP UI Proxy server started on port ${mcpUIProxyServerPort}`); + resolve(mcpUIProxyServerPort); + } else { + reject(new Error('Failed to get server address')); + } + }); + + mcpUIProxyServer.on('error', (error) => { + log.error('MCP Proxy server error:', error); + reject(error); + }); + }); +} + async function appMain() { // Ensure Windows shims are available before any MCP processes are spawned await ensureWinShims(); + // Start MCP proxy server + try { + await startMcpUIProxyServer(); + } catch (error) { + log.error('Failed to start MCP proxy server:', error); + } + registerUpdateIpcHandlers(); // Handle microphone permission requests @@ -2192,6 +2242,13 @@ app.on('will-quit', async () => { } windowPowerSaveBlockers.clear(); + // Close MCP proxy server + if (mcpUIProxyServer) { + mcpUIProxyServer.close(() => { + log.info('MCP UI Proxy server closed'); + }); + } + // Unregister all shortcuts when quitting globalShortcut.unregisterAll(); diff --git a/ui/desktop/src/preload.ts b/ui/desktop/src/preload.ts index 58f1b6c5b794..8d762e4014fb 100644 --- a/ui/desktop/src/preload.ts +++ b/ui/desktop/src/preload.ts @@ -80,6 +80,7 @@ type ElectronAPI = { getSettings: () => Promise; getSecretKey: () => Promise; getGoosedHostPort: () => Promise; + getMcpUIProxyUrl: () => Promise; setSchedulingEngine: (engine: string) => Promise; setWakelock: (enable: boolean) => Promise; getWakelockState: () => Promise; @@ -181,6 +182,7 @@ const electronAPI: ElectronAPI = { getSettings: () => ipcRenderer.invoke('get-settings'), getSecretKey: () => ipcRenderer.invoke('get-secret-key'), getGoosedHostPort: () => ipcRenderer.invoke('get-goosed-host-port'), + getMcpUIProxyUrl: () => ipcRenderer.invoke('get-mcp-ui-proxy-url'), setSchedulingEngine: (engine: string) => ipcRenderer.invoke('set-scheduling-engine', engine), setWakelock: (enable: boolean) => ipcRenderer.invoke('set-wakelock', enable), getWakelockState: () => ipcRenderer.invoke('get-wakelock-state'), diff --git a/ui/desktop/static/mcp-ui-proxy.html b/ui/desktop/static/mcp-ui-proxy.html new file mode 100644 index 000000000000..63379523d183 --- /dev/null +++ b/ui/desktop/static/mcp-ui-proxy.html @@ -0,0 +1,136 @@ + + + + + + + MCP-UI Proxy + + + + + + \ No newline at end of file From b49fd9caeeef376ed1e5037a7b290195e5a2f805 Mon Sep 17 00:00:00 2001 From: Andrew Harvard Date: Fri, 31 Oct 2025 08:38:26 -0400 Subject: [PATCH 05/52] add media-src CSP to mcp-ui-proxy --- ui/desktop/static/mcp-ui-proxy.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/desktop/static/mcp-ui-proxy.html b/ui/desktop/static/mcp-ui-proxy.html index 63379523d183..c95aa2a64629 100644 --- a/ui/desktop/static/mcp-ui-proxy.html +++ b/ui/desktop/static/mcp-ui-proxy.html @@ -10,12 +10,13 @@ - font-src: Allow fonts from any origin - connect-src: Allow network requests to any origin - frame-src: Allow embedding iframes from any origin (required for proxy functionality) + - media-src: Allow audio/video media from any origin - base-uri: Restrict tag to same-origin only - upgrade-insecure-requests: Automatically upgrade HTTP to HTTPS --> + content="default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'unsafe-inline' 'unsafe-eval' blob: https://cdn.tailwindcss.com https://cdn.jsdelivr.net https://unpkg.com; style-src * 'unsafe-inline'; font-src *; connect-src *; frame-src *; media-src *; base-uri 'self'; upgrade-insecure-requests"/> MCP-UI Proxy + + + + + diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index fd45570d58bf..4de8d27892df 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/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 75de116e5690..19bd93e25a9a 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -1722,6 +1722,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 e251d297d311..b977c17701da 100644 --- a/ui/desktop/src/components/MCPUIResourceRenderer.tsx +++ b/ui/desktop/src/components/MCPUIResourceRenderer.tsx @@ -98,14 +98,14 @@ export default function MCPUIResourceRenderer({ const theme = localStorage.getItem('theme') || 'light'; setCurrentThemeValue(theme); - // Fetch the MCP-UI proxy URL from the main process const fetchProxyUrl = async () => { try { - const url = await window.electron.getMcpUIProxyUrl(); - if (url) { - setProxyUrl(url); + 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 MCP-UI Proxy URL'); + console.error('Failed to get goosed host/port or secret key'); } } catch (error) { console.error('Error fetching MCP-UI Proxy URL:', error); diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index 20bbb0ebfbd0..bf9456bede8b 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -51,7 +51,6 @@ import { Recipe } from './recipe'; import './utils/recipeHash'; import { Client, createClient, createConfig } from './api/client'; import installExtension, { REACT_DEVELOPER_TOOLS } from 'electron-devtools-installer'; -import { initMcpUIProxy } from './proxy'; // Updater functions (moved here to keep updates.ts minimal for release replacement) function shouldSetupUpdater(): boolean { @@ -1718,8 +1717,6 @@ async function appMain() { // Ensure Windows shims are available before any MCP processes are spawned await ensureWinShims(); - await initMcpUIProxy(MAIN_WINDOW_VITE_DEV_SERVER_URL); - registerUpdateIpcHandlers(); // Handle microphone permission requests diff --git a/ui/desktop/src/preload.ts b/ui/desktop/src/preload.ts index abbe3d096aee..070e8a0afeb7 100644 --- a/ui/desktop/src/preload.ts +++ b/ui/desktop/src/preload.ts @@ -80,7 +80,6 @@ type ElectronAPI = { getSettings: () => Promise; getSecretKey: () => Promise; getGoosedHostPort: () => Promise; - getMcpUIProxyUrl: () => Promise; setWakelock: (enable: boolean) => Promise; getWakelockState: () => Promise; openNotificationsSettings: () => Promise; @@ -186,7 +185,6 @@ const electronAPI: ElectronAPI = { getSettings: () => ipcRenderer.invoke('get-settings'), getSecretKey: () => ipcRenderer.invoke('get-secret-key'), getGoosedHostPort: () => ipcRenderer.invoke('get-goosed-host-port'), - getMcpUIProxyUrl: () => ipcRenderer.invoke('get-mcp-ui-proxy-url'), setWakelock: (enable: boolean) => ipcRenderer.invoke('set-wakelock', enable), getWakelockState: () => ipcRenderer.invoke('get-wakelock-state'), openNotificationsSettings: () => ipcRenderer.invoke('open-notifications-settings'), From 7aef5a2f8c872a7d8f3a091b92299f6fa92aae22 Mon Sep 17 00:00:00 2001 From: Andrew Harvard Date: Tue, 18 Nov 2025 08:32:27 -0500 Subject: [PATCH 51/52] remove old proxy files from client --- ui/desktop/src/proxy.ts | 411 ---------------------------- ui/desktop/static/mcp-ui-proxy.html | 141 ---------- 2 files changed, 552 deletions(-) delete mode 100644 ui/desktop/src/proxy.ts delete mode 100644 ui/desktop/static/mcp-ui-proxy.html diff --git a/ui/desktop/src/proxy.ts b/ui/desktop/src/proxy.ts deleted file mode 100644 index 983614cd0d43..000000000000 --- a/ui/desktop/src/proxy.ts +++ /dev/null @@ -1,411 +0,0 @@ -/** - * MCP-UI Proxy Server - * - * This module manages a local HTTP proxy server that securely serves MCP-UI interface files - * to Electron webviews/iframes. The security model uses three layers of defense: - * - * 1. Token-based authentication: A random token is generated at startup and must be - * included in the X-MCP-UI-Proxy-Token header for all requests. - * - * 2. Origin validation: Requests must originate from the Electron app itself - * (file:// protocol in production, localhost:PORT in dev mode). - * - * 3. WebContents whitelisting: Only specific, trusted webContents can receive the proxy token. - * The token is automatically injected via webRequest.onBeforeSendHeaders, but only for - * webContents that have been explicitly registered as trusted. - * - * SECURITY FLOW: - * ============== - * - * A. Initialization (initMcpUIProxy): - * 1. Generate random proxy authentication token - * 2. Start HTTP proxy server on dynamic port - * 3. Set up webRequest.onBeforeSendHeaders handlers for known sessions - * 4. Register web-contents-created listener to track new webContents - * - * B. WebContents Registration (web-contents-created event): - * 1. When a new webContents is created, validate its type (window/webview only) - * 2. Validate its URL (must be file:// or dev server origin) - * 3. If valid, add its ID to trustedWebContentsIds Set - * 4. Set up cleanup to remove ID when webContents is destroyed - * - * Note: This event is synchronous, so the ID is added before the webContents - * can make any HTTP requests. - * - * C. Request Interception (onBeforeSendHeaders): - * 1. When any webContents makes an HTTP request, check if it's to the proxy server - * 2. Check if the webContents ID is in trustedWebContentsIds Set - * 3. Only inject the proxy token header if BOTH conditions are true - * 4. Log warnings when untrusted webContents attempt to access the proxy - * - * D. IPC Handler (get-mcp-ui-proxy-url): - * 1. Renderer processes request the proxy URL via IPC - * 2. Validate the sender's URL (must be file:// or dev server origin) - * 3. Only return the proxy URL to trusted origins - * - * This multi-layered approach prevents: - * - External browsers from accessing the proxy (token + origin validation) - * - Compromised/untrusted renderer processes from getting the proxy URL (IPC validation) - * - Compromised/untrusted webContents from getting the token injected (whitelist) - * - Malicious iframes/popups from accessing the proxy (type + URL validation) - */ - -import * as crypto from 'crypto'; -import type { Session } from 'electron'; -import { app, ipcMain, session } from 'electron'; -import express from 'express'; -import fsSync from 'node:fs'; -import http from 'node:http'; -import path from 'node:path'; -import log from './utils/logger'; - -// Security constants -const TOKEN_BYTE_LENGTH = 32; -const TOKEN_PREFIX = 'mcp-ui-proxy'; -const PROXY_TOKEN_HEADER = 'x-mcp-ui-proxy-token'; -const PROXY_HTML_PATH = '/mcp-ui-proxy.html'; - -// Server configuration -const PROXY_SERVER_HOST = 'localhost'; -const PROXY_SERVER_PORT = 0; // 0 = OS assigns available port - -// Allowed hostnames for proxy requests -// Includes both IPv4 and IPv6 loopback addresses -const ALLOWED_HOSTNAMES = ['localhost', '127.0.0.1', '::1'] as const; - -// State management -let mcpUIProxyServerPort: number | null = null; -let mcpUIProxyServer: http.Server | null = null; -let MCP_UI_PROXY_TOKEN: string | null = null; - -// Track trusted webContents IDs that are allowed to use the proxy -const trustedWebContentsIds = new Set(); - -// Type definitions -interface SecurityCheckResult { - isValid: boolean; - reason?: string; -} - -/** - * Validates the MCP-UI proxy token from request headers - */ -function validateProxyToken(token: string | undefined): SecurityCheckResult { - if (token !== MCP_UI_PROXY_TOKEN) { - return { - isValid: false, - reason: 'Invalid or missing proxy token', - }; - } - return { isValid: true }; -} - -/** - * Validates that the request originates from the Electron app - * @param origin - The origin or referer header from the request - * @param allowedOrigin - The expected origin (null in production for file:// protocol) - */ -function validateOrigin( - origin: string | undefined, - allowedOrigin: string | null -): SecurityCheckResult { - let isElectronRequest = false; - - if (allowedOrigin) { - // Dev mode: require exact localhost:port match - isElectronRequest = origin?.startsWith(allowedOrigin) || false; - } else { - // Production mode: accept file:// protocol or missing origin - // (iframes in file:// context often don't send origin/referer) - isElectronRequest = !origin || origin.startsWith('file://'); - } - - if (!isElectronRequest) { - return { - isValid: false, - reason: `Invalid origin. Got: ${origin || 'none'}, Expected: ${allowedOrigin || 'file:// or no origin'}`, - }; - } - - return { isValid: true }; -} - -/** - * Determines the correct path to static files based on environment - * @returns Absolute path to the static directory - */ -function getStaticPath(): string { - // In production: extraResources are in process.resourcesPath - // In dev: relative to the build directory - return app.isPackaged - ? path.join(process.resourcesPath, 'static') - : path.join(__dirname, '../../static'); -} - -/** - * Gracefully shuts down the MCP-UI proxy server - */ -async function shutdownMcpUIProxyServer(): Promise { - if (!mcpUIProxyServer) { - return; - } - - return new Promise((resolve) => { - mcpUIProxyServer!.close(() => { - log.info('MCP UI Proxy server closed'); - mcpUIProxyServer = null; - mcpUIProxyServerPort = null; - MCP_UI_PROXY_TOKEN = null; - resolve(); - }); - }); -} - -/** - * Initializes the MCP-UI proxy server and security infrastructure - * @param devUrl - The development server URL (undefined or empty string in production) - */ -export async function initMcpUIProxy(devUrl: string | undefined): Promise { - // Compute allowed origin for dev mode (null in production) - const ALLOWED_ORIGIN = devUrl ? new URL(devUrl).origin : null; - - // Generate secure random token for proxy authentication - MCP_UI_PROXY_TOKEN = `${TOKEN_PREFIX}:${crypto.randomBytes(TOKEN_BYTE_LENGTH).toString('hex')}`; - - // IPC handler to provide the proxy URL to renderer processes - // Security: Only allow trusted renderer processes to access the proxy URL - ipcMain.handle('get-mcp-ui-proxy-url', (event) => { - // Validate that the request comes from a trusted renderer - const senderUrl = event.sender.getURL(); - - // Allow requests from the main app (file:// in production, localhost in dev) - const isTrustedOrigin = - senderUrl.startsWith('file://') || (ALLOWED_ORIGIN && senderUrl.startsWith(ALLOWED_ORIGIN)); - - if (!isTrustedOrigin) { - log.warn(`Rejected get-mcp-ui-proxy-url request from untrusted origin: ${senderUrl}`); - return undefined; - } - - if (mcpUIProxyServerPort) { - return `http://${PROXY_SERVER_HOST}:${mcpUIProxyServerPort}${PROXY_HTML_PATH}`; - } - return undefined; - }); - - /** - * Starts the MCP-UI proxy HTTP server on a dynamic port - * @returns Promise resolving to the assigned port number - * @throws Error if server fails to start or bind to a port - */ - async function startMcpUIProxyServer(): Promise { - return new Promise((resolve, reject) => { - const expressApp = express(); - - // Get the appropriate static file directory - const staticPath = getStaticPath(); - - // Verify static directory exists (removed file listing for security) - if (!fsSync.existsSync(staticPath)) { - log.error(`MCP UI Proxy: static directory not found at ${staticPath}`); - } - - // Security middleware: validate token and origin on all requests - expressApp.use((req, res, next) => { - // Validate token (required for all requests) - const token = req.headers[PROXY_TOKEN_HEADER] as string | undefined; - const tokenCheck = validateProxyToken(token); - if (!tokenCheck.isValid) { - log.warn(`MCP-UI Proxy unauthorized: ${tokenCheck.reason}. IP: ${req.ip}`); - res.status(403).send('Forbidden'); - return; - } - - // Validate origin (defense in depth - ensure request is from Electron app) - const origin = req.headers.origin || req.headers.referer; - const originCheck = validateOrigin(origin, ALLOWED_ORIGIN); - if (!originCheck.isValid) { - log.warn(`MCP-UI Proxy unauthorized: ${originCheck.reason}. IP: ${req.ip}`); - res.status(403).send('Forbidden'); - return; - } - - next(); - }); - - // Serve only the specific file we need, not the entire directory - expressApp.get(PROXY_HTML_PATH, (_req, res) => { - const filePath = path.join(staticPath, 'mcp-ui-proxy.html'); - res.sendFile(filePath, (err) => { - if (err) { - log.error(`Failed to serve ${PROXY_HTML_PATH}:`, err); - res.status(404).send('Not Found'); - } - }); - }); - - // Return 404 for any other paths - expressApp.use((_req, res) => { - log.warn(`MCP-UI Proxy: 404 for path: ${_req.path}`); - res.status(404).send('Not Found'); - }); - - // Create HTTP server - mcpUIProxyServer = http.createServer(expressApp); - - // Listen on a dynamic port (0 = let the OS choose an available port) - mcpUIProxyServer.listen(PROXY_SERVER_PORT, PROXY_SERVER_HOST, () => { - const address = mcpUIProxyServer?.address(); - if (address && typeof address === 'object') { - mcpUIProxyServerPort = address.port; - log.info(`MCP UI Proxy server started on ${PROXY_SERVER_HOST}:${mcpUIProxyServerPort}`); - resolve(mcpUIProxyServerPort); - } else { - reject(new Error('Failed to get server address')); - } - }); - - mcpUIProxyServer.on('error', (error) => { - log.error('MCP UI Proxy server error:', error); - reject(error); - }); - }); - } - - // Start MCP-UI proxy server - try { - await startMcpUIProxyServer(); - } catch (error) { - log.error('Failed to start MCP-UI proxy server:', error); - } - - // Track sessions that have been set up to avoid duplicate handlers - const configuredSessions = new WeakSet(); - - /** - * Sets up HTTP header injection for MCP-UI proxy requests on a given session - * Ensures the proxy token is automatically included in requests to the proxy server - * This runs once per session to avoid duplicate handlers - * @param sess - The Electron session to configure - */ - const setupMcpProxyHeaderInjection = (sess: Session): void => { - // Skip if we've already configured this session - if (configuredSessions.has(sess)) { - return; - } - configuredSessions.add(sess); - - log.debug(`Setting up MCP-UI proxy header injection for session`); - - sess.webRequest.onBeforeSendHeaders((details, callback) => { - // Security: Only inject headers for requests from trusted webContents - const webContentsId = details.webContentsId; - - // Skip if webContentsId is undefined (shouldn't happen, but TypeScript requires the check) - if (webContentsId === undefined) { - callback({ cancel: false, requestHeaders: details.requestHeaders }); - return; - } - - // Inject MCP-UI proxy token header for requests to the MCP-UI proxy server - // Only if the request comes from a trusted webContents - if (mcpUIProxyServerPort && MCP_UI_PROXY_TOKEN && trustedWebContentsIds.has(webContentsId)) { - try { - const parsedUrl = new URL(details.url); - const isProxyRequest = - (ALLOWED_HOSTNAMES as readonly string[]).includes(parsedUrl.hostname) && - parsedUrl.port === String(mcpUIProxyServerPort); - - if (isProxyRequest) { - details.requestHeaders[PROXY_TOKEN_HEADER] = MCP_UI_PROXY_TOKEN; - log.debug(`Injected proxy token for trusted webContents ${webContentsId}`); - } - } catch (error) { - // If URL parsing fails, log and skip header injection - log.debug(`Failed to parse URL for header injection: ${details.url}`, error); - } - } else if (mcpUIProxyServerPort && !trustedWebContentsIds.has(webContentsId)) { - // Log when we skip injection for untrusted webContents - try { - const parsedUrl = new URL(details.url); - const isProxyRequest = - (ALLOWED_HOSTNAMES as readonly string[]).includes(parsedUrl.hostname) && - parsedUrl.port === String(mcpUIProxyServerPort); - - if (isProxyRequest) { - log.warn(`Blocked proxy token injection for untrusted webContents ${webContentsId}`); - } - } catch { - // Ignore URL parsing errors for logging - } - } - - callback({ cancel: false, requestHeaders: details.requestHeaders }); - }); - }; - - // Set up header injection immediately for known sessions - setupMcpProxyHeaderInjection(session.defaultSession); - const gooseSession = session.fromPartition('persist:goose'); - setupMcpProxyHeaderInjection(gooseSession); - - // Intercept new webContents to: - // 1. Set up header injection for their sessions - // 2. Track trusted webContents (main windows and their legitimate child frames) - // - // SECURITY NOTE: This event is synchronous and fires BEFORE the webContents can make - // any HTTP requests. This guarantees that the webContents ID is added to the trusted - // set before onBeforeSendHeaders can be called for that webContents. - app.on('web-contents-created', (_event, contents) => { - const contentsType = contents.getType(); - log.debug(`New webContents created (type: ${contentsType}, id: ${contents.id})`); - - // Set up header injection for the session (but actual injection only happens for trusted IDs) - // This ensures the session has the onBeforeSendHeaders handler installed - setupMcpProxyHeaderInjection(contents.session); - - // SECURITY CHECK 1: Type validation - // Only trust specific types of webContents: - // - 'window': Main application windows created by BrowserWindow - // - 'webview': Embedded webviews (used for MCP UIs) - // - Other types (backgroundPage, remote, etc.) are NOT trusted - const isTrustedType = contentsType === 'window' || contentsType === 'webview'; - - if (isTrustedType) { - // SECURITY CHECK 2: URL validation - // Validate the URL is from our app before trusting - // This prevents malicious windows/webviews from being trusted - const url = contents.getURL(); - const isTrustedUrl = - !url || // Empty URL at creation time is OK (will be set during load) - url.startsWith('file://') || // Production: app is served from file:// - (ALLOWED_ORIGIN && url.startsWith(ALLOWED_ORIGIN)); // Dev: app is on localhost - - if (isTrustedUrl) { - // REGISTER AS TRUSTED: Add to whitelist - // This ID will now pass the check in onBeforeSendHeaders - trustedWebContentsIds.add(contents.id); - log.info(`Registered trusted webContents ${contents.id} (type: ${contentsType})`); - - // CLEANUP: Remove from trusted set when destroyed - // This prevents the Set from growing unbounded and prevents - // ID reuse attacks (though Electron doesn't reuse IDs in practice) - contents.on('destroyed', () => { - trustedWebContentsIds.delete(contents.id); - log.debug(`Removed destroyed webContents ${contents.id} from trusted set`); - }); - } else { - // SECURITY: Reject webContents with untrusted URLs - log.warn(`Rejected webContents ${contents.id} with untrusted URL: ${url}`); - } - } else { - // SECURITY: Reject webContents with untrusted types - log.debug(`Skipped registering webContents ${contents.id} (type: ${contentsType})`); - } - }); - - // Register cleanup handler to gracefully shut down the proxy server on app quit - app.on('will-quit', async () => { - await shutdownMcpUIProxyServer(); - }); -} diff --git a/ui/desktop/static/mcp-ui-proxy.html b/ui/desktop/static/mcp-ui-proxy.html deleted file mode 100644 index 8f8152b3c4a3..000000000000 --- a/ui/desktop/static/mcp-ui-proxy.html +++ /dev/null @@ -1,141 +0,0 @@ - - - - - - - MCP-UI Proxy - - - - - - \ No newline at end of file From e9e18d7ac042308e6b30909e1c84f5db16dc498e Mon Sep 17 00:00:00 2001 From: Zane <75694352+zanesq@users.noreply.github.com> Date: Tue, 18 Nov 2025 11:31:22 -0800 Subject: [PATCH 52/52] [MCP-UI] proxy add Referrer-Policy HTTP response header (#5797) Co-authored-by: Andrew Harvard --- crates/goose-server/src/routes/mcp_ui_proxy.rs | 8 +++++++- .../goose-server/src/routes/templates/mcp_ui_proxy.html | 1 + ui/desktop/forge.config.ts | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/goose-server/src/routes/mcp_ui_proxy.rs b/crates/goose-server/src/routes/mcp_ui_proxy.rs index cfd5126f38c9..2489c8d98f16 100644 --- a/crates/goose-server/src/routes/mcp_ui_proxy.rs +++ b/crates/goose-server/src/routes/mcp_ui_proxy.rs @@ -34,7 +34,13 @@ async fn mcp_ui_proxy( } ( - [(header::CONTENT_TYPE, "text/html; charset=utf-8")], + [ + (header::CONTENT_TYPE, "text/html; charset=utf-8"), + ( + header::HeaderName::from_static("referrer-policy"), + "no-referrer", + ), + ], Html(MCP_UI_PROXY_HTML), ) .into_response() diff --git a/crates/goose-server/src/routes/templates/mcp_ui_proxy.html b/crates/goose-server/src/routes/templates/mcp_ui_proxy.html index 5a8b94c2e14f..0cac94a889da 100644 --- a/crates/goose-server/src/routes/templates/mcp_ui_proxy.html +++ b/crates/goose-server/src/routes/templates/mcp_ui_proxy.html @@ -2,6 +2,7 @@ +