feat(MCP): add registryUrl support for package management (#4200)

This commit is contained in:
LiuVaayne 2025-03-31 21:13:20 +08:00 committed by GitHub
parent b43ecb75f5
commit 8c5f61d407
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 158 additions and 38 deletions

View File

@ -21,6 +21,7 @@ class McpService {
baseUrl: server.baseUrl,
command: server.command,
args: server.args,
registryUrl: server.registryUrl,
env: server.env,
id: server.id
})
@ -94,6 +95,23 @@ class McpService {
Logger.info(`[MCP] Starting server with command: ${cmd} ${args ? args.join(' ') : ''}`)
if (server.registryUrl) {
if (cmd.includes('npx') || cmd.includes('bun') || cmd.includes('bunx')) {
server.env = {
...server.env,
NPM_CONFIG_REGISTRY: server.registryUrl
}
} else if (cmd.includes('uvx') || cmd.includes('uv')) {
server.env = {
...server.env,
UV_DEFAULT_INDEX: server.registryUrl,
PIP_INDEX_URL: server.registryUrl
}
}
}
// Logger.info(`[MCP] Environment variables for server:`, server.env)
transport = new StdioClientTransport({
command: cmd,
args,

View File

@ -1045,7 +1045,10 @@
"noToolsAvailable": "No tools available"
},
"deleteServer": "Delete Server",
"deleteServerConfirm": "Are you sure you want to delete this server?"
"deleteServerConfirm": "Are you sure you want to delete this server?",
"registry": "Package Registry",
"registryTooltip": "Choose the registry for package installation to resolve network issues with the default registry.",
"registryDefault": "Default"
},
"messages.divider": "Show divider between messages",
"messages.grid_columns": "Message grid display columns",

View File

@ -1044,7 +1044,10 @@
"noToolsAvailable": "利用可能なツールはありません"
},
"deleteServer": "サーバーを削除",
"deleteServerConfirm": "このサーバーを削除してもよろしいですか?"
"deleteServerConfirm": "このサーバーを削除してもよろしいですか?",
"registry": "パッケージ管理レジストリ",
"registryTooltip": "デフォルトのレジストリでネットワークの問題が発生した場合、パッケージインストールに使用するレジストリを選択してください。",
"registryDefault": "デフォルト"
},
"messages.divider": "メッセージ間に区切り線を表示",
"messages.grid_columns": "メッセージグリッドの表示列数",

View File

@ -1044,7 +1044,10 @@
"noToolsAvailable": "нет доступных инструментов"
},
"deleteServer": "Удалить сервер",
"deleteServerConfirm": "Вы уверены, что хотите удалить этот сервер?"
"deleteServerConfirm": "Вы уверены, что хотите удалить этот сервер?",
"registry": "Реестр пакетов",
"registryTooltip": "Выберите реестр для установки пакетов, если возникают проблемы с сетью при использовании реестра по умолчанию.",
"registryDefault": "По умолчанию"
},
"messages.divider": "Показывать разделитель между сообщениями",
"messages.grid_columns": "Количество столбцов сетки сообщений",

View File

@ -1045,7 +1045,10 @@
"noToolsAvailable": "没有可用工具"
},
"deleteServer": "删除服务器",
"deleteServerConfirm": "确定要删除此服务器吗?"
"deleteServerConfirm": "确定要删除此服务器吗?",
"registry": "包管理源",
"registryTooltip": "选择用于安装包的源,以解决默认源的网络问题。",
"registryDefault": "默认"
},
"messages.divider": "消息分割线",
"messages.grid_columns": "消息网格展示列数",

View File

@ -1044,7 +1044,10 @@
"noToolsAvailable": "沒有可用工具"
},
"deleteServer": "刪除伺服器",
"deleteServerConfirm": "確定要刪除此伺服器嗎?"
"deleteServerConfirm": "確定要刪除此伺服器嗎?",
"registry": "套件管理源",
"registryTooltip": "選擇用於安裝套件的源,以解決預設源的網路問題。",
"registryDefault": "預設"
},
"messages.divider": "訊息間顯示分隔線",
"messages.grid_columns": "訊息網格展示列數",

View File

@ -20,11 +20,26 @@ interface MCPFormValues {
serverType: 'sse' | 'stdio'
baseUrl?: string
command?: string
registryUrl?: string
args?: string
env?: string
isActive: boolean
}
interface Registry {
name: string
url: string
}
const NpmRegistry: Registry[] = [{ name: '淘宝 NPM Mirror', url: 'https://registry.npmmirror.com' }]
const PipRegistry: Registry[] = [
{ name: '清华大学', url: 'https://pypi.tuna.tsinghua.edu.cn/simple' },
{ name: '阿里云', url: 'http://mirrors.aliyun.com/pypi/simple/' },
{ name: '中国科学技术大学', url: 'https://mirrors.ustc.edu.cn/pypi/simple/' },
{ name: '华为云', url: 'https://repo.huaweicloud.com/repository/pypi/simple/' },
{ name: '腾讯云', url: 'https://mirrors.cloud.tencent.com/pypi/simple/' }
]
const McpSettings: React.FC<Props> = ({ server }) => {
const { t } = useTranslation()
const { deleteMCPServer } = useMCPServers()
@ -35,36 +50,42 @@ const McpSettings: React.FC<Props> = ({ server }) => {
const [loadingServer, setLoadingServer] = useState<string | null>(null)
const { updateMCPServer } = useMCPServers()
const [tools, setTools] = useState<MCPTool[]>([])
useEffect(() => {
if (server) {
form.setFieldsValue({
name: server.name,
description: server.description,
serverType: server.baseUrl ? 'sse' : 'stdio',
baseUrl: server.baseUrl || '',
command: server.command || '',
args: server.args ? server.args.join('\n') : '',
env: server.env
? Object.entries(server.env)
.map(([key, value]) => `${key}=${value}`)
.join('\n')
: '',
isActive: server.isActive
})
}
}, [form, server])
const [isShowRegistry, setIsShowRegistry] = useState(false)
const [registry, setRegistry] = useState<Registry[]>()
useEffect(() => {
const serverType = server.baseUrl ? 'sse' : 'stdio'
setServerType(serverType)
// Set registry UI state based on command and registryUrl
if (server.command) {
handleCommandChange(server.command)
// If there's a registryUrl, ensure registry UI is shown
if (server.registryUrl) {
setIsShowRegistry(true)
// Determine registry type based on command
if (server.command.includes('uv') || server.command.includes('uvx')) {
setRegistry(PipRegistry)
} else if (
server.command.includes('npx') ||
server.command.includes('bun') ||
server.command.includes('bunx')
) {
setRegistry(NpmRegistry)
}
}
}
form.setFieldsValue({
name: server.name,
description: server.description,
serverType: serverType,
baseUrl: server.baseUrl || '',
command: server.command || '',
registryUrl: server.registryUrl || '',
isActive: server.isActive,
args: server.args ? server.args.join('\n') : '',
env: server.env
? Object.entries(server.env)
@ -72,7 +93,8 @@ const McpSettings: React.FC<Props> = ({ server }) => {
.join('\n')
: ''
})
}, [form, server])
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [server])
// Watch the serverType field to update the form layout dynamically
useEffect(() => {
@ -110,32 +132,36 @@ const McpSettings: React.FC<Props> = ({ server }) => {
try {
const values = await form.validateFields()
// set basic fields
const mcpServer: MCPServer = {
id: server.id,
name: values.name,
description: values.description,
isActive: values.isActive
isActive: values.isActive,
registryUrl: values.registryUrl
}
// set stdio or sse server
if (values.serverType === 'sse') {
mcpServer.baseUrl = values.baseUrl
} else {
mcpServer.command = values.command
mcpServer.args = values.args ? values.args.split('\n').filter((arg) => arg.trim() !== '') : []
}
// set env variables
if (values.env) {
const env: Record<string, string> = {}
if (values.env) {
values.env.split('\n').forEach((line) => {
if (line.trim()) {
const [key, ...chunks] = line.split('=')
const value = chunks.join('=')
if (key && value) {
env[key.trim()] = value.trim()
}
values.env.split('\n').forEach((line) => {
if (line.trim()) {
const [key, ...chunks] = line.split('=')
const value = chunks.join('=')
if (key && value) {
env[key.trim()] = value.trim()
}
})
}
mcpServer.env = Object.keys(env).length > 0 ? env : undefined
}
})
mcpServer.env = env
}
try {
@ -156,9 +182,41 @@ const McpSettings: React.FC<Props> = ({ server }) => {
}
} catch (error: any) {
setLoading(false)
console.error('Failed to save MCP server settings:', error)
}
}
// Watch for command field changes
const handleCommandChange = (command: string) => {
if (command.includes('uv') || command.includes('uvx')) {
setIsShowRegistry(true)
setRegistry(PipRegistry)
} else if (command.includes('npx') || command.includes('bun') || command.includes('bunx')) {
setIsShowRegistry(true)
setRegistry(NpmRegistry)
} else {
setIsShowRegistry(false)
setRegistry(undefined)
}
}
const onSelectRegistry = (url: string) => {
const command = form.getFieldValue('command') || ''
// Add new registry env variables
if (command.includes('uv') || command.includes('uvx')) {
// envs['PIP_INDEX_URL'] = url
// envs['UV_DEFAULT_INDEX'] = url
form.setFieldsValue({ registryUrl: url })
} else if (command.includes('npx') || command.includes('bun') || command.includes('bunx')) {
// envs['NPM_CONFIG_REGISTRY'] = url
form.setFieldsValue({ registryUrl: url })
}
// Mark form as changed
setIsFormChanged(true)
}
const onDeleteMcpServer = useCallback(
async (server: MCPServer) => {
try {
@ -281,9 +339,37 @@ const McpSettings: React.FC<Props> = ({ server }) => {
name="command"
label={t('settings.mcp.command')}
rules={[{ required: serverType === 'stdio', message: '' }]}>
<Input placeholder="uvx or npx" />
<Input placeholder="uvx or npx" onChange={(e) => handleCommandChange(e.target.value)} />
</Form.Item>
{isShowRegistry && registry && (
<Form.Item
name="registryUrl"
label={t('settings.mcp.registry')}
tooltip={t('settings.mcp.registryTooltip')}>
<Radio.Group>
<Radio
key="no-proxy"
value=""
onChange={(e) => {
onSelectRegistry(e.target.value)
}}>
{t('settings.mcp.registryDefault')}
</Radio>
{registry.map((reg) => (
<Radio
key={reg.url}
value={reg.url}
onChange={(e) => {
onSelectRegistry(e.target.value)
}}>
{reg.name}
</Radio>
))}
</Radio.Group>
</Form.Item>
)}
<Form.Item
name="args"
label={t('settings.mcp.args')}

View File

@ -368,6 +368,7 @@ export interface MCPServer {
description?: string
baseUrl?: string
command?: string
registryUrl?: string
args?: string[]
env?: Record<string, string>
isActive: boolean