From 2bc8fccaf0be7132eb12e23cbb3b37c092862877 Mon Sep 17 00:00:00 2001 From: Louis Date: Mon, 19 May 2025 13:15:37 +0700 Subject: [PATCH] chore: allow users to enable/disable MCP servers (#5015) --- .../conversational-extension/package.json | 2 +- .../package.json | 2 +- extensions/model-extension/package.json | 2 +- src-tauri/src/core/mcp.rs | 14 ++- web-app/src/hooks/useMCPServers.ts | 1 + web-app/src/hooks/useProxyConfig.ts | 59 +++++++++ web-app/src/routes/settings/extensions.tsx | 41 +------ web-app/src/routes/settings/https-proxy.tsx | 116 ++++++++++++++++-- web-app/src/routes/settings/mcp-servers.tsx | 17 ++- web-app/src/services/models.ts | 51 ++++++++ web-app/src/types/modelProviders.d.ts | 17 +++ 11 files changed, 273 insertions(+), 49 deletions(-) create mode 100644 web-app/src/hooks/useProxyConfig.ts diff --git a/extensions/conversational-extension/package.json b/extensions/conversational-extension/package.json index 693adf6d6..abb76e4d0 100644 --- a/extensions/conversational-extension/package.json +++ b/extensions/conversational-extension/package.json @@ -2,7 +2,7 @@ "name": "@janhq/conversational-extension", "productName": "Conversational", "version": "1.0.0", - "description": "Enables conversations and state persistence via your filesystem.", + "description": "Enables conversations and state persistence via your file system.", "main": "dist/index.js", "author": "Jan ", "license": "MIT", diff --git a/extensions/hardware-management-extension/package.json b/extensions/hardware-management-extension/package.json index 396404df9..08346b3f2 100644 --- a/extensions/hardware-management-extension/package.json +++ b/extensions/hardware-management-extension/package.json @@ -2,7 +2,7 @@ "name": "@janhq/hardware-management-extension", "productName": "Hardware Management", "version": "1.0.0", - "description": "Manages Better Hardware settings.", + "description": "Manages hardware settings.", "main": "dist/index.js", "node": "dist/node/index.cjs.js", "author": "Jan ", diff --git a/extensions/model-extension/package.json b/extensions/model-extension/package.json index 32ef2f70c..9ce48da88 100644 --- a/extensions/model-extension/package.json +++ b/extensions/model-extension/package.json @@ -2,7 +2,7 @@ "name": "@janhq/model-extension", "productName": "Model Management", "version": "1.0.36", - "description": "Handles model lists, their details, and settings.", + "description": "Handles model list, and settings.", "main": "dist/index.js", "author": "Jan ", "license": "AGPL-3.0", diff --git a/src-tauri/src/core/mcp.rs b/src-tauri/src/core/mcp.rs index d7748611d..635c443a9 100644 --- a/src-tauri/src/core/mcp.rs +++ b/src-tauri/src/core/mcp.rs @@ -34,9 +34,15 @@ pub async fn run_mcp_commands( log::info!("MCP Servers: {server_map:#?}"); let exe_path = env::current_exe().expect("Failed to get current exe path"); - let exe_parent_path = exe_path.parent().expect("Executable must have a parent directory"); + let exe_parent_path = exe_path + .parent() + .expect("Executable must have a parent directory"); let bin_path = exe_parent_path.to_path_buf(); for (name, config) in server_map { + if let Some(false) = extract_active_status(config) { + log::info!("Server {name} is not active, skipping."); + continue; + } if let Some((command, args, envs)) = extract_command_args(config) { let mut cmd = Command::new(command.clone()); if command.clone() == "npx" { @@ -96,6 +102,12 @@ fn extract_command_args( Some((command, args, envs)) } +fn extract_active_status(config: &Value) -> Option { + let obj = config.as_object()?; + let active = obj.get("active")?.as_bool()?; + Some(active) +} + #[tauri::command] pub async fn restart_mcp_servers(app: AppHandle, state: State<'_, AppState>) -> Result<(), String> { let app_path = get_jan_data_folder_path(app.clone()); diff --git a/web-app/src/hooks/useMCPServers.ts b/web-app/src/hooks/useMCPServers.ts index 61ea97821..96cde5b98 100644 --- a/web-app/src/hooks/useMCPServers.ts +++ b/web-app/src/hooks/useMCPServers.ts @@ -8,6 +8,7 @@ export type MCPServerConfig = { command: string args: string[] env: Record + active?: boolean } // Define the structure of all MCP servers diff --git a/web-app/src/hooks/useProxyConfig.ts b/web-app/src/hooks/useProxyConfig.ts new file mode 100644 index 000000000..ed7201265 --- /dev/null +++ b/web-app/src/hooks/useProxyConfig.ts @@ -0,0 +1,59 @@ +import { create } from 'zustand' +import { persist, createJSONStorage } from 'zustand/middleware' +import { localStoregeKey } from '@/constants/localStorage' + +type ProxyConfigState = { + proxyEnabled: boolean + proxyUrl: string + proxyUsername: string + proxyPassword: string + proxyIgnoreSSL: boolean + verifyProxySSL: boolean + verifyProxyHostSSL: boolean + verifyPeerSSL: boolean + verifyHostSSL: boolean + noProxy: string + // Function to set the proxy configuration + setProxyEnabled: (proxyEnabled: boolean) => void + setProxyUrl: (proxyUrl: string) => void + setProxyUsername: (proxyUsername: string) => void + setProxyPassword: (proxyPassword: string) => void + setProxyIgnoreSSL: (proxyIgnoreSSL: boolean) => void + setVerifyProxySSL: (verifyProxySSL: boolean) => void + setVerifyProxyHostSSL: (verifyProxyHostSSL: boolean) => void + setVerifyPeerSSL: (verifyPeerSSL: boolean) => void + setVerifyHostSSL: (verifyHostSSL: boolean) => void + setNoProxy: (noProxy: string) => void +} + +export const useProxyConfig = create()( + persist( + (set) => ({ + proxyEnabled: false, + proxyUrl: '', + proxyUsername: '', + proxyPassword: '', + proxyIgnoreSSL: false, + verifyProxySSL: true, + verifyProxyHostSSL: true, + verifyPeerSSL: true, + verifyHostSSL: true, + noProxy: '', + setProxyEnabled: (proxyEnabled) => set({ proxyEnabled }), + setProxyUrl: (proxyUrl) => set({ proxyUrl }), + setProxyUsername: (proxyUsername) => set({ proxyUsername }), + setProxyPassword: (proxyPassword) => set({ proxyPassword }), + setProxyIgnoreSSL: (proxyIgnoreSSL) => set({ proxyIgnoreSSL }), + setVerifyProxySSL: (verifyProxySSL) => set({ verifyProxySSL }), + setVerifyProxyHostSSL: (verifyProxyHostSSL) => + set({ verifyProxyHostSSL }), + setVerifyPeerSSL: (verifyPeerSSL) => set({ verifyPeerSSL }), + setVerifyHostSSL: (verifyHostSSL) => set({ verifyHostSSL }), + setNoProxy: (noProxy) => set({ noProxy }), + }), + { + name: localStoregeKey.settingLocalApiServer, + storage: createJSONStorage(() => localStorage), + } + ) +) diff --git a/web-app/src/routes/settings/extensions.tsx b/web-app/src/routes/settings/extensions.tsx index 1b36deb59..5aa53e954 100644 --- a/web-app/src/routes/settings/extensions.tsx +++ b/web-app/src/routes/settings/extensions.tsx @@ -7,46 +7,15 @@ import HeaderPage from '@/containers/HeaderPage' import SettingsMenu from '@/containers/SettingsMenu' import { t } from 'i18next' import { RenderMarkdown } from '@/containers/RenderMarkdown' +import { ExtensionManager } from '@/lib/extension' // eslint-disable-next-line @typescript-eslint/no-explicit-any export const Route = createFileRoute(route.settings.extensions as any)({ component: Extensions, }) -const mockExtension = [ - { - name: 'Jan Assistant', - version: '1.0.2', - description: - 'Powers the default AI assistant that works with all your installed models.', - }, - { - name: 'Conversational', - version: '1.0.0', - description: - 'Enables conversations and state persistence via your filesystem.', - }, - - { - name: 'Engine Management', - version: '1.0.3', - description: 'Manages AI engines and their configurations.', - }, - - { - name: 'Hardware Management', - version: '1.0.0', - description: 'Manages Better Hardware settings.', - }, - - { - name: 'Model Management', - version: '1.0.36', - description: 'Handles model lists, their details, and settings.', - }, -] - function Extensions() { + const extensions = ExtensionManager.getInstance().listExtensions() return (
@@ -69,13 +38,13 @@ function Extensions() {
} > - {mockExtension.map((item, i) => { + {extensions.map((item, i) => { return ( -

{item.name}

+

{item.productName ?? item.name}

v{item.version}
@@ -83,7 +52,7 @@ function Extensions() { } description={ ( diff --git a/web-app/src/routes/settings/https-proxy.tsx b/web-app/src/routes/settings/https-proxy.tsx index 341e6aba8..43a62bd09 100644 --- a/web-app/src/routes/settings/https-proxy.tsx +++ b/web-app/src/routes/settings/https-proxy.tsx @@ -7,7 +7,9 @@ import { Switch } from '@/components/ui/switch' import { useTranslation } from 'react-i18next' import { Input } from '@/components/ui/input' import { EyeOff, Eye } from 'lucide-react' -import { useState } from 'react' +import { useEffect, useState } from 'react' +import { useProxyConfig } from '@/hooks/useProxyConfig' +import { configurePullOptions } from '@/services/models' // eslint-disable-next-line @typescript-eslint/no-explicit-any export const Route = createFileRoute(route.settings.https_proxy as any)({ @@ -17,6 +19,65 @@ export const Route = createFileRoute(route.settings.https_proxy as any)({ function HTTPSProxy() { const { t } = useTranslation() const [showPassword, setShowPassword] = useState(false) + const { + proxyUrl, + proxyEnabled, + proxyUsername, + proxyPassword, + proxyIgnoreSSL, + verifyProxySSL, + verifyProxyHostSSL, + verifyPeerSSL, + verifyHostSSL, + noProxy, + setProxyEnabled, + setProxyUsername, + setProxyPassword, + setProxyIgnoreSSL, + setVerifyProxySSL, + setVerifyProxyHostSSL, + setVerifyPeerSSL, + setVerifyHostSSL, + setNoProxy, + setProxyUrl, + } = useProxyConfig() + + useEffect(() => { + if (proxyUrl && !proxyEnabled) { + setProxyEnabled(true) + } else if (!proxyUrl && proxyEnabled) { + setProxyEnabled(false) + } + }, [proxyEnabled, proxyUrl, setProxyEnabled]) + + useEffect(() => { + const handler = setTimeout(() => { + configurePullOptions({ + proxyUrl, + proxyEnabled, + proxyUsername, + proxyPassword, + proxyIgnoreSSL, + verifyProxySSL, + verifyProxyHostSSL, + verifyPeerSSL, + verifyHostSSL, + noProxy, + }) + }, 300) + return () => clearTimeout(handler) + }, [ + noProxy, + proxyEnabled, + proxyIgnoreSSL, + proxyPassword, + proxyUrl, + proxyUsername, + verifyHostSSL, + verifyPeerSSL, + verifyProxyHostSSL, + verifyProxySSL, + ]) return (
@@ -38,6 +99,8 @@ function HTTPSProxy() { setProxyUrl(e.target.value)} />
} @@ -49,12 +112,18 @@ function HTTPSProxy() {

Credentials for your proxy server (if required).

- + setProxyUsername(e.target.value)} + />
setProxyPassword(e.target.value)} />
} /> @@ -90,27 +163,54 @@ function HTTPSProxy() { } + actions={ + setProxyIgnoreSSL(checked)} + /> + } /> } + actions={ + setVerifyProxySSL(checked)} + /> + } /> } + actions={ + + setVerifyProxyHostSSL(checked) + } + /> + } /> } + actions={ + setVerifyPeerSSL(checked)} + /> + } /> } + actions={ + setVerifyHostSSL(checked)} + /> + } />
diff --git a/web-app/src/routes/settings/mcp-servers.tsx b/web-app/src/routes/settings/mcp-servers.tsx index d418f2b49..023da2d32 100644 --- a/web-app/src/routes/settings/mcp-servers.tsx +++ b/web-app/src/routes/settings/mcp-servers.tsx @@ -127,6 +127,16 @@ function MCPServers() { } } + const toggleServer = (serverKey: string, active: boolean) => { + if (serverKey) { + // Save single server + editServer(serverKey, { + ...(mcpServers[serverKey] as MCPServerConfig), + active, + }) + } + } + return (
@@ -245,7 +255,12 @@ function MCPServers() { />
- + + toggleServer(key, checked) + } + />
} diff --git a/web-app/src/services/models.ts b/web-app/src/services/models.ts index 575b882d5..296ca79fe 100644 --- a/web-app/src/services/models.ts +++ b/web-app/src/services/models.ts @@ -167,3 +167,54 @@ export const deleteModel = async (id: string) => { throw error } } + +/** + * Configures the proxy options for model downloads. + * @param param0 + */ +export const configurePullOptions = async ({ + proxyEnabled, + proxyUrl, + proxyUsername, + proxyPassword, + proxyIgnoreSSL, + verifyProxySSL, + verifyProxyHostSSL, + verifyPeerSSL, + verifyHostSSL, + noProxy, +}: ProxyOptions) => { + const extension = ExtensionManager.getInstance().get( + ExtensionTypeEnum.Model + ) + + if (!extension) throw new Error('Model extension not found') + try { + await extension.configurePullOptions( + proxyEnabled + ? { + proxy_username: proxyUsername, + proxy_password: proxyPassword, + proxy_url: proxyUrl, + verify_proxy_ssl: proxyIgnoreSSL ? false : verifyProxySSL, + verify_proxy_host_ssl: proxyIgnoreSSL ? false : verifyProxyHostSSL, + verify_peer_ssl: proxyIgnoreSSL ? false : verifyPeerSSL, + verify_host_ssl: proxyIgnoreSSL ? false : verifyHostSSL, + no_proxy: noProxy, + } + : { + proxy_username: '', + proxy_password: '', + proxy_url: '', + verify_proxy_ssl: false, + verify_proxy_host_ssl: false, + verify_peer_ssl: false, + verify_host_ssl: false, + no_proxy: '', + } + ) + } catch (error) { + console.error('Failed to configure pull options:', error) + throw error + } +} diff --git a/web-app/src/types/modelProviders.d.ts b/web-app/src/types/modelProviders.d.ts index 7caf804c2..a0d077953 100644 --- a/web-app/src/types/modelProviders.d.ts +++ b/web-app/src/types/modelProviders.d.ts @@ -51,3 +51,20 @@ type ProviderObject = { * The model provider type */ type ModelProvider = ProviderObject + +/** + * Proxy configuration options + * @description This type defines the structure of the proxy configuration options. + */ +type ProxyOptions = { + proxyEnabled: boolean + proxyUrl: string + proxyUsername: string + proxyPassword: string + proxyIgnoreSSL: boolean + verifyProxySSL: boolean + verifyProxyHostSSL: boolean + verifyPeerSSL: boolean + verifyHostSSL: boolean + noProxy: string +} \ No newline at end of file