diff --git a/web/package.json b/web/package.json
index 7999c74e9..cdf2d8d8b 100644
--- a/web/package.json
+++ b/web/package.json
@@ -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",
diff --git a/web/screens/Settings/MCP/configuration.tsx b/web/screens/Settings/MCP/configuration.tsx
new file mode 100644
index 000000000..b829066d2
--- /dev/null
+++ b/web/screens/Settings/MCP/configuration.tsx
@@ -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 && (
+
+ {error}
+
+ )}
+
+ {success && (
+
+ {success}
+
+ )}
+
+
+
+
+
+
+
+
+ >
+ )
+}
+
+export default MCPConfiguration
diff --git a/web/screens/Settings/MCP/index.tsx b/web/screens/Settings/MCP/index.tsx
index 90cb785f5..b45339220 100644
--- a/web/screens/Settings/MCP/index.tsx
+++ b/web/screens/Settings/MCP/index.tsx
@@ -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 (
-
-
MCP Configuration
-
- {error && (
-
- {error}
+
+
+
+
MCP servers
+ setTabValue(value as string)}
+ />
- )}
-
- {success && (
-
- {success}
-
- )}
-
-
-
-
-
-
-
-
-
+
)
}
diff --git a/web/screens/Settings/MCP/search.tsx b/web/screens/Settings/MCP/search.tsx
new file mode 100644
index 000000000..22aebd284
--- /dev/null
+++ b/web/screens/Settings/MCP/search.tsx
@@ -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
+ }
+ }
+}
+
+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([])
+ 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 (
+ <>
+ NPX Package List
+
+ Search and add npm packages as MCP servers
+
+
+
+
+ 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)"
+ />
+
+
+ {error &&
{error}
}
+
+
+ {packages.length > 0 ? (
+
+ {packages.map((pkg, index) => (
+
+
+
{pkg.name?.split('/')[1]}
+
+
+ {pkg.version}
+
+
+
+
+
+
+ ))}
+
+ ) : (
+ !loading && (
+
+
+ No packages found. Try searching for a different organization.
+
+
+ )
+ )}
+
+ {showToast && (
+
+
+ {toastMessage}
+
+
+
+ )}
+ >
+ )
+
+ // 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
diff --git a/web/screens/Thread/ThreadCenterPanel/TextMessage/index.tsx b/web/screens/Thread/ThreadCenterPanel/TextMessage/index.tsx
index 3af675de5..9d1558ec9 100644
--- a/web/screens/Thread/ThreadCenterPanel/TextMessage/index.tsx
+++ b/web/screens/Thread/ThreadCenterPanel/TextMessage/index.tsx
@@ -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 && }
-