jan/web/screens/Settings/MCP/search.tsx
2025-04-15 20:05:19 +07:00

209 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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