From 44b2b859dadc3c07d56dbdc83e7848bb184d0e7c Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Fri, 7 Nov 2025 18:41:15 +0800 Subject: [PATCH] feat(MCPRouter): add MCPRouter provider support and integration - Introduced MCPRouter provider with token management and server synchronization functionalities. - Added MCPRouter logo to settings page for visual representation. - Updated provider configuration to include MCPRouter details and API interactions. - Implemented functions for saving, retrieving, and clearing MCPRouter tokens, along with server synchronization logic. --- .../assets/images/providers/mcprouter.webp | Bin 0 -> 1628 bytes .../src/pages/settings/MCPSettings/index.tsx | 4 +- .../settings/MCPSettings/providers/config.ts | 12 ++ .../MCPSettings/providers/mcprouter.ts | 157 ++++++++++++++++++ 4 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 src/renderer/src/assets/images/providers/mcprouter.webp create mode 100644 src/renderer/src/pages/settings/MCPSettings/providers/mcprouter.ts diff --git a/src/renderer/src/assets/images/providers/mcprouter.webp b/src/renderer/src/assets/images/providers/mcprouter.webp new file mode 100644 index 0000000000000000000000000000000000000000..7d6557fafc58a4b4919e6be065b9006bfcd339e9 GIT binary patch literal 1628 zcmV-i2BY~>Nk&Fg1^@t8MM6+kP&gn+1^@uiDgd1UD#!rH06vjOpGu{qqamZTI#{q0 z32AQNEzmNMy!+Xo`F*&i0rUshzVP4Y-{yDRKnHsS&eeP}|JhGH-%a}g{vh0$yvX{f;Qax zo5FxN&XVwEuFP*|!r0DWvNX$!fzkAoA0u_7w~Sr!$wEi$WAY1}dP3BdY?|=7j=FBF z>Sx(CF8fI5uKKUEbxg+l6l!p_gyK$KV5*j|N6=V);hZ&ki~Hj zjzK0cC}vS6>wNFHYybfM`R;;2ou~i+0O?)wyN!L5=(Il`cTAcz@MdAvprag7!lHBx z*N@01UU@fzv;tz@C_Uu&>d9<68C=|UQp+q*rqDfewp3dq_u=WzPi6x5{mfqUF8+&Z zm=Z*+!YMJb#>#AG_3olNU9{rv@jK&jEPg#dBDKmu`-)PQcLy!rJ@JP$q!DyV3&Fr* zfD&{u*av;MtK<=Oy37mznJ)6GWmEz{e#jAsVH7TV;{1oGTE7awBTH|2*(3`7jwR#4Df#X-bEC{4-B{(Td94o@KV$M?dr{e>&p-pxKUC4^8cZ-jIq$HG@~PkAQ%(3DX>>#VD9_2xVvF zTALuZ4gO%h12M_`m+3Emya4*e{JlyELt)W)#F9>0rU&Ft>_IK3B(toag>Y#kwo(C^qkc4zA(V&!28E`D1^)J zM-8 zRr7=aISJ;E#GV;M$*Spy$Aju#0@fgYrn7*U&zz0edOEJNnnVOsY^A@GmtS>i*O!tG zn1y+tx1qa=zqj29WJFHz?`$DGK+hChn&|;(AZLyOpYFZ+&Xi0)xyL_0!@T$^rnv$5 z8Z!s>`M)ME-veZ|j?O@keA+r@G8}{vz;8~7zZC8IH}DSWF6^3urZs*X-AO>HG)F4; zVk8&2%d}ZhA-j*J^?ev<%u% z3Lk}>=lcu)x!$%wvX223*NQmsyOEWT3M&!zMzzwv$@PR<1ET!|+dncRid?3)OIZK= zzREfB&2ijR3B7AxF*T5DmokJHI>xF=!&hitJrLZP!=m{#Jk8EP047VY6LZ(306w%` zW-cPdSOnOxa@{$H(815H74fw-H?-}DHzaL`e?-szhN{diQ<)v!VfB*opaX2FY literal 0 HcmV?d00001 diff --git a/src/renderer/src/pages/settings/MCPSettings/index.tsx b/src/renderer/src/pages/settings/MCPSettings/index.tsx index 6e4ac3d2af..6adb64ca23 100644 --- a/src/renderer/src/pages/settings/MCPSettings/index.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/index.tsx @@ -2,6 +2,7 @@ import { ArrowLeftOutlined } from '@ant-design/icons' import Ai302ProviderLogo from '@renderer/assets/images/providers/302ai.webp' import BailianProviderLogo from '@renderer/assets/images/providers/bailian.png' import LanyunProviderLogo from '@renderer/assets/images/providers/lanyun.png' +import MCPRouterProviderLogo from '@renderer/assets/images/providers/mcprouter.webp' import ModelScopeProviderLogo from '@renderer/assets/images/providers/modelscope.png' import TokenFluxProviderLogo from '@renderer/assets/images/providers/tokenflux.png' import DividerWithText from '@renderer/components/DividerWithText' @@ -72,7 +73,8 @@ const MCPSettings: FC = () => { tokenflux: , lanyun: , '302ai': , - bailian: + bailian: , + mcprouter: } return ( diff --git a/src/renderer/src/pages/settings/MCPSettings/providers/config.ts b/src/renderer/src/pages/settings/MCPSettings/providers/config.ts index efe46be5fb..6b094536e1 100644 --- a/src/renderer/src/pages/settings/MCPSettings/providers/config.ts +++ b/src/renderer/src/pages/settings/MCPSettings/providers/config.ts @@ -3,6 +3,7 @@ import type { MCPServer } from '@renderer/types' import { getAI302Token, saveAI302Token, syncAi302Servers } from './302ai' import { getBailianToken, saveBailianToken, syncBailianServers } from './bailian' import { getTokenLanYunToken, LANYUN_KEY_HOST, saveTokenLanYunToken, syncTokenLanYunServers } from './lanyun' +import { getMCPRouterToken, saveMCPRouterToken, syncMCPRouterServers } from './mcprouter' import { getModelScopeToken, MODELSCOPE_HOST, saveModelScopeToken, syncModelScopeServers } from './modelscope' import { getTokenFluxToken, saveTokenFluxToken, syncTokenFluxServers, TOKENFLUX_HOST } from './tokenflux' @@ -73,5 +74,16 @@ export const providers: ProviderConfig[] = [ getToken: getBailianToken, saveToken: saveBailianToken, syncServers: syncBailianServers + }, + { + key: 'mcprouter', + name: 'MCP Router', + description: 'MCP Router 平台 MCP 服务', + discoverUrl: 'https://mcprouter.co', + apiKeyUrl: 'https://mcprouter.co/settings/keys', + tokenFieldName: 'mcprouterToken', + getToken: getMCPRouterToken, + saveToken: saveMCPRouterToken, + syncServers: syncMCPRouterServers } ] diff --git a/src/renderer/src/pages/settings/MCPSettings/providers/mcprouter.ts b/src/renderer/src/pages/settings/MCPSettings/providers/mcprouter.ts new file mode 100644 index 0000000000..a993ce7383 --- /dev/null +++ b/src/renderer/src/pages/settings/MCPSettings/providers/mcprouter.ts @@ -0,0 +1,157 @@ +import { loggerService } from '@logger' +import { nanoid } from '@reduxjs/toolkit' +import type { MCPServer } from '@renderer/types' +import i18next from 'i18next' + +const logger = loggerService.withContext('MCPRouterSyncUtils') + +// Token storage constants and utilities +const TOKEN_STORAGE_KEY = 'mcprouter_token' +export const MCPROUTER_HOST = 'https://mcprouter.co' + +export const saveMCPRouterToken = (token: string): void => { + localStorage.setItem(TOKEN_STORAGE_KEY, token) +} + +export const getMCPRouterToken = (): string | null => { + return localStorage.getItem(TOKEN_STORAGE_KEY) +} + +export const clearMCPRouterToken = (): void => { + localStorage.removeItem(TOKEN_STORAGE_KEY) +} + +export const hasMCPRouterToken = (): boolean => { + return !!getMCPRouterToken() +} + +interface MCPRouterServer { + 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 +} + +interface MCPRouterSyncResult { + success: boolean + message: string + addedServers: MCPServer[] + updatedServers: MCPServer[] + errorDetails?: string +} + +// Function to fetch and process MCPRouter servers +export const syncMCPRouterServers = async ( + token: string, + existingServers: MCPServer[] +): Promise => { + const t = i18next.t + + try { + const response = await fetch('https://api.mcprouter.to/v1/list-servers', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + 'HTTP-Referer': 'https://cherry-ai.com', + 'X-Title': 'Cherry Studio' + }, + body: JSON.stringify({}) + }) + + // Handle authentication errors + if (response.status === 401 || response.status === 403) { + clearMCPRouterToken() + return { + success: false, + message: t('settings.mcp.sync.unauthorized', 'Sync Unauthorized'), + addedServers: [], + updatedServers: [] + } + } + + // Handle server errors + if (response.status === 500 || !response.ok) { + return { + success: false, + message: t('settings.mcp.sync.error'), + addedServers: [], + updatedServers: [], + errorDetails: `Status: ${response.status}` + } + } + + // Process successful response + const data = await response.json() + const servers: MCPRouterServer[] = data.data?.servers || [] + + if (servers.length === 0) { + return { + success: true, + message: t('settings.mcp.sync.noServersAvailable', 'No MCP servers available'), + addedServers: [], + updatedServers: [] + } + } + + // Transform MCPRouter servers to MCP servers format + const addedServers: MCPServer[] = [] + const updatedServers: MCPServer[] = [] + + for (const server of servers) { + try { + // Check if server already exists using server_key + const existingServer = existingServers.find((s) => s.id === `@mcprouter/${server.server_key}`) + + const mcpServer: MCPServer = { + id: `@mcprouter/${server.server_key}`, + name: server.title || server.name || `MCPRouter Server ${nanoid()}`, + description: server.description || '', + type: 'streamableHttp', + baseUrl: server.server_url, + isActive: true, + provider: 'MCPRouter', + providerUrl: `https://mcprouter.co/${server.server_key}`, + logoUrl: '', + tags: [], + headers: { + Authorization: `Bearer ${token}` + } + } + + if (existingServer) { + // Update existing server with corrected URL and latest info + updatedServers.push(mcpServer) + } else { + // Add new server + addedServers.push(mcpServer) + } + } catch (err) { + logger.error('Error processing MCPRouter server:', err as Error) + } + } + + const totalServers = addedServers.length + updatedServers.length + return { + success: true, + message: t('settings.mcp.sync.success', { count: totalServers }), + addedServers, + updatedServers + } + } catch (error) { + logger.error('MCPRouter sync error:', error as Error) + return { + success: false, + message: t('settings.mcp.sync.error'), + addedServers: [], + updatedServers: [], + errorDetails: String(error) + } + } +}