chore: allow users to enable/disable MCP servers (#5015)

This commit is contained in:
Louis 2025-05-19 13:15:37 +07:00 committed by GitHub
parent 16514050b9
commit 2bc8fccaf0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 273 additions and 49 deletions

View File

@ -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 <service@jan.ai>",
"license": "MIT",

View File

@ -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 <service@jan.ai>",

View File

@ -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 <service@jan.ai>",
"license": "AGPL-3.0",

View File

@ -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<bool> {
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());

View File

@ -8,6 +8,7 @@ export type MCPServerConfig = {
command: string
args: string[]
env: Record<string, string>
active?: boolean
}
// Define the structure of all MCP servers

View File

@ -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<ProxyConfigState>()(
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),
}
)
)

View File

@ -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 (
<div className="flex flex-col h-full">
<HeaderPage>
@ -69,13 +38,13 @@ function Extensions() {
</div>
}
>
{mockExtension.map((item, i) => {
{extensions.map((item, i) => {
return (
<CardItem
key={i}
title={
<div className="flex items-center gap-x-2">
<h1 className="text-main-view-fg">{item.name}</h1>
<h1 className="text-main-view-fg">{item.productName ?? item.name}</h1>
<div className="bg-main-view-fg/10 px-1 py-0.5 rounded text-main-view-fg/70 text-xs">
v{item.version}
</div>
@ -83,7 +52,7 @@ function Extensions() {
}
description={
<RenderMarkdown
content={item.description}
content={item.description ?? ""}
components={{
// Make links open in a new tab
a: ({ ...props }) => (

View File

@ -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 (
<div className="flex flex-col h-full">
@ -38,6 +99,8 @@ function HTTPSProxy() {
<Input
className="w-full"
placeholder="http://<user>:<password>@<domain or IP>:<port>"
value={proxyUrl}
onChange={(e) => setProxyUrl(e.target.value)}
/>
</div>
}
@ -49,12 +112,18 @@ function HTTPSProxy() {
<div className="space-y-2">
<p>Credentials for your proxy server (if required).</p>
<div className="flex gap-2">
<Input placeholder="Username" />
<Input
placeholder="Username"
value={proxyUsername}
onChange={(e) => setProxyUsername(e.target.value)}
/>
<div className="relative shrink-0 w-1/2">
<Input
type={showPassword ? 'text' : 'password'}
placeholder="Password"
className="pr-16"
value={proxyPassword}
onChange={(e) => setProxyPassword(e.target.value)}
/>
<div className="absolute right-2 top-1/2 transform -translate-y-1/2 flex items-center gap-1">
<button
@ -79,7 +148,11 @@ function HTTPSProxy() {
description={
<div className="space-y-2">
<p>List of hosts that should bypass the proxy.</p>
<Input placeholder="localhost, 127.0.0.1" />
<Input
placeholder="localhost, 127.0.0.1"
value={noProxy}
onChange={(e) => setNoProxy(e.target.value)}
/>
</div>
}
/>
@ -90,27 +163,54 @@ function HTTPSProxy() {
<CardItem
title="Ignore SSL Certificates"
description="Allow self-signed or unverified certificates (may be required for certain proxies). Enable this reduces security. Only use this if you trust your proxy server."
actions={<Switch />}
actions={
<Switch
checked={proxyIgnoreSSL}
onCheckedChange={(checked) => setProxyIgnoreSSL(checked)}
/>
}
/>
<CardItem
title="Proxy SSL"
description="Validate SSL certificate when connecting to the proxy server."
actions={<Switch />}
actions={
<Switch
checked={verifyProxySSL}
onCheckedChange={(checked) => setVerifyProxySSL(checked)}
/>
}
/>
<CardItem
title="Proxy Host SSL"
description="Validate SSL certificate of the proxy server host."
actions={<Switch />}
actions={
<Switch
checked={verifyProxyHostSSL}
onCheckedChange={(checked) =>
setVerifyProxyHostSSL(checked)
}
/>
}
/>
<CardItem
title="Peer SSL"
description="Validate SSL certificate of the peer connections."
actions={<Switch />}
actions={
<Switch
checked={verifyPeerSSL}
onCheckedChange={(checked) => setVerifyPeerSSL(checked)}
/>
}
/>
<CardItem
title="Host SSL"
description="Validate SSL certificate of destination hosts."
actions={<Switch />}
actions={
<Switch
checked={verifyHostSSL}
onCheckedChange={(checked) => setVerifyHostSSL(checked)}
/>
}
/>
</Card>
</div>

View File

@ -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 (
<div className="flex flex-col h-full">
<HeaderPage>
@ -245,7 +255,12 @@ function MCPServers() {
/>
</div>
<div className="ml-2">
<Switch />
<Switch
checked={config.active === false ? false : true}
onCheckedChange={(checked) =>
toggleServer(key, checked)
}
/>
</div>
</div>
}

View File

@ -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<ModelExtension>(
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
}
}

View File

@ -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
}