chore: allow users to enable/disable MCP servers (#5015)
This commit is contained in:
parent
16514050b9
commit
2bc8fccaf0
@ -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",
|
||||
|
||||
@ -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>",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -8,6 +8,7 @@ export type MCPServerConfig = {
|
||||
command: string
|
||||
args: string[]
|
||||
env: Record<string, string>
|
||||
active?: boolean
|
||||
}
|
||||
|
||||
// Define the structure of all MCP servers
|
||||
|
||||
59
web-app/src/hooks/useProxyConfig.ts
Normal file
59
web-app/src/hooks/useProxyConfig.ts
Normal 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),
|
||||
}
|
||||
)
|
||||
)
|
||||
@ -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 }) => (
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
17
web-app/src/types/modelProviders.d.ts
vendored
17
web-app/src/types/modelProviders.d.ts
vendored
@ -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
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user