Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
a828eac
upgrade @mcp-ui/client
aharvard Oct 30, 2025
02b3d7c
declare supported content types
aharvard Oct 30, 2025
8da9d52
simplify ui action handling
aharvard Oct 30, 2025
be436f7
set up self-host proxy for UIResourceRenderer
aharvard Oct 30, 2025
b49fd9c
add media-src CSP to mcp-ui-proxy
aharvard Oct 31, 2025
8f877fa
relax script-src CSP on MCP-UI Proxy
aharvard Oct 31, 2025
0ce5a8a
Enhance MCP-UI proxy server security with token validation and origin…
aharvard Oct 31, 2025
94d2162
update console errors for clarity
aharvard Oct 31, 2025
745f2eb
Revert "Enhance MCP-UI proxy server security with token validation an…
aharvard Oct 31, 2025
715c09a
fix static path issue for builds
aharvard Oct 31, 2025
c42f12b
bring back MCP-UI proxy security
aharvard Oct 31, 2025
72f1ef1
remove troubleshooting text in UI
aharvard Oct 31, 2025
7677e76
Update ui/desktop/src/components/MCPUIResourceRenderer.tsx
aharvard Nov 3, 2025
5af7c66
fix: use consistent lowercase casing for mcp-ui-proxy-token header
aharvard Nov 3, 2025
949f9b4
Update ui/desktop/src/components/MCPUIResourceRenderer.tsx
aharvard Nov 3, 2025
fec432d
refactor: remove excessive debug logging from MCP-UI proxy
aharvard Nov 3, 2025
093bf6c
fix: await MCP-UI proxy server close on app quit
aharvard Nov 3, 2025
9084e51
fix: update Content-Security-Policy in MCP-UI proxy HTML
aharvard Nov 3, 2025
8f2bd7e
Update ui/desktop/src/components/MCPUIResourceRenderer.tsx
aharvard Nov 3, 2025
4f38d64
Update ui/desktop/src/main.ts
aharvard Nov 3, 2025
2f9c187
Update ui/desktop/src/main.ts
aharvard Nov 3, 2025
ed320d2
Update ui/desktop/src/main.ts
aharvard Nov 3, 2025
5af994a
security: add origin validation to MCP-UI proxy postMessage calls
aharvard Nov 3, 2025
4d04991
Update ui/desktop/src/main.ts
aharvard Nov 3, 2025
972f95f
Update ui/desktop/src/main.ts
aharvard Nov 3, 2025
6a748ec
Update ui/desktop/src/main.ts
aharvard Nov 3, 2025
6522b1b
Update ui/desktop/src/main.ts
aharvard Nov 3, 2025
927c969
Update ui/desktop/static/mcp-ui-proxy.html
aharvard Nov 3, 2025
127ba09
security: use 'null' origin instead of wildcard for srcdoc iframe
aharvard Nov 3, 2025
80b2c21
refactor: consolidate webRequest handlers and fix origin validation l…
aharvard Nov 3, 2025
b1e8628
Update ui/desktop/src/main.ts
aharvard Nov 3, 2025
5eb9bf3
Update ui/desktop/src/components/MCPUIResourceRenderer.tsx
aharvard Nov 3, 2025
52304d7
Update ui/desktop/src/main.ts
aharvard Nov 3, 2025
10468bb
fix issues that co-pilot code review introduced
aharvard Nov 3, 2025
d3f5360
fix spelling in comment
aharvard Nov 3, 2025
72e52e8
fix: add null check for iframe contentWindow in MCP-UI proxy
aharvard Nov 3, 2025
bf42719
optimize ALLOWED_ORIGIN value
aharvard Nov 3, 2025
8c3988d
refactor: move MCP-UI proxy logic to a dedicated module
aharvard Nov 6, 2025
881eaa5
refactor and document proxy code for maintainability
aharvard Nov 6, 2025
ee9959a
Merge remote-tracking branch 'origin/main' into feat/mcp-ui-improvements
aharvard Nov 6, 2025
c444706
address copilot feedback
aharvard Nov 6, 2025
913b0bf
update pnpm lock post-merge from main
aharvard Nov 6, 2025
c170e4c
remove accidental pnpm-lock
aharvard Nov 7, 2025
f2c66e1
security: validate IPC caller origin for MCP-UI proxy URL
aharvard Nov 10, 2025
356b409
security: restrict proxy token injection to trusted webContents
aharvard Nov 10, 2025
dfe457e
security: add IPv6 loopback support to proxy allowed hostnames
aharvard Nov 10, 2025
e1590f0
security: replace blanket static serving with explicit route
aharvard Nov 10, 2025
92d7c3c
Merge main and resolve conflict in preload.ts
aharvard Nov 10, 2025
8433401
Update ui/desktop/src/components/MCPUIResourceRenderer.tsx
aharvard Nov 10, 2025
40276bf
fix: remove orphaned setSchedulingEngine from preload.ts
aharvard Nov 10, 2025
a6daeed
fix: prefix unused req parameters with underscore in proxy.ts
aharvard Nov 10, 2025
b6740fb
feat: MCP UI proxy to goose-server (#5749)
alexhancock Nov 17, 2025
273f4de
Merge remote-tracking branch 'origin/main' into feat/mcp-ui-improvements
aharvard Nov 18, 2025
7aef5a2
remove old proxy files from client
aharvard Nov 18, 2025
e9e18d7
[MCP-UI] proxy add Referrer-Policy HTTP response header (#5797)
zanesq Nov 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions ui/desktop/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion ui/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
141 changes: 33 additions & 108 deletions ui/desktop/src/components/MCPUIResourceRenderer.tsx
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

just a note regarding the deletions in this file: previously we were over-handling messages form the iframe, upgrading @mcp-ui/client and using a proxy takes most of this off our plate.

Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
UIActionResultNotification,
UIActionResultPrompt,
UIActionResultToolCall,
UIActionResult,
} from '@mcp-ui/client';
import { useState, useEffect } from 'react';
import { toast } from 'react-toastify';
Expand All @@ -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<string, unknown>;
};

type UIRequestData = {
type: 'ui-request-data';
messageId: string;
payload: {
requestType: string;
params: Record<string, unknown>;
};
};

// 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<T = unknown> = {
status: 'success';
Expand Down Expand Up @@ -126,20 +92,34 @@ export default function MCPUIResourceRenderer({
appendPromptToChat,
}: MCPUIResourceRendererProps) {
const [currentThemeValue, setCurrentThemeValue] = useState<string>('light');
const [proxyUrl, setProxyUrl] = useState<string | undefined>(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: ActionEventsFromIframe
): Promise<UIActionHandlerResult> => {
const handleUIAction = async (actionEvent: UIActionResult): Promise<UIActionHandlerResult> => {
// result to pass back to the MCP-UI
let result: UIActionHandlerResult;

const handleToolCase = async (
const handleToolAction = async (
actionEvent: UIActionResultToolCall
): Promise<UIActionHandlerResult> => {
const { toolName, params } = actionEvent.payload;
Expand All @@ -156,7 +136,7 @@ export default function MCPUIResourceRenderer({
};
};

const handlePromptCase = async (
const handlePromptAction = async (
actionEvent: UIActionResultPrompt
): Promise<UIActionHandlerResult> => {
const { prompt } = actionEvent.payload;
Expand Down Expand Up @@ -191,7 +171,7 @@ export default function MCPUIResourceRenderer({
};
};

const handleLinkCase = async (actionEvent: UIActionResultLink) => {
const handleLinkAction = async (actionEvent: UIActionResultLink) => {
const { url } = actionEvent.payload;

try {
Expand Down Expand Up @@ -244,7 +224,7 @@ export default function MCPUIResourceRenderer({
}
};

const handleNotifyCase = async (
const handleNotifyAction = async (
actionEvent: UIActionResultNotification
): Promise<UIActionHandlerResult> => {
const { message } = actionEvent.payload;
Expand All @@ -262,7 +242,7 @@ export default function MCPUIResourceRenderer({
};
};

const handleIntentCase = async (
const handleIntentAction = async (
actionEvent: UIActionResultIntent
): Promise<UIActionHandlerResult> => {
toast.info(
Expand All @@ -285,77 +265,27 @@ export default function MCPUIResourceRenderer({
};
};

const handleSizeChangeCase = async (
actionEvent: UISizeChange
): Promise<UIActionHandlerResult> => {
return {
status: 'success' as const,
message: 'Size change handled',
data: actionEvent.payload,
};
};

const handleIframeReadyCase = async (
actionEvent: UILifecycleIframeReady
): Promise<UIActionHandlerResult> => {
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<UIActionHandlerResult> => {
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: {
Expand All @@ -372,7 +302,7 @@ export default function MCPUIResourceRenderer({
}
}
} catch (error) {
console.error('[MCP-UI] Unexpected error:', error);
console.error('Unexpected error:', error);
result = {
status: 'error',
error: {
Expand All @@ -383,12 +313,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;
};

Expand All @@ -398,19 +322,20 @@ export default function MCPUIResourceRenderer({
<UIResourceRenderer
resource={content.resource}
onUIAction={handleUIAction}
supportedContentTypes={['rawHtml', 'externalUrl']} // Goose does not support remoteDOM content
htmlProps={{
autoResizeIframe: {
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
// usage of this is experimental, leaving in place for demos
host: 'goose',
theme: currentThemeValue,
},
proxy: proxyUrl, // refer to https://mcpui.dev/guide/client/using-a-proxy
}}
/>
</div>
Expand Down
57 changes: 57 additions & 0 deletions ui/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<number> {
return new Promise((resolve, reject) => {
const expressApp = express();
Copy link
Collaborator

Choose a reason for hiding this comment

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

nice - and this looks like it is already in the dependencies from electron?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

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
Expand Down Expand Up @@ -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();

Expand Down
2 changes: 2 additions & 0 deletions ui/desktop/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ type ElectronAPI = {
getSettings: () => Promise<unknown | null>;
getSecretKey: () => Promise<string>;
getGoosedHostPort: () => Promise<string | null>;
getMcpUIProxyUrl: () => Promise<string | undefined>;
setSchedulingEngine: (engine: string) => Promise<boolean>;
setWakelock: (enable: boolean) => Promise<boolean>;
getWakelockState: () => Promise<boolean>;
Expand Down Expand Up @@ -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'),
Expand Down
Loading
Loading