mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-24 18:50:56 +08:00
feat(McpServersList): add ModelScope sync functionality (#5250)
* feat: add sync servers functionality and UI components for MCP settings * Rename 'Discover Models' to 'Discover MCP Servers' Replace Radio button group with Select dropdown for provider selection in the sync servers popup UI and update related styling Rename 'Discover Models' to 'Discover MCP Servers' * Add error messages for MCP server sync Improve error handling in modelscopeSyncUtils with new localization strings for unauthorized access and no available servers. Update error message handling in all language files and use nanoid for unique server naming.
This commit is contained in:
parent
0fa8ae9560
commit
a58f23f68e
@ -1171,6 +1171,22 @@
|
||||
"sse": "SSE",
|
||||
"streamableHttp": "Streamable HTTP",
|
||||
"stdio": "STDIO"
|
||||
},
|
||||
"sync": {
|
||||
"title": "Sync Servers",
|
||||
"selectProvider": "Select Provider:",
|
||||
"discoverMcpServers": "Discover MCP Servers",
|
||||
"discoverMcpServersDescription": "Visit the platform to discover available MCP servers",
|
||||
"getToken": "Get API Token",
|
||||
"getTokenDescription": "Retrieve your personal API token from your account",
|
||||
"setToken": "Enter Your Token",
|
||||
"tokenRequired": "API Token is required",
|
||||
"tokenPlaceholder": "Enter API token here",
|
||||
"button": "Sync",
|
||||
"error": "Sync MCP Servers error",
|
||||
"success": "Sync MCP Servers successful",
|
||||
"unauthorized": "Sync Unauthorized",
|
||||
"noServersAvailable": "No MCP servers available"
|
||||
}
|
||||
},
|
||||
"messages.divider": "Show divider between messages",
|
||||
|
||||
@ -1169,6 +1169,22 @@
|
||||
"sse": "SSE",
|
||||
"streamableHttp": "ストリーミング",
|
||||
"stdio": "STDIO"
|
||||
},
|
||||
"sync": {
|
||||
"title": "サーバーの同期",
|
||||
"selectProvider": "プロバイダーを選択:",
|
||||
"discoverMcpServers": "MCPサーバーを発見",
|
||||
"discoverMcpServersDescription": "プラットフォームを訪れて利用可能なMCPサーバーを発見",
|
||||
"getToken": "API トークンを取得する",
|
||||
"getTokenDescription": "アカウントから個人用 API トークンを取得します",
|
||||
"setToken": "トークンを入力してください",
|
||||
"tokenRequired": "API トークンは必須です",
|
||||
"tokenPlaceholder": "ここに API トークンを入力してください",
|
||||
"button": "同期する",
|
||||
"error": "MCPサーバーの同期エラー",
|
||||
"success": "MCPサーバーの同期成功",
|
||||
"unauthorized": "同期が許可されていません",
|
||||
"noServersAvailable": "利用可能な MCP サーバーがありません"
|
||||
}
|
||||
},
|
||||
"messages.divider": "メッセージ間に区切り線を表示",
|
||||
|
||||
@ -1169,6 +1169,22 @@
|
||||
"sse": "SSE",
|
||||
"streamableHttp": "Потоковый HTTP",
|
||||
"stdio": "STDIO"
|
||||
},
|
||||
"sync": {
|
||||
"title": "Синхронизация серверов",
|
||||
"selectProvider": "Выберите провайдера:",
|
||||
"discoverMcpServers": "Обнаружить серверы MCP",
|
||||
"discoverMcpServersDescription": "Посетите платформу, чтобы обнаружить доступные серверы MCP",
|
||||
"getToken": "Получить API токен",
|
||||
"getTokenDescription": "Получите персональный API токен из вашей учетной записи",
|
||||
"setToken": "Введите ваш токен",
|
||||
"tokenRequired": "Требуется API токен",
|
||||
"tokenPlaceholder": "Введите API токен здесь",
|
||||
"button": "Синхронизировать",
|
||||
"error": "Ошибка синхронизации серверов MCP",
|
||||
"success": "Синхронизация серверов MCP успешна",
|
||||
"unauthorized": "Синхронизация не разрешена",
|
||||
"noServersAvailable": "Нет доступных серверов MCP"
|
||||
}
|
||||
},
|
||||
"messages.divider": "Показывать разделитель между сообщениями",
|
||||
|
||||
@ -1171,6 +1171,22 @@
|
||||
"sse": "SSE",
|
||||
"streamableHttp": "流式",
|
||||
"stdio": "STDIO"
|
||||
},
|
||||
"sync": {
|
||||
"title": "同步服务器",
|
||||
"selectProvider": "选择提供商:",
|
||||
"discoverMcpServers": "发现MCP服务器",
|
||||
"discoverMcpServersDescription": "访问平台以发现可用的MCP服务器",
|
||||
"getToken": "获取 API 令牌",
|
||||
"getTokenDescription": "从您的帐户中获取个人 API 令牌",
|
||||
"setToken": "输入您的令牌",
|
||||
"tokenRequired": "需要 API 令牌",
|
||||
"tokenPlaceholder": "在此输入 API 令牌",
|
||||
"button": "同步",
|
||||
"error": "同步MCP服务器出错",
|
||||
"success": "同步MCP服务器成功",
|
||||
"unauthorized": "同步未授权",
|
||||
"noServersAvailable": "无可用的 MCP 服务器"
|
||||
}
|
||||
},
|
||||
"messages.divider": "消息分割线",
|
||||
|
||||
@ -1170,6 +1170,22 @@
|
||||
"sse": "SSE",
|
||||
"streamableHttp": "流式",
|
||||
"stdio": "STDIO"
|
||||
},
|
||||
"sync": {
|
||||
"title": "同步伺服器",
|
||||
"selectProvider": "選擇提供者:",
|
||||
"discoverMcpServers": "發現MCP伺服器",
|
||||
"discoverMcpServersDescription": "訪問平台以發現可用的MCP伺服器",
|
||||
"getToken": "獲取 API 令牌",
|
||||
"getTokenDescription": "從您的帳戶獲取個人 API 令牌",
|
||||
"setToken": "輸入您的令牌",
|
||||
"tokenRequired": "需要 API 令牌",
|
||||
"tokenPlaceholder": "在此輸入 API 令牌",
|
||||
"button": "同步",
|
||||
"error": "同步MCP伺服器出錯",
|
||||
"success": "同步MCP伺服器成功",
|
||||
"unauthorized": "同步未授權",
|
||||
"noServersAvailable": "無可用的 MCP 伺服器"
|
||||
}
|
||||
},
|
||||
"messages.divider": "訊息間顯示分隔線",
|
||||
|
||||
@ -5,7 +5,7 @@ import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
||||
import { MCPServer } from '@renderer/types'
|
||||
import { Button, Empty, Tag } from 'antd'
|
||||
import { MonitorCheck, Plus, Settings2 } from 'lucide-react'
|
||||
import { MonitorCheck, Plus, RefreshCw, Settings2 } from 'lucide-react'
|
||||
import { FC, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router'
|
||||
@ -13,6 +13,8 @@ import styled from 'styled-components'
|
||||
|
||||
import { SettingTitle } from '..'
|
||||
import EditMcpJsonPopup from './EditMcpJsonPopup'
|
||||
import SyncServersPopup from './SyncServersPopup'
|
||||
|
||||
const McpServersList: FC = () => {
|
||||
const { mcpServers, addMCPServer, updateMcpServers } = useMCPServers()
|
||||
const { t } = useTranslation()
|
||||
@ -34,6 +36,10 @@ const McpServersList: FC = () => {
|
||||
window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-list' })
|
||||
}, [addMCPServer, navigate, t])
|
||||
|
||||
const onSyncServers = useCallback(() => {
|
||||
SyncServersPopup.show(mcpServers)
|
||||
}, [mcpServers])
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<ListHeader>
|
||||
@ -41,9 +47,14 @@ const McpServersList: FC = () => {
|
||||
<span>{t('settings.mcp.newServer')}</span>
|
||||
<Button icon={<EditOutlined />} type="text" onClick={() => EditMcpJsonPopup.show()} shape="circle" />
|
||||
</SettingTitle>
|
||||
<Button icon={<Plus size={16} />} type="default" onClick={onAddMcpServer} shape="round">
|
||||
{t('settings.mcp.addServer')}
|
||||
</Button>
|
||||
<ButtonGroup>
|
||||
<Button icon={<Plus size={16} />} type="default" onClick={onAddMcpServer} shape="round">
|
||||
{t('settings.mcp.addServer')}
|
||||
</Button>
|
||||
<Button icon={<RefreshCw size={16} />} type="default" onClick={onSyncServers} shape="round">
|
||||
{t('settings.mcp.sync.title', 'Sync Servers')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</ListHeader>
|
||||
<DragableList style={{ width: '100%' }} list={mcpServers} onUpdate={updateMcpServers}>
|
||||
{(server: MCPServer) => (
|
||||
@ -176,4 +187,9 @@ const ServerFooter = styled.div`
|
||||
margin-top: 10px;
|
||||
`
|
||||
|
||||
const ButtonGroup = styled.div`
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
export default McpServersList
|
||||
|
||||
348
src/renderer/src/pages/settings/MCPSettings/SyncServersPopup.tsx
Normal file
348
src/renderer/src/pages/settings/MCPSettings/SyncServersPopup.tsx
Normal file
@ -0,0 +1,348 @@
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
||||
import { MCPServer } from '@renderer/types'
|
||||
import { Button, Form, Input, Modal, Select } from 'antd'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { getModelScopeToken, saveModelScopeToken, syncModelScopeServers } from './modelscopeSyncUtils'
|
||||
|
||||
// Provider configuration interface
|
||||
interface ProviderConfig {
|
||||
key: string
|
||||
name: string
|
||||
description: string
|
||||
discoverUrl: string
|
||||
apiKeyUrl: string
|
||||
tokenFieldName: string
|
||||
getToken: () => string | null
|
||||
saveToken: (token: string) => void
|
||||
syncServers: (token: string, existingServers: MCPServer[]) => Promise<any>
|
||||
}
|
||||
|
||||
// Provider configurations
|
||||
const providers: ProviderConfig[] = [
|
||||
{
|
||||
key: 'modelscope',
|
||||
name: 'ModelScope',
|
||||
description: 'ModelScope 平台 MCP 服务',
|
||||
discoverUrl: 'https://www.modelscope.cn/mcp?hosted=1&page=1',
|
||||
apiKeyUrl: 'https://www.modelscope.cn/my/myaccesstoken',
|
||||
tokenFieldName: 'modelScopeToken',
|
||||
getToken: getModelScopeToken,
|
||||
saveToken: saveModelScopeToken,
|
||||
syncServers: syncModelScopeServers
|
||||
}
|
||||
]
|
||||
|
||||
interface Props {
|
||||
resolve: (data: any) => void
|
||||
existingServers: MCPServer[]
|
||||
}
|
||||
|
||||
const PopupContainer: React.FC<Props> = ({ resolve, existingServers }) => {
|
||||
const { addMCPServer } = useMCPServers()
|
||||
const [open, setOpen] = useState(true)
|
||||
const [isSyncing, setIsSyncing] = useState(false)
|
||||
const [selectedProviderKey, setSelectedProviderKey] = useState(providers[0].key)
|
||||
const [tokens, setTokens] = useState<Record<string, string>>({})
|
||||
const { t } = useTranslation()
|
||||
const [form] = Form.useForm()
|
||||
|
||||
// Get the currently selected provider config
|
||||
const selectedProvider = providers.find((p) => p.key === selectedProviderKey) || providers[0]
|
||||
|
||||
useEffect(() => {
|
||||
// Initialize tokens for all providers
|
||||
const initialTokens: Record<string, string> = {}
|
||||
|
||||
providers.forEach((provider) => {
|
||||
const token = provider.getToken()
|
||||
if (token) {
|
||||
initialTokens[provider.tokenFieldName] = token
|
||||
form.setFieldsValue({ [provider.tokenFieldName]: token })
|
||||
}
|
||||
})
|
||||
|
||||
setTokens(initialTokens)
|
||||
}, [form])
|
||||
|
||||
const handleSync = useCallback(async () => {
|
||||
try {
|
||||
await form.validateFields()
|
||||
} catch (error) {
|
||||
return
|
||||
}
|
||||
|
||||
setIsSyncing(true)
|
||||
|
||||
try {
|
||||
const token = form.getFieldValue(selectedProvider.tokenFieldName)
|
||||
|
||||
// Save token if present
|
||||
if (token) {
|
||||
selectedProvider.saveToken(token)
|
||||
setTokens((prev) => ({ ...prev, [selectedProvider.tokenFieldName]: token }))
|
||||
}
|
||||
|
||||
// Sync servers
|
||||
const result = await selectedProvider.syncServers(token, existingServers)
|
||||
|
||||
if (result.success && result.addedServers?.length > 0) {
|
||||
// Add the new servers to the store
|
||||
for (const server of result.addedServers) {
|
||||
addMCPServer(server)
|
||||
}
|
||||
window.message.success(result.message)
|
||||
setOpen(false)
|
||||
} else {
|
||||
// Show message but keep dialog open
|
||||
if (result.success) {
|
||||
window.message.info(result.message)
|
||||
} else {
|
||||
window.message.error(result.message)
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
window.message.error(`${t('settings.mcp.sync.error')}: ${error.message}`)
|
||||
} finally {
|
||||
setIsSyncing(false)
|
||||
}
|
||||
}, [addMCPServer, existingServers, form, selectedProvider, t])
|
||||
|
||||
const onCancel = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
resolve({})
|
||||
}
|
||||
|
||||
SyncServersPopup.hide = onCancel
|
||||
|
||||
// Check if sync button should be disabled
|
||||
const isSyncDisabled = () => {
|
||||
const token = tokens[selectedProvider.tokenFieldName]
|
||||
return !token
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('settings.mcp.sync.title', 'Sync Servers')}
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
afterClose={onClose}
|
||||
width={550}
|
||||
footer={null}
|
||||
transitionName="animation-move-down"
|
||||
centered>
|
||||
<ContentContainer>
|
||||
{/* Only show provider selector if there are multiple providers */}
|
||||
|
||||
<ProviderSelector>
|
||||
<SelectorLabel>{t('settings.mcp.sync.selectProvider', 'Select Provider:')}</SelectorLabel>
|
||||
<Select
|
||||
value={selectedProviderKey}
|
||||
onChange={setSelectedProviderKey}
|
||||
style={{ width: 200 }}
|
||||
options={providers.map((provider) => ({
|
||||
value: provider.key,
|
||||
label: provider.name
|
||||
}))}
|
||||
/>
|
||||
</ProviderSelector>
|
||||
|
||||
<ProviderContent>
|
||||
<Form form={form} layout="vertical" style={{ width: '100%' }}>
|
||||
<StepSection>
|
||||
<StepNumber>1</StepNumber>
|
||||
<StepContent>
|
||||
<StepTitle>{t('settings.mcp.sync.discoverMcpServers', 'Discover MCP Servers')}</StepTitle>
|
||||
<StepDescription>
|
||||
{t(
|
||||
'settings.mcp.sync.discoverMcpServersDescription',
|
||||
'Visit the platform to discover available MCP servers'
|
||||
)}
|
||||
</StepDescription>
|
||||
<LinkContainer>
|
||||
<ExternalLink href={selectedProvider.discoverUrl} target="_blank">
|
||||
<LinkIcon>🌐</LinkIcon>
|
||||
<span>{t('settings.mcp.sync.discoverMcpServers', 'Discover MCP Servers')}</span>
|
||||
</ExternalLink>
|
||||
</LinkContainer>
|
||||
</StepContent>
|
||||
</StepSection>
|
||||
|
||||
<StepSection>
|
||||
<StepNumber>2</StepNumber>
|
||||
<StepContent>
|
||||
<StepTitle>{t('settings.mcp.sync.getToken', 'Get API Token')}</StepTitle>
|
||||
<StepDescription>
|
||||
{t('settings.mcp.sync.getTokenDescription', 'Retrieve your personal API token from your account')}
|
||||
</StepDescription>
|
||||
<LinkContainer>
|
||||
<ExternalLink href={selectedProvider.apiKeyUrl} target="_blank">
|
||||
<LinkIcon>🔑</LinkIcon>
|
||||
<span>{t('settings.mcp.sync.getToken', 'Get API Token')}</span>
|
||||
</ExternalLink>
|
||||
</LinkContainer>
|
||||
</StepContent>
|
||||
</StepSection>
|
||||
|
||||
<StepSection>
|
||||
<StepNumber>3</StepNumber>
|
||||
<StepContent>
|
||||
<StepTitle>{t('settings.mcp.sync.setToken', 'Enter Your Token')}</StepTitle>
|
||||
<Form.Item
|
||||
name={selectedProvider.tokenFieldName}
|
||||
rules={[{ required: true, message: t('settings.mcp.sync.tokenRequired', 'API Token is required') }]}>
|
||||
<Input.Password
|
||||
placeholder={t('settings.mcp.sync.tokenPlaceholder', 'Enter API token here')}
|
||||
onChange={(e) => {
|
||||
setTokens((prev) => ({ ...prev, [selectedProvider.tokenFieldName]: e.target.value }))
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</StepContent>
|
||||
</StepSection>
|
||||
</Form>
|
||||
</ProviderContent>
|
||||
|
||||
<ButtonContainer>
|
||||
<Button type="default" onClick={onCancel}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button type="primary" onClick={handleSync} loading={isSyncing} disabled={isSyncDisabled()}>
|
||||
{t('settings.mcp.sync.button', 'Sync')}
|
||||
</Button>
|
||||
</ButtonContainer>
|
||||
</ContentContainer>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const ContentContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
`
|
||||
|
||||
const ProviderSelector = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 15px;
|
||||
`
|
||||
|
||||
const SelectorLabel = styled.div`
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
`
|
||||
|
||||
const ProviderContent = styled.div`
|
||||
border-top: 1px solid var(--color-border);
|
||||
padding-top: 20px;
|
||||
|
||||
&.no-border {
|
||||
border-top: none;
|
||||
padding-top: 0;
|
||||
}
|
||||
`
|
||||
|
||||
const StepSection = styled.div`
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
`
|
||||
|
||||
const StepNumber = styled.div`
|
||||
background-color: var(--color-primary);
|
||||
color: white;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
`
|
||||
|
||||
const StepContent = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
`
|
||||
|
||||
const StepTitle = styled.div`
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
margin-bottom: 4px;
|
||||
`
|
||||
|
||||
const StepDescription = styled.div`
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 13px;
|
||||
margin-bottom: 8px;
|
||||
`
|
||||
|
||||
const LinkContainer = styled.div`
|
||||
margin-top: 4px;
|
||||
`
|
||||
|
||||
const ExternalLink = styled.a`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
background-color: var(--color-background-2);
|
||||
font-size: 14px;
|
||||
color: var(--color-primary);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-background-3);
|
||||
text-decoration: none;
|
||||
}
|
||||
`
|
||||
|
||||
const LinkIcon = styled.span`
|
||||
font-size: 16px;
|
||||
`
|
||||
|
||||
const ButtonContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
`
|
||||
|
||||
const TopViewKey = 'SyncServersPopup'
|
||||
|
||||
export default class SyncServersPopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
TopView.hide(TopViewKey)
|
||||
}
|
||||
static show(existingServers: MCPServer[]) {
|
||||
return new Promise<any>((resolve) => {
|
||||
TopView.show(
|
||||
<PopupContainer
|
||||
existingServers={existingServers}
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
TopView.hide(TopViewKey)
|
||||
}}
|
||||
/>,
|
||||
TopViewKey
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,129 @@
|
||||
import { nanoid } from '@reduxjs/toolkit'
|
||||
import { MCPServer } from '@renderer/types'
|
||||
import i18next from 'i18next'
|
||||
|
||||
// Token storage constants and utilities
|
||||
const TOKEN_STORAGE_KEY = 'modelscope_token'
|
||||
|
||||
export const saveModelScopeToken = (token: string): void => {
|
||||
localStorage.setItem(TOKEN_STORAGE_KEY, token)
|
||||
}
|
||||
|
||||
export const getModelScopeToken = (): string | null => {
|
||||
return localStorage.getItem(TOKEN_STORAGE_KEY)
|
||||
}
|
||||
|
||||
export const clearModelScopeToken = (): void => {
|
||||
localStorage.removeItem(TOKEN_STORAGE_KEY)
|
||||
}
|
||||
|
||||
export const hasModelScopeToken = (): boolean => {
|
||||
return !!getModelScopeToken()
|
||||
}
|
||||
|
||||
interface ModelScopeServer {
|
||||
id: string
|
||||
name: string
|
||||
chinese_name?: string
|
||||
description?: string
|
||||
operational_urls?: { url: string }[]
|
||||
}
|
||||
|
||||
interface ModelScopeSyncResult {
|
||||
success: boolean
|
||||
message: string
|
||||
addedServers: MCPServer[]
|
||||
errorDetails?: string
|
||||
}
|
||||
|
||||
// Function to fetch and process ModelScope servers
|
||||
export const syncModelScopeServers = async (
|
||||
token: string,
|
||||
existingServers: MCPServer[]
|
||||
): Promise<ModelScopeSyncResult> => {
|
||||
const t = i18next.t
|
||||
|
||||
try {
|
||||
const response = await fetch('https://www.modelscope.cn/api/v1/mcp/services/operational', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
// Handle authentication errors
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
clearModelScopeToken()
|
||||
return {
|
||||
success: false,
|
||||
message: t('settings.mcp.sync.unauthorized', 'Sync Unauthorized'),
|
||||
addedServers: []
|
||||
}
|
||||
}
|
||||
|
||||
// Handle server errors
|
||||
if (response.status === 500 || !response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
message: t('settings.mcp.sync.error'),
|
||||
addedServers: [],
|
||||
errorDetails: `Status: ${response.status}`
|
||||
}
|
||||
}
|
||||
|
||||
// Process successful response
|
||||
const data = await response.json()
|
||||
const servers: ModelScopeServer[] = data.Data?.Result || []
|
||||
|
||||
if (servers.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
message: t('settings.mcp.sync.noServersAvailable', 'No MCP servers available'),
|
||||
addedServers: []
|
||||
}
|
||||
}
|
||||
|
||||
// Transform ModelScope servers to MCP servers format
|
||||
const addedServers: MCPServer[] = []
|
||||
|
||||
for (const server of servers) {
|
||||
try {
|
||||
if (!server.operational_urls?.[0]?.url) continue
|
||||
|
||||
// Skip if server already exists
|
||||
if (existingServers.some((s) => s.id === `@modelscope/${server.id}`)) continue
|
||||
|
||||
const mcpServer: MCPServer = {
|
||||
id: `@modelscope/${server.id}`,
|
||||
name: server.chinese_name || server.name || `ModelScope Server ${nanoid()}`,
|
||||
description: server.description || '',
|
||||
type: 'sse',
|
||||
baseUrl: server.operational_urls[0].url,
|
||||
command: '',
|
||||
args: [],
|
||||
env: {},
|
||||
isActive: true
|
||||
}
|
||||
|
||||
addedServers.push(mcpServer)
|
||||
} catch (err) {
|
||||
console.error('Error processing ModelScope server:', err)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: t('settings.mcp.sync.success', { count: addedServers.length }),
|
||||
addedServers
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('ModelScope sync error:', error)
|
||||
return {
|
||||
success: false,
|
||||
message: t('settings.mcp.sync.error'),
|
||||
addedServers: [],
|
||||
errorDetails: String(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user