chore: update UI mcp settings
This commit is contained in:
parent
70be283d0e
commit
6aa2ea8da4
@ -37,6 +37,7 @@
|
|||||||
"marked": "^9.1.2",
|
"marked": "^9.1.2",
|
||||||
"next": "14.2.3",
|
"next": "14.2.3",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
|
"npx-scope-finder": "^1.3.0",
|
||||||
"openai": "^4.90.0",
|
"openai": "^4.90.0",
|
||||||
"postcss": "8.4.31",
|
"postcss": "8.4.31",
|
||||||
"postcss-url": "10.1.3",
|
"postcss-url": "10.1.3",
|
||||||
|
|||||||
98
web/screens/Settings/MCP/configuration.tsx
Normal file
98
web/screens/Settings/MCP/configuration.tsx
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import React, { useState, useEffect, useCallback } from 'react'
|
||||||
|
|
||||||
|
import { Button, TextArea } from '@janhq/joi'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
|
import { janDataFolderPathAtom } from '@/helpers/atoms/AppConfig.atom'
|
||||||
|
|
||||||
|
const MCPConfiguration = () => {
|
||||||
|
const janDataFolderPath = useAtomValue(janDataFolderPathAtom)
|
||||||
|
const [configContent, setConfigContent] = useState('')
|
||||||
|
const [isSaving, setIsSaving] = useState(false)
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
const [success, setSuccess] = useState('')
|
||||||
|
|
||||||
|
const readConfigFile = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
// Read the file
|
||||||
|
const content = await window.core?.api.getMcpConfigs()
|
||||||
|
setConfigContent(content)
|
||||||
|
|
||||||
|
setError('')
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error reading config file:', err)
|
||||||
|
setError('Failed to read config file')
|
||||||
|
}
|
||||||
|
}, [janDataFolderPath])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (janDataFolderPath) {
|
||||||
|
readConfigFile()
|
||||||
|
}
|
||||||
|
}, [janDataFolderPath, readConfigFile])
|
||||||
|
|
||||||
|
const saveConfigFile = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setIsSaving(true)
|
||||||
|
setSuccess('')
|
||||||
|
setError('')
|
||||||
|
|
||||||
|
// Validate JSON
|
||||||
|
try {
|
||||||
|
JSON.parse(configContent)
|
||||||
|
} catch (err) {
|
||||||
|
setError('Invalid JSON format')
|
||||||
|
setIsSaving(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await window.core?.api?.saveMcpConfigs({ configs: configContent })
|
||||||
|
|
||||||
|
setSuccess('Config saved successfully')
|
||||||
|
setIsSaving(false)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error saving config file:', err)
|
||||||
|
setError('Failed to save config file')
|
||||||
|
setIsSaving(false)
|
||||||
|
}
|
||||||
|
}, [janDataFolderPath, configContent])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{error && (
|
||||||
|
<div className="mb-4 rounded bg-[hsla(var(--destructive-bg))] px-4 py-3 text-[hsla(var(--destructive-fg))]">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{success && (
|
||||||
|
<div className="mb-4 rounded bg-[hsla(var(--success-bg))] px-4 py-3 text-[hsla(var(--success-fg))]">
|
||||||
|
{success}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mb-4 mt-2">
|
||||||
|
<label className="mb-2 block text-sm font-medium">
|
||||||
|
Configuration File (JSON)
|
||||||
|
</label>
|
||||||
|
<TextArea
|
||||||
|
// className="h-80 w-full rounded border border-gray-800 p-2 font-mono text-sm"
|
||||||
|
className="font-mono text-xs"
|
||||||
|
value={configContent}
|
||||||
|
rows={20}
|
||||||
|
onChange={(e) => {
|
||||||
|
setConfigContent(e.target.value)
|
||||||
|
setSuccess('')
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button onClick={saveConfigFile} disabled={isSaving}>
|
||||||
|
{isSaving ? 'Saving...' : 'Save Config'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MCPConfiguration
|
||||||
@ -1,100 +1,37 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react'
|
import React, { useState, useEffect, useCallback } from 'react'
|
||||||
|
|
||||||
import { fs, joinPath } from '@janhq/core'
|
import { ScrollArea, Tabs } from '@janhq/joi'
|
||||||
import { Button } from '@janhq/joi'
|
import MCPSearch from './search'
|
||||||
|
import MCPConfiguration from './configuration'
|
||||||
import { useAtomValue } from 'jotai'
|
import { useAtomValue } from 'jotai'
|
||||||
|
import { showScrollBarAtom } from '@/helpers/atoms/Setting.atom'
|
||||||
import { janDataFolderPathAtom } from '@/helpers/atoms/AppConfig.atom'
|
|
||||||
|
|
||||||
const MCP = () => {
|
const MCP = () => {
|
||||||
const janDataFolderPath = useAtomValue(janDataFolderPathAtom)
|
const [tabValue, setTabValue] = useState('search_mcp')
|
||||||
const [configContent, setConfigContent] = useState('')
|
const showScrollBar = useAtomValue(showScrollBarAtom)
|
||||||
const [isSaving, setIsSaving] = useState(false)
|
|
||||||
const [error, setError] = useState('')
|
|
||||||
const [success, setSuccess] = useState('')
|
|
||||||
|
|
||||||
console.log(janDataFolderPath, 'janDataFolderPath')
|
|
||||||
|
|
||||||
const readConfigFile = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
// Read the file
|
|
||||||
const content = await window.core?.api.getMcpConfigs()
|
|
||||||
setConfigContent(content)
|
|
||||||
|
|
||||||
setError('')
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error reading config file:', err)
|
|
||||||
setError('Failed to read config file')
|
|
||||||
}
|
|
||||||
}, [janDataFolderPath])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (janDataFolderPath) {
|
|
||||||
readConfigFile()
|
|
||||||
}
|
|
||||||
}, [janDataFolderPath, readConfigFile])
|
|
||||||
|
|
||||||
const saveConfigFile = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
setIsSaving(true)
|
|
||||||
setSuccess('')
|
|
||||||
setError('')
|
|
||||||
|
|
||||||
// Validate JSON
|
|
||||||
try {
|
|
||||||
JSON.parse(configContent)
|
|
||||||
} catch (err) {
|
|
||||||
setError('Invalid JSON format')
|
|
||||||
setIsSaving(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await window.core?.api?.saveMcpConfigs({ configs: configContent })
|
|
||||||
|
|
||||||
setSuccess('Config saved successfully')
|
|
||||||
setIsSaving(false)
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error saving config file:', err)
|
|
||||||
setError('Failed to save config file')
|
|
||||||
setIsSaving(false)
|
|
||||||
}
|
|
||||||
}, [janDataFolderPath, configContent])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<ScrollArea
|
||||||
<h2 className="mb-4 text-xl font-bold">MCP Configuration</h2>
|
type={showScrollBar ? 'always' : 'scroll'}
|
||||||
|
className="h-full w-full"
|
||||||
{error && (
|
>
|
||||||
<div className="mb-4 rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700">
|
<div className="block w-full px-4 pb-4">
|
||||||
{error}
|
<div className="sticky top-0 bg-[hsla(var(--app-bg))] py-4">
|
||||||
</div>
|
<h2 className="mb-4 text-lg font-bold">MCP servers</h2>
|
||||||
)}
|
<Tabs
|
||||||
|
options={[
|
||||||
{success && (
|
{ name: 'Search MCP', value: 'search_mcp' },
|
||||||
<div className="mb-4 rounded border border-green-400 bg-green-100 px-4 py-3 text-green-700">
|
{ name: 'Configuration', value: 'config' },
|
||||||
{success}
|
]}
|
||||||
</div>
|
tabStyle="segmented"
|
||||||
)}
|
value={tabValue}
|
||||||
|
onValueChange={(value) => setTabValue(value as string)}
|
||||||
<div className="mb-4">
|
|
||||||
<label className="mb-2 block text-sm font-medium">
|
|
||||||
Configuration File (JSON)
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
className="h-80 w-full rounded border border-gray-800 p-2 font-mono text-sm"
|
|
||||||
value={configContent}
|
|
||||||
onChange={(e) => {
|
|
||||||
setConfigContent(e.target.value)
|
|
||||||
setSuccess('')
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{tabValue === 'search_mcp' && <MCPSearch />}
|
||||||
<div className="flex justify-end">
|
{tabValue === 'config' && <MCPConfiguration />}
|
||||||
<Button onClick={saveConfigFile} disabled={isSaving}>
|
|
||||||
{isSaving ? 'Saving...' : 'Save Config'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
208
web/screens/Settings/MCP/search.tsx
Normal file
208
web/screens/Settings/MCP/search.tsx
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
import React, { useState, useEffect, useCallback } from 'react'
|
||||||
|
|
||||||
|
import { npxFinder, NPMPackage } from 'npx-scope-finder'
|
||||||
|
|
||||||
|
import { Button, Input } from '@janhq/joi'
|
||||||
|
import { PlusIcon } from 'lucide-react'
|
||||||
|
import { toaster } from '@/containers/Toast'
|
||||||
|
|
||||||
|
interface MCPConfig {
|
||||||
|
mcpServers: {
|
||||||
|
[key: string]: {
|
||||||
|
command: string
|
||||||
|
args: string[]
|
||||||
|
env: Record<string, string>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const MCPSearch = () => {
|
||||||
|
const [showToast, setShowToast] = useState(false)
|
||||||
|
const [toastMessage, setToastMessage] = useState('')
|
||||||
|
const [toastType, setToastType] = useState<'success' | 'error'>('success')
|
||||||
|
const [orgName, setOrgName] = useState('@modelcontextprotocol')
|
||||||
|
const [packages, setPackages] = useState<NPMPackage[]>([])
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
|
const searchOrganizationPackages = useCallback(async () => {
|
||||||
|
if (!orgName) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
setError('')
|
||||||
|
|
||||||
|
// Remove @ symbol if present at the beginning
|
||||||
|
// const scopeName = orgName.startsWith('@') ? orgName.substring(1) : orgName
|
||||||
|
|
||||||
|
// Use npxFinder to search for packages from the specified organization
|
||||||
|
const result = await npxFinder(orgName)
|
||||||
|
|
||||||
|
setPackages(result || [])
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error searching for packages:', err)
|
||||||
|
setError('Failed to search for packages. Please try again.')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, [orgName])
|
||||||
|
|
||||||
|
// Search for packages when the component mounts
|
||||||
|
useEffect(() => {
|
||||||
|
searchOrganizationPackages()
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h2 className="mt-2 text-lg font-bold">NPX Package List</h2>
|
||||||
|
<p className="text-[hsla(var(--text-secondary))]">
|
||||||
|
Search and add npm packages as MCP servers
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-6">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
id="orgName"
|
||||||
|
type="text"
|
||||||
|
value={orgName}
|
||||||
|
onChange={(e) => setOrgName(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' && orgName) {
|
||||||
|
e.preventDefault()
|
||||||
|
searchOrganizationPackages()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="input w-full"
|
||||||
|
placeholder="Enter npm scope name (e.g. @janhq)"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={searchOrganizationPackages}
|
||||||
|
disabled={loading || !orgName}
|
||||||
|
>
|
||||||
|
{loading ? 'Searching...' : 'Search'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{error && <p className="mt-2 text-sm text-red-500">{error}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{packages.length > 0 ? (
|
||||||
|
<div className="mt-6">
|
||||||
|
{packages.map((pkg, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="my-2 rounded-xl border border-[hsla(var(--app-border))]"
|
||||||
|
>
|
||||||
|
<div className="flex justify-between border-b border-[hsla(var(--app-border))] px-4 py-3">
|
||||||
|
<span>{pkg.name?.split('/')[1]}</span>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-mono text-sm text-[hsla(var(--text-secondary))]">
|
||||||
|
{pkg.version}
|
||||||
|
</span>
|
||||||
|
<Button theme="icon" onClick={() => handleAddToConfig(pkg)}>
|
||||||
|
<PlusIcon />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="px-4 py-3">
|
||||||
|
<p>{pkg.description || 'No description'}</p>
|
||||||
|
<p className="my-2 whitespace-nowrap text-[hsla(var(--text-secondary))]">
|
||||||
|
Usage: npx {pkg.name}
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
href={`https://www.npmjs.com/package/${pkg.name}`}
|
||||||
|
>{`https://www.npmjs.com/package/${pkg.name}`}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
!loading && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<p>
|
||||||
|
No packages found. Try searching for a different organization.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showToast && (
|
||||||
|
<div
|
||||||
|
className={`fixed bottom-4 right-4 z-50 rounded-md p-4 shadow-lg ${
|
||||||
|
toastType === 'success'
|
||||||
|
? 'bg-green-100 text-green-800'
|
||||||
|
: 'bg-red-100 text-red-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span>{toastMessage}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowToast(false)}
|
||||||
|
className="ml-4 text-gray-500 hover:text-gray-700"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
// Function to add a package to the MCP configuration
|
||||||
|
async function handleAddToConfig(pkg: NPMPackage) {
|
||||||
|
try {
|
||||||
|
// Get current configuration
|
||||||
|
const currentConfig = await window.core?.api.getMcpConfigs()
|
||||||
|
|
||||||
|
// Parse the configuration
|
||||||
|
let config: MCPConfig
|
||||||
|
try {
|
||||||
|
config = JSON.parse(currentConfig || '{"mcpServers": {}}')
|
||||||
|
} catch (err) {
|
||||||
|
// If parsing fails, start with an empty configuration
|
||||||
|
config = { mcpServers: {} }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a unique server name based on the package name
|
||||||
|
const serverName = pkg.name?.split('/')[1] || 'unknown'
|
||||||
|
|
||||||
|
// Check if this server already exists
|
||||||
|
if (config.mcpServers[serverName]) {
|
||||||
|
toaster({
|
||||||
|
title: `Add ${serverName} success`,
|
||||||
|
description: `Server ${serverName} already exists in configuration`,
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the new server configuration
|
||||||
|
config.mcpServers[serverName] = {
|
||||||
|
command: 'npx',
|
||||||
|
args: ['-y', pkg.name || ''],
|
||||||
|
env: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the updated configuration
|
||||||
|
await window.core?.api?.saveMcpConfigs({
|
||||||
|
configs: JSON.stringify(config, null, 2),
|
||||||
|
})
|
||||||
|
|
||||||
|
toaster({
|
||||||
|
title: `Add ${serverName} success`,
|
||||||
|
description: `Added ${serverName} to MCP configuration`,
|
||||||
|
type: 'success',
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
toaster({
|
||||||
|
title: `Add ${pkg.name?.split('/')[1] || 'unknown'} failed`,
|
||||||
|
description: `Failed to add package to configuration`,
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
console.error('Error adding package to configuration:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MCPSearch
|
||||||
@ -81,7 +81,7 @@ const MessageContainer: React.FC<
|
|||||||
'group relative mx-auto px-4',
|
'group relative mx-auto px-4',
|
||||||
!(props.metadata && 'parent_id' in props.metadata) && 'py-2',
|
!(props.metadata && 'parent_id' in props.metadata) && 'py-2',
|
||||||
chatWidth === 'compact' && 'max-w-[700px]',
|
chatWidth === 'compact' && 'max-w-[700px]',
|
||||||
isUser && 'pb-4 pt-0'
|
!isUser && 'pb-4 pt-0'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{!(props.metadata && 'parent_id' in props.metadata) && (
|
{!(props.metadata && 'parent_id' in props.metadata) && (
|
||||||
@ -92,7 +92,6 @@ const MessageContainer: React.FC<
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{!isUser && !isSystem && <LogoMark width={28} />}
|
{!isUser && !isSystem && <LogoMark width={28} />}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'font-extrabold capitalize',
|
'font-extrabold capitalize',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user