chore: update UI mcp settings

This commit is contained in:
Faisal Amir 2025-04-15 20:01:31 +07:00
parent 70be283d0e
commit 6aa2ea8da4
5 changed files with 333 additions and 90 deletions

View File

@ -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",

View 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

View File

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

View 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

View File

@ -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',