diff --git a/src/main/presenter/llmProviderPresenter/providers/modelscopeProvider.ts b/src/main/presenter/llmProviderPresenter/providers/modelscopeProvider.ts index e6b4df50b..eca4e601f 100644 --- a/src/main/presenter/llmProviderPresenter/providers/modelscopeProvider.ts +++ b/src/main/presenter/llmProviderPresenter/providers/modelscopeProvider.ts @@ -304,35 +304,24 @@ export class ModelscopeProvider extends OpenAICompatibleProvider { ] const randomEmoji = emojis[Math.floor(Math.random() * emojis.length)] - // Get display name: chinese_name first, then id - const displayName = mcpServer.chinese_name || mcpServer.id + // Get display name: chinese_name first, then name, then id + const displayName = mcpServer.chinese_name || mcpServer.name || mcpServer.id return { - name: `@modelscope/${mcpServer.id}`, // Use ModelScope ID format - description: - mcpServer.locales?.zh?.description || - mcpServer.description || - `ModelScope MCP Server: ${displayName}`, command: '', // Not needed for SSE type args: [], // Not needed for SSE type env: {}, - type: 'sse' as const, // SSE type for operational servers - baseUrl: baseUrl, // Use operational URL - enabled: false, // Default to disabled for safety - source: 'modelscope' as const, - // Additional metadata descriptions: mcpServer.locales?.zh?.description || mcpServer.description || `ModelScope MCP Server: ${displayName}`, icons: randomEmoji, // Random emoji instead of URL - provider: 'ModelScope', - providerUrl: `https://www.modelscope.cn/mcp/servers/@${mcpServer.id}`, - logoUrl: '', // No longer using logo URL - tags: mcpServer.tags || [], - // Store original data for reference - originalId: mcpServer.id, - displayName: displayName + autoApprove: ['all'], + disable: false, // Default to disabled for safety + type: 'sse' as const, // SSE type for operational servers + baseUrl: baseUrl, // Use operational URL + source: 'modelscope', + sourceId: mcpServer.id } } } diff --git a/src/main/presenter/mcpPresenter/index.ts b/src/main/presenter/mcpPresenter/index.ts index 2e7981eb8..3d1b7715a 100644 --- a/src/main/presenter/mcpPresenter/index.ts +++ b/src/main/presenter/mcpPresenter/index.ts @@ -12,6 +12,7 @@ import { } from '@shared/presenter' import { ServerManager } from './serverManager' import { ToolManager } from './toolManager' +import { McpRouterManager } from './mcprouterManager' import { eventBus, SendTarget } from '@/eventbus' import { MCP_EVENTS, NOTIFICATION_EVENTS } from '@/events' import { IConfigPresenter } from '@shared/presenter' @@ -83,6 +84,8 @@ export class McpPresenter implements IMCPPresenter { private toolManager: ToolManager private configPresenter: IConfigPresenter private isInitialized: boolean = false + // McpRouter + private mcprouter?: McpRouterManager constructor(configPresenter?: IConfigPresenter) { console.log('Initializing MCP Presenter') @@ -90,6 +93,12 @@ export class McpPresenter implements IMCPPresenter { this.configPresenter = configPresenter || presenter.configPresenter this.serverManager = new ServerManager(this.configPresenter) this.toolManager = new ToolManager(this.configPresenter, this.serverManager) + // init mcprouter manager + try { + this.mcprouter = new McpRouterManager(this.configPresenter) + } catch (e) { + console.warn('[MCP] McpRouterManager init failed:', e) + } // 监听自定义提示词服务器检查事件 eventBus.on(CONFIG_EVENTS.CUSTOM_PROMPTS_SERVER_CHECK_REQUIRED, async () => { @@ -181,6 +190,78 @@ export class McpPresenter implements IMCPPresenter { } } + // =============== McpRouter marketplace APIs =============== + async listMcpRouterServers( + page: number, + limit: number + ): Promise<{ + servers: Array<{ + uuid: string + created_at: string + updated_at: string + name: string + author_name: string + title: string + description: string + content?: string + server_key: string + config_name?: string + server_url?: string + }> + }> { + if (!this.mcprouter) throw new Error('McpRouterManager not available') + const data = await this.mcprouter.listServers(page, limit) + return { servers: data && data.servers ? data.servers : [] } + } + + async installMcpRouterServer(serverKey: string): Promise { + if (!this.mcprouter) throw new Error('McpRouterManager not available') + return this.mcprouter.installServer(serverKey) + } + + async getMcpRouterApiKey(): Promise { + return this.configPresenter.getSetting('mcprouterApiKey') || '' + } + + async setMcpRouterApiKey(key: string): Promise { + this.configPresenter.setSetting('mcprouterApiKey', key) + } + + async isServerInstalled(source: string, sourceId: string): Promise { + const servers = await this.configPresenter.getMcpServers() + for (const config of Object.values(servers)) { + if (config.source === source && config.sourceId === sourceId) { + return true + } + } + return false + } + + async updateMcpRouterServersAuth(apiKey: string): Promise { + const servers = await this.configPresenter.getMcpServers() + const updates: Array<{ name: string; config: Partial }> = [] + + for (const [serverName, config] of Object.entries(servers)) { + if (config.source === 'mcprouter' && config.customHeaders) { + const updatedHeaders = { + ...config.customHeaders, + Authorization: `Bearer ${apiKey}` + } + updates.push({ + name: serverName, + config: { customHeaders: updatedHeaders } + }) + } + } + + // 批量更新所有服务器的 Authorization + for (const update of updates) { + await this.configPresenter.updateMcpServer(update.name, update.config) + } + + console.log(`Updated Authorization for ${updates.length} mcprouter servers`) + } + private scheduleBackgroundRegistryUpdate(): void { setTimeout(async () => { try { diff --git a/src/main/presenter/mcpPresenter/mcprouterManager.ts b/src/main/presenter/mcpPresenter/mcprouterManager.ts new file mode 100644 index 000000000..186682847 --- /dev/null +++ b/src/main/presenter/mcpPresenter/mcprouterManager.ts @@ -0,0 +1,124 @@ +import { IConfigPresenter, MCPServerConfig } from '@shared/presenter' + +type McpRouterListResponse = { + code: number + message: string + data?: { + servers: Array<{ + uuid: string + created_at: string + updated_at: string + name: string + author_name: string + title: string + description: string + content?: string + server_key: string + config_name?: string + server_url?: string + }> + } +} + +type McpRouterGetResponse = { + code: number + message: string + data?: { + created_at: string + updated_at: string + name: string + author_name: string + title: string + description: string + content?: string + server_key: string + config_name: string + server_url: string + } +} + +const LIST_ENDPOINT = 'https://api.mcprouter.to/v1/list-servers' +const GET_ENDPOINT = 'https://api.mcprouter.to/v1/get-server' + +export class McpRouterManager { + constructor(private readonly configPresenter: IConfigPresenter) {} + + private getCommonHeaders(): Record { + return { + 'Content-Type': 'application/json', + 'HTTP-Referer': 'deepchatai.cn', + 'X-Title': 'DeepChat' + } + } + + async listServers(page: number, limit: number): Promise { + const res = await fetch(LIST_ENDPOINT, { + method: 'POST', + headers: this.getCommonHeaders(), + body: JSON.stringify({ page, limit }) + }) + if (!res.ok) throw new Error(`McpRouter list failed: HTTP ${res.status}`) + const json = (await res.json()) as McpRouterListResponse + if (json.code !== 0) throw new Error(json.message || 'List servers error') + return json.data || { servers: [] } + } + + async getServer(serverKey: string): Promise { + const apiKey = this.configPresenter.getSetting('mcprouterApiKey') || '' + if (!apiKey) throw new Error('McpRouter API key missing') + const headers = { + ...this.getCommonHeaders(), + Authorization: `Bearer ${apiKey}` + } + const res = await fetch(GET_ENDPOINT, { + method: 'POST', + headers, + body: JSON.stringify({ server: serverKey }) + }) + if (!res.ok) throw new Error(`McpRouter get failed: HTTP ${res.status}`) + const json = (await res.json()) as McpRouterGetResponse + if (json.code !== 0 || !json.data) throw new Error(json.message || 'Get server error') + return json.data + } + + private pickRandomEmoji(): string { + const emojis = ['🧩', '🛠️', '⚙️', '🚀', '🔧', '🧪', '📦', '🛰️', '🧠', '🔌', '📡', '🗂️'] + const idx = Math.floor(Math.random() * emojis.length) + return emojis[idx] + } + + /** + * Install a server from McpRouter to local MCP config as HTTP (Streamable) server + */ + async installServer(serverKey: string): Promise { + const detail = await this.getServer(serverKey) + if (!detail) throw new Error('Server detail not found') + + const apiKey = this.configPresenter.getSetting('mcprouterApiKey') || '' + if (!apiKey) throw new Error('McpRouter API key missing') + + // Build MCPServerConfig + const config: MCPServerConfig = { + command: '', + args: [], + env: {}, + descriptions: detail.description || detail.title || detail.name, + icons: this.pickRandomEmoji(), + autoApprove: ['all'], + disable: false, + type: 'http', + baseUrl: detail.server_url, + customHeaders: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${apiKey}`, + 'HTTP-Referer': 'deepchatai.cn', + 'X-Title': 'DeepChat' + }, + source: 'mcprouter', + sourceId: serverKey + } + + const serverName = detail.config_name || detail.server_key || detail.name + return await this.configPresenter.addMcpServer(serverName, config) + } +} diff --git a/src/renderer/src/components/mcp-config/mcpServerForm.vue b/src/renderer/src/components/mcp-config/mcpServerForm.vue index c2c32f679..4fdda6211 100644 --- a/src/renderer/src/components/mcp-config/mcpServerForm.vue +++ b/src/renderer/src/components/mcp-config/mcpServerForm.vue @@ -56,6 +56,8 @@ const icons = ref(props.initialConfig?.icons || '📁') const type = ref<'sse' | 'stdio' | 'inmemory' | 'http'>(props.initialConfig?.type || 'stdio') const baseUrl = ref(props.initialConfig?.baseUrl || '') const customHeaders = ref('') +const customHeadersFocused = ref(false) +const customHeadersDisplayValue = ref('') const npmRegistry = ref(props.initialConfig?.customNpmRegistry || '') // 模型选择相关 @@ -551,6 +553,65 @@ watch( { immediate: true } ) +// 遮蔽敏感内容的函数 +const maskSensitiveValue = (value: string): string => { + // 只遮蔽等号后面的值,保留键名 + return value.replace(/=(.+)/g, (_, val) => { + const trimmedVal = val.trim() + if (trimmedVal.length <= 4) { + // 很短的值完全遮蔽 + return '=' + '*'.repeat(trimmedVal.length) + } else if (trimmedVal.length <= 12) { + // 中等长度:显示前1个字符,其余用固定数量星号 + return '=' + trimmedVal.substring(0, 1) + '*'.repeat(6) + } else { + // 长值:显示前2个和后2个字符,中间用固定8个星号 + const start = trimmedVal.substring(0, 2) + const end = trimmedVal.substring(trimmedVal.length - 2) + return '=' + start + '*'.repeat(8) + end + } + }) +} + +// 生成用于显示的 customHeaders 值 +const updateCustomHeadersDisplay = (): void => { + if (customHeadersFocused.value || !customHeaders.value.trim()) { + customHeadersDisplayValue.value = customHeaders.value + } else { + // 遮蔽敏感内容 + const lines = customHeaders.value.split('\n') + const maskedLines = lines.map((line) => { + const trimmedLine = line.trim() + if (!trimmedLine || !trimmedLine.includes('=')) { + return line + } + return maskSensitiveValue(line) + }) + customHeadersDisplayValue.value = maskedLines.join('\n') + } +} + +// 处理 customHeaders 获得焦点 +const handleCustomHeadersFocus = (): void => { + customHeadersFocused.value = true + updateCustomHeadersDisplay() +} + +// 处理 customHeaders 失去焦点 +const handleCustomHeadersBlur = (): void => { + customHeadersFocused.value = false + updateCustomHeadersDisplay() +} + +// 监听 customHeaders 变化以更新显示值 +watch( + customHeaders, + () => { + updateCustomHeadersDisplay() + }, + { immediate: true } +) + // 初始化时解析args中的provider和modelId(针对imageServer) watch( [() => name.value, () => args.value, () => type.value], @@ -655,8 +716,6 @@ const parseKeyValueHeaders = (text: string): Record => { return headers } -// --- 结束新增辅助函数 --- - // 定义 customHeaders 的 placeholder const customHeadersPlaceholder = `Authorization=Bearer your_token HTTP-Referer=deepchatai.cn` @@ -1083,17 +1142,43 @@ HTTP-Referer=deepchatai.cn` -