chore: update UI mcp settings
This commit is contained in:
parent
70be283d0e
commit
6aa2ea8da4
@ -37,6 +37,7 @@
|
||||
"marked": "^9.1.2",
|
||||
"next": "14.2.3",
|
||||
"next-themes": "^0.2.1",
|
||||
"npx-scope-finder": "^1.3.0",
|
||||
"openai": "^4.90.0",
|
||||
"postcss": "8.4.31",
|
||||
"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 { fs, joinPath } from '@janhq/core'
|
||||
import { Button } from '@janhq/joi'
|
||||
import { ScrollArea, Tabs } from '@janhq/joi'
|
||||
import MCPSearch from './search'
|
||||
import MCPConfiguration from './configuration'
|
||||
import { useAtomValue } from 'jotai'
|
||||
|
||||
import { janDataFolderPathAtom } from '@/helpers/atoms/AppConfig.atom'
|
||||
import { showScrollBarAtom } from '@/helpers/atoms/Setting.atom'
|
||||
|
||||
const MCP = () => {
|
||||
const janDataFolderPath = useAtomValue(janDataFolderPathAtom)
|
||||
const [configContent, setConfigContent] = useState('')
|
||||
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])
|
||||
const [tabValue, setTabValue] = useState('search_mcp')
|
||||
const showScrollBar = useAtomValue(showScrollBarAtom)
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<h2 className="mb-4 text-xl font-bold">MCP Configuration</h2>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700">
|
||||
{error}
|
||||
<ScrollArea
|
||||
type={showScrollBar ? 'always' : 'scroll'}
|
||||
className="h-full w-full"
|
||||
>
|
||||
<div className="block w-full px-4 pb-4">
|
||||
<div className="sticky top-0 bg-[hsla(var(--app-bg))] py-4">
|
||||
<h2 className="mb-4 text-lg font-bold">MCP servers</h2>
|
||||
<Tabs
|
||||
options={[
|
||||
{ name: 'Search MCP', value: 'search_mcp' },
|
||||
{ name: 'Configuration', value: 'config' },
|
||||
]}
|
||||
tabStyle="segmented"
|
||||
value={tabValue}
|
||||
onValueChange={(value) => setTabValue(value as string)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<div className="mb-4 rounded border border-green-400 bg-green-100 px-4 py-3 text-green-700">
|
||||
{success}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<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('')
|
||||
}}
|
||||
/>
|
||||
{tabValue === 'search_mcp' && <MCPSearch />}
|
||||
{tabValue === 'config' && <MCPConfiguration />}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={saveConfigFile} disabled={isSaving}>
|
||||
{isSaving ? 'Saving...' : 'Save Config'}
|
||||
</Button>
|
||||
</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',
|
||||
!(props.metadata && 'parent_id' in props.metadata) && 'py-2',
|
||||
chatWidth === 'compact' && 'max-w-[700px]',
|
||||
isUser && 'pb-4 pt-0'
|
||||
!isUser && 'pb-4 pt-0'
|
||||
)}
|
||||
>
|
||||
{!(props.metadata && 'parent_id' in props.metadata) && (
|
||||
@ -92,7 +92,6 @@ const MessageContainer: React.FC<
|
||||
)}
|
||||
>
|
||||
{!isUser && !isSystem && <LogoMark width={28} />}
|
||||
|
||||
<div
|
||||
className={twMerge(
|
||||
'font-extrabold capitalize',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user