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..cfd5126f38c9 --- /dev/null +++ b/crates/goose-server/src/routes/mcp_ui_proxy.rs @@ -0,0 +1,47 @@ +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")], + 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..5a8b94c2e14f --- /dev/null +++ b/crates/goose-server/src/routes/templates/mcp_ui_proxy.html @@ -0,0 +1,141 @@ + + + + + + + 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'),