refactor: mcp service

This commit is contained in:
kangfenmao 2025-03-28 03:31:15 +08:00
parent bb6fdd2db7
commit 29b5ba787b
49 changed files with 861 additions and 1583 deletions

View File

@ -1,26 +0,0 @@
diff --git a/dist/cjs/client/stdio.js b/dist/cjs/client/stdio.js
index 2ada8771c5f76673b5021d6453c6bdd7e0b88013..89f6ea9ca7de86294d3d966f3454b98e19bfe534 100644
--- a/dist/cjs/client/stdio.js
+++ b/dist/cjs/client/stdio.js
@@ -68,7 +68,7 @@ class StdioClientTransport {
this._process = (0, node_child_process_1.spawn)(this._serverParams.command, (_a = this._serverParams.args) !== null && _a !== void 0 ? _a : [], {
env: (_b = this._serverParams.env) !== null && _b !== void 0 ? _b : getDefaultEnvironment(),
stdio: ["pipe", "pipe", (_c = this._serverParams.stderr) !== null && _c !== void 0 ? _c : "inherit"],
- shell: false,
+ shell: process.platform === 'win32' ? true : false,
signal: this._abortController.signal,
windowsHide: node_process_1.default.platform === "win32" && isElectron(),
cwd: this._serverParams.cwd,
diff --git a/dist/esm/client/stdio.js b/dist/esm/client/stdio.js
index 387c982fd40fd8db9790a78e1a05c9ecb81501c0..7b7e60a306bca73149609015a27e904a0a68ca02 100644
--- a/dist/esm/client/stdio.js
+++ b/dist/esm/client/stdio.js
@@ -61,7 +61,7 @@ export class StdioClientTransport {
this._process = spawn(this._serverParams.command, (_a = this._serverParams.args) !== null && _a !== void 0 ? _a : [], {
env: (_b = this._serverParams.env) !== null && _b !== void 0 ? _b : getDefaultEnvironment(),
stdio: ["pipe", "pipe", (_c = this._serverParams.stderr) !== null && _c !== void 0 ? _c : "inherit"],
- shell: false,
+ shell: process.platform === 'win32' ? true : false,
signal: this._abortController.signal,
windowsHide: process.platform === "win32" && isElectron(),
cwd: this._serverParams.cwd,

View File

@ -0,0 +1,18 @@
diff --git a/dist/index.node.js b/dist/index.node.js
index bb108cbc210af5b99e864fd1dd8c555e948ecf7a..8ef8c1aab59215c21d161c0e52125724528ecab8 100644
--- a/dist/index.node.js
+++ b/dist/index.node.js
@@ -1,8 +1,11 @@
let crypto;
crypto =
globalThis.crypto?.webcrypto ?? // Node.js 16 REPL has globalThis.crypto as node:crypto
- globalThis.crypto ?? // Node.js 18+
- (await import("node:crypto")).webcrypto; // Node.js 16 non-REPL
+ globalThis.crypto ?? // Node.js 18+
+ (async() => {
+ const crypto = await import("node:crypto");
+ return crypto.webcrypto;
+ })();
/**
* Creates an array of length `size` of random bytes
* @param size

View File

@ -65,13 +65,11 @@
"@electron/notarize": "^2.5.0", "@electron/notarize": "^2.5.0",
"@google/generative-ai": "^0.21.0", "@google/generative-ai": "^0.21.0",
"@langchain/community": "^0.3.36", "@langchain/community": "^0.3.36",
"@modelcontextprotocol/sdk": "patch:@modelcontextprotocol/sdk@npm%3A1.6.1#~/.yarn/patches/@modelcontextprotocol-sdk-npm-1.6.1-b46313efe7.patch",
"@notionhq/client": "^2.2.15", "@notionhq/client": "^2.2.15",
"@tryfabric/martian": "^1.2.4", "@tryfabric/martian": "^1.2.4",
"@types/react-infinite-scroll-component": "^5.0.0", "@types/react-infinite-scroll-component": "^5.0.0",
"@xyflow/react": "^12.4.4", "@xyflow/react": "^12.4.4",
"adm-zip": "^0.5.16", "adm-zip": "^0.5.16",
"chokidar": "^4.0.3",
"docx": "^9.0.2", "docx": "^9.0.2",
"electron-log": "^5.1.5", "electron-log": "^5.1.5",
"electron-store": "^8.2.0", "electron-store": "^8.2.0",
@ -105,12 +103,12 @@
"@google/genai": "^0.4.0", "@google/genai": "^0.4.0",
"@hello-pangea/dnd": "^16.6.0", "@hello-pangea/dnd": "^16.6.0",
"@kangfenmao/keyv-storage": "^0.1.0", "@kangfenmao/keyv-storage": "^0.1.0",
"@modelcontextprotocol/sdk": "^1.8.0",
"@notionhq/client": "^2.2.15", "@notionhq/client": "^2.2.15",
"@reduxjs/toolkit": "^2.2.5", "@reduxjs/toolkit": "^2.2.5",
"@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch", "@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch",
"@tryfabric/martian": "^1.2.4", "@tryfabric/martian": "^1.2.4",
"@types/adm-zip": "^0", "@types/adm-zip": "^0",
"@types/chokidar": "^2.1.7",
"@types/fs-extra": "^11", "@types/fs-extra": "^11",
"@types/lodash": "^4.17.5", "@types/lodash": "^4.17.5",
"@types/markdown-it": "^14", "@types/markdown-it": "^14",
@ -185,7 +183,8 @@
"pdf-parse@npm:1.1.1": "patch:pdf-parse@npm%3A1.1.1#~/.yarn/patches/pdf-parse-npm-1.1.1-04a6109b2a.patch", "pdf-parse@npm:1.1.1": "patch:pdf-parse@npm%3A1.1.1#~/.yarn/patches/pdf-parse-npm-1.1.1-04a6109b2a.patch",
"@langchain/openai@npm:^0.3.16": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch", "@langchain/openai@npm:^0.3.16": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch",
"@langchain/openai@npm:>=0.1.0 <0.4.0": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch", "@langchain/openai@npm:>=0.1.0 <0.4.0": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch",
"openai@npm:^4.77.0": "patch:openai@npm%3A4.77.3#~/.yarn/patches/openai-npm-4.77.3-59c6d42e7a.patch" "openai@npm:^4.77.0": "patch:openai@npm%3A4.77.3#~/.yarn/patches/openai-npm-4.77.3-59c6d42e7a.patch",
"pkce-challenge@npm:^4.1.0": "patch:pkce-challenge@npm%3A4.1.0#~/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch"
}, },
"packageManager": "yarn@4.6.0", "packageManager": "yarn@4.6.0",
"lint-staged": { "lint-staged": {

View File

@ -1,8 +1,5 @@
const { ProxyAgent } = require('undici')
const { SocksProxyAgent } = require('socks-proxy-agent')
const https = require('https') const https = require('https')
const fs = require('fs') const fs = require('fs')
const { pipeline } = require('stream/promises')
/** /**
* Downloads a file from a URL with redirect handling * Downloads a file from a URL with redirect handling
@ -11,42 +8,28 @@ const { pipeline } = require('stream/promises')
* @returns {Promise<void>} Promise that resolves when download is complete * @returns {Promise<void>} Promise that resolves when download is complete
*/ */
async function downloadWithRedirects(url, destinationPath) { async function downloadWithRedirects(url, destinationPath) {
const proxyUrl = process.env.HTTPS_PROXY || process.env.HTTP_PROXY return new Promise((resolve, reject) => {
if (proxyUrl.startsWith('socks')) { const request = (url) => {
const proxyAgent = new SocksProxyAgent(proxyUrl) https
return new Promise((resolve, reject) => { .get(url, (response) => {
const request = (url) => { if (response.statusCode == 301 || response.statusCode == 302) {
https request(response.headers.location)
.get(url, { agent: proxyAgent }, (response) => { return
if (response.statusCode == 301 || response.statusCode == 302) { }
request(response.headers.location) if (response.statusCode !== 200) {
return reject(new Error(`Download failed: ${response.statusCode} ${response.statusMessage}`))
} return
if (response.statusCode !== 200) { }
reject(new Error(`Download failed: ${response.statusCode} ${response.statusMessage}`)) const file = fs.createWriteStream(destinationPath)
return response.pipe(file)
} file.on('finish', () => resolve())
const file = fs.createWriteStream(destinationPath) })
response.pipe(file) .on('error', (err) => {
file.on('finish', () => resolve()) reject(err)
}) })
.on('error', (err) => {
reject(err)
})
}
request(url)
})
} else {
const proxyAgent = new ProxyAgent(proxyUrl)
const response = await fetch(url, {
dispatcher: proxyAgent
})
if (!response.ok) {
throw new Error(`Download failed: ${response.status} ${response.statusText}`)
} }
const file = fs.createWriteStream(destinationPath) request(url)
await pipeline(response.body, file) })
}
} }
module.exports = { downloadWithRedirects } module.exports = { downloadWithRedirects }

View File

@ -2,7 +2,7 @@ import fs from 'node:fs'
import { isMac, isWin } from '@main/constant' import { isMac, isWin } from '@main/constant'
import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process' import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process'
import { MCPServer, Shortcut, ThemeMode } from '@types' import { Shortcut, ThemeMode } from '@types'
import { BrowserWindow, ipcMain, session, shell } from 'electron' import { BrowserWindow, ipcMain, session, shell } from 'electron'
import log from 'electron-log' import log from 'electron-log'
@ -16,7 +16,7 @@ import FileService from './services/FileService'
import FileStorage from './services/FileStorage' import FileStorage from './services/FileStorage'
import { GeminiService } from './services/GeminiService' import { GeminiService } from './services/GeminiService'
import KnowledgeService from './services/KnowledgeService' import KnowledgeService from './services/KnowledgeService'
import MCPService from './services/MCPService' import mcpService from './services/MCPService'
import * as NutstoreService from './services/NutstoreService' import * as NutstoreService from './services/NutstoreService'
import ObsidianVaultService from './services/ObsidianVaultService' import ObsidianVaultService from './services/ObsidianVaultService'
import { ProxyConfig, proxyManager } from './services/ProxyManager' import { ProxyConfig, proxyManager } from './services/ProxyManager'
@ -31,7 +31,6 @@ import { compress, decompress } from './utils/zip'
const fileManager = new FileStorage() const fileManager = new FileStorage()
const backupManager = new BackupManager() const backupManager = new BackupManager()
const exportService = new ExportService(fileManager) const exportService = new ExportService(fileManager)
const mcpService = new MCPService()
const obsidianVaultService = new ObsidianVaultService() const obsidianVaultService = new ObsidianVaultService()
export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
@ -264,36 +263,15 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
) )
// Register MCP handlers // Register MCP handlers
ipcMain.on('mcp:servers-from-renderer', (_, servers) => mcpService.setServers(servers)) ipcMain.handle('mcp:remove-server', mcpService.removeServer)
ipcMain.handle('mcp:list-servers', async () => mcpService.listAvailableServices()) ipcMain.handle('mcp:list-tools', mcpService.listTools)
ipcMain.handle('mcp:add-server', async (_, server: MCPServer) => mcpService.addServer(server)) ipcMain.handle('mcp:call-tool', mcpService.callTool)
ipcMain.handle('mcp:update-server', async (_, server: MCPServer) => mcpService.updateServer(server))
ipcMain.handle('mcp:delete-server', async (_, serverName: string) => mcpService.deleteServer(serverName))
ipcMain.handle('mcp:set-server-active', async (_, { name, isActive }) =>
mcpService.setServerActive({ name, isActive })
)
// According to preload, this should take no parameters, but our implementation accepts
// an optional serverName for better flexibility
ipcMain.handle('mcp:list-tools', async (_, serverName?: string) => mcpService.listTools(serverName))
ipcMain.handle('mcp:call-tool', async (_, params: { client: string; name: string; args: any }) =>
mcpService.callTool(params)
)
ipcMain.handle('mcp:cleanup', async () => mcpService.cleanup())
ipcMain.handle('app:is-binary-exist', (_, name: string) => isBinaryExists(name)) ipcMain.handle('app:is-binary-exist', (_, name: string) => isBinaryExists(name))
ipcMain.handle('app:get-binary-path', (_, name: string) => getBinaryPath(name)) ipcMain.handle('app:get-binary-path', (_, name: string) => getBinaryPath(name))
ipcMain.handle('app:install-uv-binary', () => runInstallScript('install-uv.js')) ipcMain.handle('app:install-uv-binary', () => runInstallScript('install-uv.js'))
ipcMain.handle('app:install-bun-binary', () => runInstallScript('install-bun.js')) ipcMain.handle('app:install-bun-binary', () => runInstallScript('install-bun.js'))
// Listen for changes in MCP servers and notify renderer
mcpService.on('servers-updated', (servers) => {
mainWindow?.webContents.send('mcp:servers-updated', servers)
})
app.on('before-quit', () => mcpService.cleanup())
//copilot //copilot
ipcMain.handle('copilot:get-auth-message', CopilotService.getAuthMessage) ipcMain.handle('copilot:get-auth-message', CopilotService.getAuthMessage)
ipcMain.handle('copilot:get-copilot-token', CopilotService.getCopilotToken) ipcMain.handle('copilot:get-copilot-token', CopilotService.getCopilotToken)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 353 KiB

View File

@ -2,6 +2,10 @@ import fs from 'node:fs'
export default class FileService { export default class FileService {
public static async readFile(_: Electron.IpcMainInvokeEvent, path: string) { public static async readFile(_: Electron.IpcMainInvokeEvent, path: string) {
const stats = fs.statSync(path)
if (stats.isDirectory()) {
throw new Error(`Cannot read directory: ${path}`)
}
return fs.readFileSync(path, 'utf8') return fs.readFileSync(path, 'utf8')
} }
} }

View File

@ -213,6 +213,11 @@ class FileStorage {
public readFile = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<string> => { public readFile = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<string> => {
const filePath = path.join(this.storageDir, id) const filePath = path.join(this.storageDir, id)
const stats = await fs.promises.stat(filePath)
if (stats.isDirectory()) {
throw new Error(`Cannot read directory: ${filePath}`)
}
if (documentExts.includes(path.extname(filePath))) { if (documentExts.includes(path.extname(filePath))) {
const originalCwd = process.cwd() const originalCwd = process.cwd()

View File

@ -1,719 +1,156 @@
import { EventEmitter } from 'node:events'
import { promises as fs } from 'node:fs'
import { join } from 'node:path'
import { isLinux, isMac, isWin } from '@main/constant'
import { getBinaryPath } from '@main/utils/process' import { getBinaryPath } from '@main/utils/process'
import type { Client } from '@modelcontextprotocol/sdk/client/index.js' import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import type { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js' import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
import type { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
import { MCPServer, MCPTool } from '@types' import { MCPServer } from '@types'
import { app } from 'electron' import Logger from 'electron-log'
import log from 'electron-log'
import { v4 as uuidv4 } from 'uuid'
import { CacheService } from './CacheService' class McpService {
import { windowService } from './WindowService' private client: Client | null = null
private clients: Map<string, Client> = new Map()
interface ActiveServer { private getServerKey(server: MCPServer): string {
client: Client return JSON.stringify({
server: MCPServer baseUrl: server.baseUrl,
} command: server.command,
args: server.args,
/** env: server.env,
* Service for managing Model Context Protocol servers and tools id: server.id
*/
export default class MCPService extends EventEmitter {
private servers: MCPServer[] = []
private activeServers: Map<string, ActiveServer> = new Map()
private clients: { [key: string]: Client } = {}
private Client: typeof Client | undefined
private stdioTransport: typeof StdioClientTransport | undefined
private sseTransport: typeof SSEClientTransport | undefined
private initialized = false
private initPromise: Promise<void> | null = null
private configPath: string
// Simplified server loading state management
private readyState = {
serversLoaded: false,
promise: null as Promise<void> | null,
resolve: null as ((value: void) => void) | null
}
constructor() {
super()
const userDataPath = app.getPath('userData')
this.configPath = join(userDataPath, 'cherry-mcp-servers.json')
this.createServerLoadingPromise()
this.init().catch((err) => this.logError('Failed to initialize MCP service', err))
}
/**
* Create a promise that resolves when servers are loaded
*/
private createServerLoadingPromise(): void {
this.readyState.promise = new Promise<void>((resolve) => {
this.readyState.resolve = resolve
}) })
} }
private async ensureConfigExists(): Promise<void> { constructor() {
try { this.initClient = this.initClient.bind(this)
await fs.access(this.configPath) this.listTools = this.listTools.bind(this)
} catch { this.callTool = this.callTool.bind(this)
const defaultServers = { this.closeClient = this.closeClient.bind(this)
name: 'mcp-auto-install', this.removeServer = this.removeServer.bind(this)
command: 'npx',
args: ['-y', '@mcpmarket/mcp-auto-install', 'connect'],
env: {
MCP_SETTINGS_PATH: this.configPath
},
isActive: true
}
const defaultConfig = {
mcpServers: {
'mcp-auto-install': defaultServers
}
}
// 尝试从Redux获取已有配置
try {
const mainWindow = windowService.getMainWindow()
if (mainWindow) {
const servers = await mainWindow.webContents.executeJavaScript(`
window.store.getState().mcp.servers
`)
if (servers && servers.length > 0) {
// 将从Redux获取的配置保存到文件
await this.saveConfigToFile(servers.concat([defaultServers]))
log.info('[MCP] Migrated servers config from Redux to file')
return
}
}
} catch (error) {
log.warn('[MCP] Failed to get servers from Redux:', error)
}
// 如果没有Redux配置则创建默认配置
await fs.writeFile(this.configPath, JSON.stringify(defaultConfig, null, 2))
log.info('[MCP] Created default config file')
}
} }
private async loadConfigFromFile(): Promise<MCPServer[]> { async initClient(server: MCPServer) {
try { const serverKey = this.getServerKey(server)
const data = await fs.readFile(this.configPath, 'utf-8')
const config = JSON.parse(data)
if (config.mcpServers && typeof config.mcpServers === 'object') { // Check if we already have a client for this server configuration
console.log('读写读写读写', config) const existingClient = this.clients.get(serverKey)
return Object.entries(config.mcpServers).map(([name, serverData]) => ({ if (existingClient) {
name, this.client = existingClient
...(serverData as Omit<MCPServer, 'name'>)
}))
}
return []
} catch (error) {
log.error('[MCP] Error loading config file:', error)
return []
}
}
private async saveConfigToFile(servers: MCPServer[]): Promise<void> {
try {
// 将数组转换为对象结构
const mcpServers = servers.reduce(
(acc, server) => {
const { name, ...serverData } = server
acc[name] = serverData
return acc
},
{} as Record<string, Omit<MCPServer, 'name'>>
)
const config = { mcpServers }
await fs.writeFile(this.configPath, JSON.stringify(config, null, 2))
} catch (error) {
log.error('[MCP] Error saving config file:', error)
throw error
}
}
/**
* Set servers received from Redux and trigger initialization if needed
*/
public setServers(servers: any): void {
// 如果已初始化,则更新服务器列表并保存到文件
this.servers = servers
if (this.initialized) {
log.info(`[MCP] Received ${servers.length} servers from Redux, saving to file`)
// 保存到文件
this.saveConfigToFile(servers).catch((err) => {
log.error('[MCP] Failed to save servers to file:', err)
})
} else {
log.info(`[MCP] Received ${servers.length} servers from Redux, but service not initialized yet`)
// 如果未初始化,则标记已加载并解决 Promise
if (!this.readyState.serversLoaded && this.readyState.resolve) {
this.readyState.serversLoaded = true
this.readyState.resolve()
this.readyState.resolve = null
}
// 初始化服务
// this.init().catch((err) => this.logError('Failed to initialize MCP service', err))
}
}
/**
* Initialize the MCP service if not already initialized
*/
public async init(): Promise<void> {
if (this.initialized) return
if (this.initPromise) return this.initPromise
this.initPromise = (async () => {
try {
log.info('[MCP] Starting initialization')
// 加载 SDK 组件
const [Client, StdioTransport, SSETransport] = await Promise.all([
this.importClient(),
this.importStdioClientTransport(),
this.importSSEClientTransport()
])
this.Client = Client
this.stdioTransport = StdioTransport
this.sseTransport = SSETransport
// 等待Redux初始化完成后再加载配置
if (!this.readyState.serversLoaded && this.readyState.promise) {
await this.readyState.promise
}
// 确保配置文件存在
await this.ensureConfigExists()
// 从文件加载配置
const serversFromFile = await this.loadConfigFromFile()
if (serversFromFile.length > 0) {
this.servers = serversFromFile
// 将从文件加载的配置通知给 Redux
this.notifyReduxServersChanged(serversFromFile)
}
// 标记为已初始化并解决 readyState 的 Promise
this.initialized = true
if (this.readyState.resolve) {
this.readyState.serversLoaded = true
this.readyState.resolve()
this.readyState.resolve = null
}
// 加载活跃服务器
await this.loadActiveServers()
log.info('[MCP] Initialization successfully')
return
} catch (err) {
this.initialized = false
log.error('[MCP] Failed to initialize:', err)
throw err
} finally {
this.initPromise = null
}
})()
return this.initPromise
}
/**
* Helper to create consistent error logging functions
*/
private logError(message: string, err?: unknown): void {
log.error(`[MCP] ${message}`, err)
}
/**
* Import the MCP client SDK
*/
private async importClient() {
try {
const { Client } = await import('@modelcontextprotocol/sdk/client/index.js')
return Client
} catch (err) {
this.logError('Failed to import Client:', err)
throw err
}
}
/**
* Import the stdio transport
*/
private async importStdioClientTransport() {
try {
const { StdioClientTransport } = await import('@modelcontextprotocol/sdk/client/stdio.js')
return StdioClientTransport
} catch (err) {
log.error('[MCP] Failed to import StdioTransport:', err)
throw err
}
}
/**
* Import the SSE transport
*/
private async importSSEClientTransport() {
try {
const { SSEClientTransport } = await import('@modelcontextprotocol/sdk/client/sse.js')
return SSEClientTransport
} catch (err) {
log.error('[MCP] Failed to import SSETransport:', err)
throw err
}
}
/**
* List all available MCP servers
*/
public async listAvailableServices(): Promise<MCPServer[]> {
await this.ensureInitialized()
return this.servers
}
/**
* Ensure the service is initialized before operations
*/
private async ensureInitialized() {
if (!this.initialized) {
log.debug('[MCP] Ensuring initialization')
await this.init()
}
}
/**
* Add a new MCP server
*/
public async addServer(server: MCPServer): Promise<void> {
await this.ensureInitialized()
// Check for duplicate name
if (this.servers.some((s) => s.name === server.name)) {
throw new Error(`Server with name ${server.name} already exists`)
}
// Activate if needed
if (server.isActive) {
await this.activate(server)
}
// Add to servers list
this.servers = [...this.servers, server]
this.notifyReduxServersChanged(this.servers)
}
/**
* Update an existing MCP server
*/
public async updateServer(server: MCPServer): Promise<void> {
await this.ensureInitialized()
const index = this.servers.findIndex((s) => s.name === server.name)
if (index === -1) {
throw new Error(`Server ${server.name} not found`)
}
// Check activation status change
const wasActive = this.servers[index].isActive
if (wasActive && !server.isActive) {
await this.deactivate(server.name)
} else if (!wasActive && server.isActive) {
await this.activate(server)
} else {
await this.restartServer(server)
}
// Update servers list
const updatedServers = [...this.servers]
updatedServers[index] = server
this.servers = updatedServers
// Notify Redux
this.notifyReduxServersChanged(updatedServers)
}
public async restartServer(_server: MCPServer): Promise<void> {
await this.ensureInitialized()
const server = this.servers.find((s) => s.name === _server.name)
if (server) {
if (server.isActive) {
await this.deactivate(server.name)
}
await this.activate(server)
}
}
/**
* Delete an MCP server
*/
public async deleteServer(serverName: string): Promise<void> {
await this.ensureInitialized()
// Deactivate if running
if (this.clients[serverName]) {
await this.deactivate(serverName)
}
// Update servers list
const filteredServers = this.servers.filter((s) => s.name !== serverName)
this.servers = filteredServers
this.notifyReduxServersChanged(filteredServers)
}
/**
* Set a server's active state
*/
public async setServerActive(params: { name: string; isActive: boolean }): Promise<void> {
await this.ensureInitialized()
const { name, isActive } = params
const server = this.servers.find((s) => s.name === name)
if (!server) {
throw new Error(`Server ${name} not found`)
}
// Activate or deactivate as needed
if (isActive) {
await this.activate(server)
} else {
await this.deactivate(name)
}
// Update server status
server.isActive = isActive
this.notifyReduxServersChanged([...this.servers])
}
/**
* Notify Redux in the renderer process about server changes
*/
private notifyReduxServersChanged(servers: MCPServer[]): void {
const mainWindow = windowService.getMainWindow()
if (mainWindow) {
mainWindow.webContents.send('mcp:servers-changed', servers)
}
}
/**
* Activate an MCP server
*/
public async activate(server: MCPServer): Promise<void> {
await this.ensureInitialized()
const { name, baseUrl, command, env } = server
const args = [...(server.args || [])]
// Skip if already running
if (this.clients[name]) {
log.info(`[MCP] Server ${name} is already running`)
return return
} }
// If there's an existing client for a different server, close it
if (this.client) {
await this.closeClient()
}
// Create new client instance for each connection
this.client = new Client({ name: 'McpService', version: '1.0.0' }, { capabilities: {} })
const args = [...(server.args || [])]
let transport: StdioClientTransport | SSEClientTransport let transport: StdioClientTransport | SSEClientTransport
try { try {
// Create appropriate transport based on configuration // Create appropriate transport based on configuration
if (baseUrl) { if (server.baseUrl) {
transport = new this.sseTransport!(new URL(baseUrl)) transport = new SSEClientTransport(new URL(server.baseUrl))
} else if (command) { } else if (server.command) {
let cmd: string = command let cmd = server.command
if (command === 'npx') {
if (server.command === 'npx') {
cmd = await getBinaryPath('bun') cmd = await getBinaryPath('bun')
if (cmd === 'bun') { if (cmd === 'bun') {
cmd = 'npx' cmd = 'npx'
} }
log.info(`[MCP] Using command: ${cmd}`) Logger.info(`[MCP] Using command: ${cmd}`)
// add -x to args if args exist // add -x to args if args exist
if (args && args.length > 0) { if (args && args.length > 0) {
if (!args.includes('-y')) { if (!args.includes('-y')) {
args.unshift('-y') !args.includes('-y') && args.unshift('-y')
} }
if (cmd.includes('bun') && !args.includes('x')) { if (cmd.includes('bun') && !args.includes('x')) {
args.unshift('x') args.unshift('x')
} }
} }
} else if (command === 'uvx') { }
if (server.command === 'uvx') {
cmd = await getBinaryPath('uvx') cmd = await getBinaryPath('uvx')
} }
log.info(`[MCP] Starting server with command: ${cmd} ${args ? args.join(' ') : ''}`) Logger.info(`[MCP] Starting server with command: ${cmd} ${args ? args.join(' ') : ''}`)
transport = new this.stdioTransport!({ transport = new StdioClientTransport({
command: cmd, command: cmd,
args, args,
stderr: 'pipe', env: server.env
env: {
PATH: this.getEnhancedPath(process.env.PATH || ''),
...env
}
}) })
} else { } else {
throw new Error('Either baseUrl or command must be provided') throw new Error('Either baseUrl or command must be provided')
} }
// Create and connect client await this.client.connect(transport)
const client = new this.Client!({ name, version: '1.0.0' }, { capabilities: {} })
await client.connect(transport) // Store the new client in the cache
this.clients.set(serverKey, this.client)
// Store client and server info Logger.info(`[MCP] Activated server: ${server.name}`)
this.clients[name] = client } catch (error: any) {
this.activeServers.set(name, { client, server }) Logger.error(`[MCP] Error activating server ${server.name}:`, error)
log.info(`[MCP] Activated server: ${server.name}`)
this.emit('server-started', { name })
} catch (error) {
log.error(`[MCP] Error activating server ${name}:`, error)
this.setServerActive({ name, isActive: false })
throw error throw error
} }
} }
/** async closeClient() {
* Deactivate an MCP server if (this.client) {
*/ // Remove the client from the cache
public async deactivate(name: string): Promise<void> { for (const [key, client] of this.clients.entries()) {
await this.ensureInitialized() if (client === this.client) {
this.clients.delete(key)
if (!this.clients[name]) { break
log.warn(`[MCP] Server ${name} is not running`)
return
}
try {
log.info(`[MCP] Stopping server: ${name}`)
await this.clients[name].close()
delete this.clients[name]
this.activeServers.delete(name)
this.emit('server-stopped', { name })
} catch (error) {
log.error(`[MCP] Error deactivating server ${name}:`, error)
throw error
}
}
/**
* List available tools from active MCP servers
*/
public async listTools(serverName?: string): Promise<MCPTool[]> {
await this.ensureInitialized()
log.info(`[MCP] Listing tools from ${serverName || 'all active servers'}`)
try {
// If server name provided, list tools for that server only
if (serverName) {
return await this.listToolsFromServer(serverName)
}
// Otherwise list tools from all active servers
let allTools: MCPTool[] = []
for (const clientName in this.clients) {
log.info(`[MCP] Listing tools from ${clientName}`)
try {
const tools = await this.listToolsFromServer(clientName)
allTools = allTools.concat(tools)
} catch (error) {
this.logError(`Error listing tools for ${clientName}`, error)
} }
} }
log.info(`[MCP] Total tools listed: ${allTools.length}`) await this.client.close()
return allTools this.client = null
} catch (error) {
this.logError('Error listing tools:', error)
return []
} }
} }
/** async removeServer(_: Electron.IpcMainInvokeEvent, server: MCPServer) {
* Helper method to list tools from a specific server await this.closeClient()
*/ this.clients.delete(this.getServerKey(server))
private async listToolsFromServer(serverName: string): Promise<MCPTool[]> { }
log.info(`[MCP] start list tools from ${serverName}:`)
if (!this.clients[serverName]) {
throw new Error(`MCP Client ${serverName} not found`)
}
const cacheKey = `mcp:list_tool:${serverName}`
if (CacheService.has(cacheKey)) { async listTools(_: Electron.IpcMainInvokeEvent, server: MCPServer) {
log.info(`[MCP] Tools from ${serverName} loaded from cache`) await this.initClient(server)
// Check if cache is still valid const { tools } = await this.client!.listTools()
const cachedTools = CacheService.get<MCPTool[]>(cacheKey) return tools.map((tool) => ({
if (cachedTools && cachedTools.length > 0) {
return cachedTools
}
CacheService.remove(cacheKey)
}
const { tools } = await this.clients[serverName].listTools()
const transformedTools = tools.map((tool: any) => ({
...tool, ...tool,
serverName, serverId: server.id,
id: 'f' + uuidv4().replace(/-/g, '') serverName: server.name
})) }))
// Cache the tools for 5 minutes
if (transformedTools.length > 0) {
CacheService.set(cacheKey, transformedTools, 5 * 60 * 1000)
}
log.info(`[MCP] Tools from ${serverName}:`, transformedTools)
return transformedTools
} }
/** /**
* Call a tool on an MCP server * Call a tool on an MCP server
*/ */
public async callTool(params: { client: string; name: string; args: any }): Promise<any> { public async callTool(
await this.ensureInitialized() _: Electron.IpcMainInvokeEvent,
{ server, name, args }: { server: MCPServer; name: string; args: any }
const { client, name, args } = params ): Promise<any> {
await this.initClient(server)
if (!this.clients[client]) {
throw new Error(`MCP Client ${client} not found`)
}
log.info('[MCP] Calling:', client, name, args)
try { try {
return await this.clients[client].callTool({ Logger.info('[MCP] Calling:', server.name, name, args)
name, const result = await this.client!.callTool({ name, arguments: args })
arguments: args return result
})
} catch (error) { } catch (error) {
log.error(`[MCP] Error calling tool ${name} on ${client}:`, error) Logger.error(`[MCP] Error calling tool ${name} on ${server.name}:`, error)
throw error throw error
} }
} }
/**
* Clean up all MCP resources
*/
public async cleanup(): Promise<void> {
const clientNames = Object.keys(this.clients)
if (clientNames.length === 0) {
log.info('[MCP] No active servers to clean up')
return
}
log.info(`[MCP] Cleaning up ${clientNames.length} active servers`)
// Deactivate all clients
await Promise.allSettled(
clientNames.map((name) =>
this.deactivate(name).catch((err) => {
log.error(`[MCP] Error during cleanup of ${name}:`, err)
})
)
)
this.clients = {}
this.activeServers.clear()
log.info('[MCP] All servers cleaned up')
}
/**
* Load all active servers
*/
private async loadActiveServers(): Promise<void> {
console.log('loadActiveServers', this.servers)
const activeServers = this.servers.filter((server) => server.isActive)
if (activeServers.length === 0) {
log.info('[MCP] No active servers to load')
return
}
log.info(`[MCP] Start loading ${activeServers.length} active servers`)
// Activate servers in parallel for better performance
await Promise.allSettled(
activeServers.map(async (server) => {
try {
await this.activate(server)
} catch (error) {
this.logError(`Failed to activate server ${server.name}`, error)
this.emit('server-error', { name: server.name, error })
}
})
)
log.info(`[MCP] End loading ${Object.keys(this.clients).length} active servers`)
}
/**
* Get enhanced PATH including common tool locations
*/
private getEnhancedPath(originalPath: string): string {
// 将原始 PATH 按分隔符分割成数组
const pathSeparator = process.platform === 'win32' ? ';' : ':'
const existingPaths = new Set(originalPath.split(pathSeparator).filter(Boolean))
const homeDir = process.env.HOME || process.env.USERPROFILE || ''
// 定义要添加的新路径
const newPaths: string[] = []
if (isMac) {
newPaths.push(
'/bin',
'/usr/bin',
'/usr/local/bin',
'/usr/local/sbin',
'/opt/homebrew/bin',
'/opt/homebrew/sbin',
'/usr/local/opt/node/bin',
`${homeDir}/.nvm/current/bin`,
`${homeDir}/.npm-global/bin`,
`${homeDir}/.yarn/bin`,
`${homeDir}/.cargo/bin`,
'/opt/local/bin'
)
}
if (isLinux) {
newPaths.push(
'/bin',
'/usr/bin',
'/usr/local/bin',
`${homeDir}/.nvm/current/bin`,
`${homeDir}/.npm-global/bin`,
`${homeDir}/.yarn/bin`,
`${homeDir}/.cargo/bin`,
'/snap/bin'
)
}
if (isWin) {
newPaths.push(`${process.env.APPDATA}\\npm`, `${homeDir}\\AppData\\Local\\Yarn\\bin`, `${homeDir}\\.cargo\\bin`)
}
// 只添加不存在的路径
for (const path of newPaths) {
if (path && !existingPaths.has(path)) {
existingPaths.add(path)
}
}
// 转换回字符串
return Array.from(existingPaths).join(pathSeparator)
}
} }
export default new McpService()

View File

@ -42,3 +42,7 @@ export function dumpPersistState() {
} }
return JSON.stringify(persistState) return JSON.stringify(persistState)
} }
export const runAsyncFunction = async (fn: () => void) => {
await fn()
}

View File

@ -146,17 +146,9 @@ declare global {
openExternal: (url: string, options?: OpenExternalOptions) => Promise<void> openExternal: (url: string, options?: OpenExternalOptions) => Promise<void>
} }
mcp: { mcp: {
// servers removeServer: (server: MCPServer) => Promise<void>
listServers: () => Promise<MCPServer[]> listTools: (server: MCPServer) => Promise<MCPTool[]>
addServer: (server: MCPServer) => Promise<void> callTool: ({ server, name, args }: { server: MCPServer; name: string; args: any }) => Promise<any>
updateServer: (server: MCPServer) => Promise<void>
deleteServer: (serverName: string) => Promise<void>
setServerActive: (name: string, isActive: boolean) => Promise<void>
// tools
listTools: () => Promise<MCPTool[]>
callTool: ({ client, name, args }: { client: string; name: string; args: any }) => Promise<any>
// status
cleanup: () => Promise<void>
} }
copilot: { copilot: {
getAuthMessage: ( getAuthMessage: (

View File

@ -120,15 +120,10 @@ const api = {
ipcRenderer.invoke('aes:decrypt', encryptedData, iv, secretKey) ipcRenderer.invoke('aes:decrypt', encryptedData, iv, secretKey)
}, },
mcp: { mcp: {
listServers: () => ipcRenderer.invoke('mcp:list-servers'), removeServer: (server: MCPServer) => ipcRenderer.invoke('mcp:remove-server', server),
addServer: (server: MCPServer) => ipcRenderer.invoke('mcp:add-server', server), listTools: (server: MCPServer) => ipcRenderer.invoke('mcp:list-tools', server),
updateServer: (server: MCPServer) => ipcRenderer.invoke('mcp:update-server', server), callTool: ({ server, name, args }: { server: MCPServer; name: string; args: any }) =>
deleteServer: (serverName: string) => ipcRenderer.invoke('mcp:delete-server', serverName), ipcRenderer.invoke('mcp:call-tool', { server, name, args })
setServerActive: (name: string, isActive: boolean) =>
ipcRenderer.invoke('mcp:set-server-active', { name, isActive }),
listTools: (serverName?: string) => ipcRenderer.invoke('mcp:list-tools', serverName),
callTool: (params: { client: string; name: string; args: any }) => ipcRenderer.invoke('mcp:call-tool', params),
cleanup: () => ipcRenderer.invoke('mcp:cleanup')
}, },
shell: { shell: {
openExternal: shell.openExternal openExternal: shell.openExternal

View File

@ -19,7 +19,7 @@
--color-gray-2: #414853; --color-gray-2: #414853;
--color-gray-3: #32363f; --color-gray-3: #32363f;
--color-text-1: rgba(255, 255, 245, 0.86); --color-text-1: rgba(255, 255, 245, 0.9);
--color-text-2: rgba(235, 235, 245, 0.6); --color-text-2: rgba(235, 235, 245, 0.6);
--color-text-3: rgba(235, 235, 245, 0.38); --color-text-3: rgba(235, 235, 245, 0.38);

View File

@ -23,7 +23,7 @@ const Container = styled.div`
` `
const Icon = styled(ToolOutlined)` const Icon = styled(ToolOutlined)`
color: #d97757; color: var(--color-primary);
font-size: 15px; font-size: 15px;
margin-right: 6px; margin-right: 6px;
` `

View File

@ -4,15 +4,25 @@ import styled from 'styled-components'
interface IndicatorLightProps { interface IndicatorLightProps {
color: string color: string
size?: number
shadow?: boolean
style?: React.CSSProperties
animation?: boolean
} }
const Light = styled.div<{ color: string }>` const Light = styled.div<{
width: 8px; color: string
height: 8px; size: number
shadow?: boolean
style?: React.CSSProperties
animation?: boolean
}>`
width: ${({ size }) => size}px;
height: ${({ size }) => size}px;
border-radius: 50%; border-radius: 50%;
background-color: ${({ color }) => color}; background-color: ${({ color }) => color};
box-shadow: 0 0 6px ${({ color }) => color}; box-shadow: ${({ shadow, color }) => (shadow ? `0 0 6px ${color}` : 'none')};
animation: pulse 2s infinite; animation: ${({ animation }) => (animation ? 'pulse 2s infinite' : 'none')};
@keyframes pulse { @keyframes pulse {
0% { 0% {
@ -27,9 +37,9 @@ const Light = styled.div<{ color: string }>`
} }
` `
const IndicatorLight: React.FC<IndicatorLightProps> = ({ color }) => { const IndicatorLight: React.FC<IndicatorLightProps> = ({ color, size = 8, shadow = true, style, animation = true }) => {
const actualColor = color === 'green' ? '#22c55e' : color const actualColor = color === 'green' ? '#22c55e' : color
return <Light color={actualColor} /> return <Light color={actualColor} size={size} shadow={shadow} style={style} animation={animation} />
} }
export default IndicatorLight export default IndicatorLight

View File

@ -8,17 +8,20 @@ interface ListItemProps {
subtitle?: string subtitle?: string
titleStyle?: React.CSSProperties titleStyle?: React.CSSProperties
onClick?: () => void onClick?: () => void
rightContent?: ReactNode
style?: React.CSSProperties
} }
const ListItem = ({ active, icon, title, subtitle, titleStyle, onClick }: ListItemProps) => { const ListItem = ({ active, icon, title, subtitle, titleStyle, onClick, rightContent, style }: ListItemProps) => {
return ( return (
<ListItemContainer className={active ? 'active' : ''} onClick={onClick}> <ListItemContainer className={active ? 'active' : ''} onClick={onClick} style={style}>
<ListItemContent> <ListItemContent>
{icon && <IconWrapper>{icon}</IconWrapper>} {icon && <IconWrapper>{icon}</IconWrapper>}
<TextContainer> <TextContainer>
<TitleText style={titleStyle}>{title}</TitleText> <TitleText style={titleStyle}>{title}</TitleText>
{subtitle && <SubtitleText>{subtitle}</SubtitleText>} {subtitle && <SubtitleText>{subtitle}</SubtitleText>}
</TextContainer> </TextContainer>
{rightContent && <RightContentWrapper>{rightContent}</RightContentWrapper>}
</ListItemContent> </ListItemContent>
</ListItemContainer> </ListItemContainer>
) )
@ -84,4 +87,8 @@ const SubtitleText = styled.div`
color: var(--color-text-3); color: var(--color-text-3);
` `
const RightContentWrapper = styled.div`
margin-left: auto;
`
export default ListItem export default ListItem

View File

@ -1,4 +1,4 @@
import { isMac } from '@renderer/config/constant' import { isMac, isWindows } from '@renderer/config/constant'
import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor' import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor'
import type { FC, PropsWithChildren } from 'react' import type { FC, PropsWithChildren } from 'react'
import type { HTMLAttributes } from 'react' import type { HTMLAttributes } from 'react'
@ -63,4 +63,6 @@ const NavbarRightContainer = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0 12px; padding: 0 12px;
padding-right: ${isWindows ? '140px' : 12};
justify-content: flex-end;
` `

View File

@ -11,7 +11,6 @@ import { useEffect } from 'react'
import { useDefaultModel } from './useAssistant' import { useDefaultModel } from './useAssistant'
import useFullScreenNotice from './useFullScreenNotice' import useFullScreenNotice from './useFullScreenNotice'
import { useInitMCPServers } from './useMCPServers'
import { useRuntime } from './useRuntime' import { useRuntime } from './useRuntime'
import { useSettings } from './useSettings' import { useSettings } from './useSettings'
import useUpdateHandler from './useUpdateHandler' import useUpdateHandler from './useUpdateHandler'
@ -26,7 +25,6 @@ export function useAppInit() {
useUpdateHandler() useUpdateHandler()
useFullScreenNotice() useFullScreenNotice()
useInitMCPServers()
useEffect(() => { useEffect(() => {
avatar?.value && dispatch(setAvatar(avatar.value)) avatar?.value && dispatch(setAvatar(avatar.value))

View File

@ -1,7 +1,6 @@
import store, { useAppSelector } from '@renderer/store' import store, { useAppDispatch, useAppSelector } from '@renderer/store'
import { setMCPServers } from '@renderer/store/mcp' import { addMCPServer, deleteMCPServer, setMCPServers, updateMCPServer } from '@renderer/store/mcp'
import { MCPServer } from '@renderer/types' import { MCPServer } from '@renderer/types'
import { useEffect } from 'react'
const ipcRenderer = window.electron.ipcRenderer const ipcRenderer = window.electron.ipcRenderer
@ -13,82 +12,16 @@ ipcRenderer.on('mcp:servers-changed', (_event, servers) => {
export const useMCPServers = () => { export const useMCPServers = () => {
const mcpServers = useAppSelector((state) => state.mcp.servers) const mcpServers = useAppSelector((state) => state.mcp.servers)
const activedMcpServers = useAppSelector((state) => state.mcp.servers?.filter((server) => server.isActive)) const activedMcpServers = useAppSelector((state) => state.mcp.servers?.filter((server) => server.isActive))
const dispatch = useAppDispatch()
const addMCPServer = async (server: MCPServer) => {
try {
await window.api.mcp.addServer(server)
// Main process will send back updated servers via mcp:servers-changed
} catch (error) {
console.error('Failed to add MCP server:', error)
throw error
}
}
const updateMCPServer = async (server: MCPServer) => {
try {
await window.api.mcp.updateServer(server)
// Main process will send back updated servers via mcp:servers-changed
} catch (error) {
console.error('Failed to update MCP server:', error)
throw error
}
}
const deleteMCPServer = async (name: string) => {
try {
await window.api.mcp.deleteServer(name)
// Main process will send back updated servers via mcp:servers-changed
} catch (error) {
console.error('Failed to delete MCP server:', error)
throw error
}
}
const setMCPServerActive = async (name: string, isActive: boolean) => {
try {
await window.api.mcp.setServerActive(name, isActive)
// Main process will send back updated servers via mcp:servers-changed
} catch (error) {
console.error('Failed to set MCP server active status:', error)
throw error
}
}
const getActiveMCPServers = () => {
return mcpServers.filter((server) => server.isActive)
}
return { return {
mcpServers, mcpServers,
activedMcpServers, activedMcpServers,
addMCPServer, addMCPServer: (server: MCPServer) => dispatch(addMCPServer(server)),
updateMCPServer, updateMCPServer: (server: MCPServer) => dispatch(updateMCPServer(server)),
deleteMCPServer, deleteMCPServer: (id: string) => dispatch(deleteMCPServer(id)),
setMCPServerActive, setMCPServerActive: (server: MCPServer, isActive: boolean) => dispatch(updateMCPServer({ ...server, isActive })),
getActiveMCPServers getActiveMCPServers: () => mcpServers.filter((server) => server.isActive),
updateMcpServers: (servers: MCPServer[]) => dispatch(setMCPServers(servers))
} }
} }
export const useInitMCPServers = () => {
const mcpServers = useAppSelector((state) => state.mcp.servers)
// const dispatch = useAppDispatch()
// Send servers to main process when they change in Redux
useEffect(() => {
ipcRenderer.send('mcp:servers-from-renderer', mcpServers)
}, [mcpServers])
// Initial load of MCP servers from main process
// useEffect(() => {
// const loadServers = async () => {
// try {
// const servers = await window.api.mcp.listServers()
// dispatch(setMCPServers(servers))
// } catch (error) {
// console.error('Failed to load MCP servers:', error)
// }
// }
// loadServers()
// }, [dispatch])
}

View File

@ -961,10 +961,7 @@
"argsTooltip": "Each argument on a new line", "argsTooltip": "Each argument on a new line",
"baseUrlTooltip": "Remote server base URL", "baseUrlTooltip": "Remote server base URL",
"command": "Command", "command": "Command",
"commandRequired": "Please enter a command",
"config_description": "Configure Model Context Protocol servers", "config_description": "Configure Model Context Protocol servers",
"confirmDelete": "Delete Server",
"confirmDeleteMessage": "Are you sure you want to delete the server?",
"deleteError": "Failed to delete server", "deleteError": "Failed to delete server",
"deleteSuccess": "Server deleted successfully", "deleteSuccess": "Server deleted successfully",
"dependenciesInstall": "Install Dependencies", "dependenciesInstall": "Install Dependencies",
@ -975,7 +972,8 @@
"editServer": "Edit Server", "editServer": "Edit Server",
"env": "Environment Variables", "env": "Environment Variables",
"envTooltip": "Format: KEY=value, one per line", "envTooltip": "Format: KEY=value, one per line",
"findMore": "Find More MCP Servers", "findMore": "Find More MCP",
"searchNpx": "Search MCP",
"install": "Install", "install": "Install",
"installError": "Failed to install dependencies", "installError": "Failed to install dependencies",
"installSuccess": "Dependencies installed successfully", "installSuccess": "Dependencies installed successfully",
@ -985,8 +983,8 @@
"jsonSaveSuccess": "JSON configuration has been saved.", "jsonSaveSuccess": "JSON configuration has been saved.",
"missingDependencies": "is Missing, please install it to continue.", "missingDependencies": "is Missing, please install it to continue.",
"name": "Name", "name": "Name",
"nameRequired": "Please enter a server name",
"noServers": "No servers configured", "noServers": "No servers configured",
"newServer": "MCP Server",
"npx_list": { "npx_list": {
"actions": "Actions", "actions": "Actions",
"desc": "Search and add npm packages as MCP servers", "desc": "Search and add npm packages as MCP servers",
@ -1002,10 +1000,13 @@
"usage": "Usage", "usage": "Usage",
"version": "Version" "version": "Version"
}, },
"errors": {
"32000": "MCP server failed to start, please check the parameters according to the tutorial"
},
"serverPlural": "servers", "serverPlural": "servers",
"serverSingular": "server", "serverSingular": "server",
"title": "MCP Servers", "title": "MCP Servers",
"toggleError": "Toggle failed", "startError": "Start failed",
"type": "Type", "type": "Type",
"updateError": "Failed to update server", "updateError": "Failed to update server",
"updateSuccess": "Server updated successfully", "updateSuccess": "Server updated successfully",

View File

@ -960,10 +960,7 @@
"argsTooltip": "1行に1つの引数を入力してください", "argsTooltip": "1行に1つの引数を入力してください",
"baseUrlTooltip": "リモートURLアドレス", "baseUrlTooltip": "リモートURLアドレス",
"command": "コマンド", "command": "コマンド",
"commandRequired": "コマンドを入力してください",
"config_description": "モデルコンテキストプロトコルサーバーの設定", "config_description": "モデルコンテキストプロトコルサーバーの設定",
"confirmDelete": "サーバーを削除",
"confirmDeleteMessage": "本当にこのサーバーを削除しますか?",
"deleteError": "サーバーの削除に失敗しました", "deleteError": "サーバーの削除に失敗しました",
"deleteSuccess": "サーバーが正常に削除されました", "deleteSuccess": "サーバーが正常に削除されました",
"dependenciesInstall": "依存関係をインストール", "dependenciesInstall": "依存関係をインストール",
@ -974,7 +971,8 @@
"editServer": "サーバーを編集", "editServer": "サーバーを編集",
"env": "環境変数", "env": "環境変数",
"envTooltip": "形式: KEY=value, 1行に1つ", "envTooltip": "形式: KEY=value, 1行に1つ",
"findMore": "MCP サーバーを見つける", "findMore": "MCP を見つける",
"searchNpx": "MCP を検索",
"install": "インストール", "install": "インストール",
"installError": "依存関係のインストールに失敗しました", "installError": "依存関係のインストールに失敗しました",
"installSuccess": "依存関係のインストールに成功しました", "installSuccess": "依存関係のインストールに成功しました",
@ -984,8 +982,8 @@
"jsonSaveSuccess": "JSON設定が保存されました。", "jsonSaveSuccess": "JSON設定が保存されました。",
"missingDependencies": "が不足しています。続行するにはインストールしてください。", "missingDependencies": "が不足しています。続行するにはインストールしてください。",
"name": "名前", "name": "名前",
"nameRequired": "サーバー名を入力してください",
"noServers": "サーバーが設定されていません", "noServers": "サーバーが設定されていません",
"newServer": "MCP サーバー",
"npx_list": { "npx_list": {
"actions": "アクション", "actions": "アクション",
"desc": "npm パッケージを検索して MCP サーバーとして追加", "desc": "npm パッケージを検索して MCP サーバーとして追加",
@ -1004,11 +1002,14 @@
"serverPlural": "サーバー", "serverPlural": "サーバー",
"serverSingular": "サーバー", "serverSingular": "サーバー",
"title": "MCP サーバー", "title": "MCP サーバー",
"toggleError": "切り替えに失敗しました", "startError": "起動に失敗しました",
"type": "タイプ", "type": "タイプ",
"updateError": "サーバーの更新に失敗しました", "updateError": "サーバーの更新に失敗しました",
"updateSuccess": "サーバーが正常に更新されました", "updateSuccess": "サーバーが正常に更新されました",
"url": "URL" "url": "URL",
"errors": {
"32000": "MCP サーバーが起動しませんでした。パラメーターを確認してください"
}
}, },
"messages.divider": "メッセージ間に区切り線を表示", "messages.divider": "メッセージ間に区切り線を表示",
"messages.grid_columns": "メッセージグリッドの表示列数", "messages.grid_columns": "メッセージグリッドの表示列数",

View File

@ -960,10 +960,7 @@
"argsTooltip": "Каждый аргумент с новой строки", "argsTooltip": "Каждый аргумент с новой строки",
"baseUrlTooltip": "Адрес удаленного URL", "baseUrlTooltip": "Адрес удаленного URL",
"command": "Команда", "command": "Команда",
"commandRequired": "Пожалуйста, введите команду",
"config_description": "Настройка серверов протокола контекста модели", "config_description": "Настройка серверов протокола контекста модели",
"confirmDelete": "Удалить сервер",
"confirmDeleteMessage": "Вы уверены, что хотите удалить этот сервер?",
"deleteError": "Не удалось удалить сервер", "deleteError": "Не удалось удалить сервер",
"deleteSuccess": "Сервер успешно удален", "deleteSuccess": "Сервер успешно удален",
"dependenciesInstall": "Установить зависимости", "dependenciesInstall": "Установить зависимости",
@ -974,7 +971,8 @@
"editServer": "Редактировать сервер", "editServer": "Редактировать сервер",
"env": "Переменные окружения", "env": "Переменные окружения",
"envTooltip": "Формат: KEY=value, по одной на строку", "envTooltip": "Формат: KEY=value, по одной на строку",
"findMore": "Найти больше MCP серверов", "findMore": "Найти больше MCP",
"searchNpx": "Найти MCP",
"install": "Установить", "install": "Установить",
"installError": "Не удалось установить зависимости", "installError": "Не удалось установить зависимости",
"installSuccess": "Зависимости успешно установлены", "installSuccess": "Зависимости успешно установлены",
@ -984,8 +982,8 @@
"jsonSaveSuccess": "JSON конфигурация сохранена", "jsonSaveSuccess": "JSON конфигурация сохранена",
"missingDependencies": "отсутствует, пожалуйста, установите для продолжения.", "missingDependencies": "отсутствует, пожалуйста, установите для продолжения.",
"name": "Имя", "name": "Имя",
"nameRequired": "Пожалуйста, введите имя сервера",
"noServers": "Серверы не настроены", "noServers": "Серверы не настроены",
"newServer": "MCP сервер",
"npx_list": { "npx_list": {
"actions": "Действия", "actions": "Действия",
"desc": "Поиск и добавление npm пакетов в качестве MCP серверов", "desc": "Поиск и добавление npm пакетов в качестве MCP серверов",
@ -1001,10 +999,13 @@
"usage": "Использование", "usage": "Использование",
"version": "Версия" "version": "Версия"
}, },
"errors": {
"32000": "MCP сервер не запущен, пожалуйста, проверьте параметры"
},
"serverPlural": "серверы", "serverPlural": "серверы",
"serverSingular": "сервер", "serverSingular": "сервер",
"title": "Серверы MCP", "title": "Серверы MCP",
"toggleError": "Переключение не удалось", "startError": "Запуск не удалось",
"type": "Тип", "type": "Тип",
"updateError": "Ошибка обновления сервера", "updateError": "Ошибка обновления сервера",
"updateSuccess": "Сервер успешно обновлен", "updateSuccess": "Сервер успешно обновлен",

View File

@ -961,10 +961,7 @@
"argsTooltip": "每个参数占一行", "argsTooltip": "每个参数占一行",
"baseUrlTooltip": "远程 URL 地址", "baseUrlTooltip": "远程 URL 地址",
"command": "命令", "command": "命令",
"commandRequired": "请输入命令",
"config_description": "配置模型上下文协议服务器", "config_description": "配置模型上下文协议服务器",
"confirmDelete": "删除服务器",
"confirmDeleteMessage": "您确定要删除该服务器吗?",
"deleteError": "删除服务器失败", "deleteError": "删除服务器失败",
"deleteSuccess": "服务器删除成功", "deleteSuccess": "服务器删除成功",
"dependenciesInstall": "安装依赖项", "dependenciesInstall": "安装依赖项",
@ -975,7 +972,8 @@
"editServer": "编辑服务器", "editServer": "编辑服务器",
"env": "环境变量", "env": "环境变量",
"envTooltip": "格式KEY=value每行一个", "envTooltip": "格式KEY=value每行一个",
"findMore": "更多 MCP 服务器", "findMore": "更多 MCP",
"searchNpx": "搜索 MCP",
"install": "安装", "install": "安装",
"installError": "安装依赖项失败", "installError": "安装依赖项失败",
"installSuccess": "依赖项安装成功", "installSuccess": "依赖项安装成功",
@ -985,8 +983,8 @@
"jsonSaveSuccess": "JSON配置已保存", "jsonSaveSuccess": "JSON配置已保存",
"missingDependencies": "缺失,请安装它以继续", "missingDependencies": "缺失,请安装它以继续",
"name": "名称", "name": "名称",
"nameRequired": "请输入服务器名称",
"noServers": "未配置服务器", "noServers": "未配置服务器",
"newServer": "MCP 服务器",
"npx_list": { "npx_list": {
"actions": "操作", "actions": "操作",
"desc": "搜索并添加 npm 包作为 MCP 服务", "desc": "搜索并添加 npm 包作为 MCP 服务",
@ -1002,10 +1000,13 @@
"usage": "用法", "usage": "用法",
"version": "版本" "version": "版本"
}, },
"errors": {
"32000": "MCP 服务器启动失败,请根据教程检查参数是否填写完整"
},
"serverPlural": "服务器", "serverPlural": "服务器",
"serverSingular": "服务器", "serverSingular": "服务器",
"title": "MCP 服务器", "title": "MCP 服务器",
"toggleError": "切换失败", "startError": "启动失败",
"type": "类型", "type": "类型",
"updateError": "更新服务器失败", "updateError": "更新服务器失败",
"updateSuccess": "服务器更新成功", "updateSuccess": "服务器更新成功",

View File

@ -960,10 +960,7 @@
"argsTooltip": "每個參數佔一行", "argsTooltip": "每個參數佔一行",
"baseUrlTooltip": "遠端 URL 地址", "baseUrlTooltip": "遠端 URL 地址",
"command": "指令", "command": "指令",
"commandRequired": "請輸入指令",
"config_description": "設定模型上下文協議伺服器", "config_description": "設定模型上下文協議伺服器",
"confirmDelete": "刪除伺服器",
"confirmDeleteMessage": "您確定要刪除該伺服器嗎?",
"deleteError": "刪除伺服器失敗", "deleteError": "刪除伺服器失敗",
"deleteSuccess": "伺服器刪除成功", "deleteSuccess": "伺服器刪除成功",
"dependenciesInstall": "安裝相依套件", "dependenciesInstall": "安裝相依套件",
@ -974,7 +971,8 @@
"editServer": "編輯伺服器", "editServer": "編輯伺服器",
"env": "環境變數", "env": "環境變數",
"envTooltip": "格式KEY=value每行一個", "envTooltip": "格式KEY=value每行一個",
"findMore": "更多 MCP 伺服器", "findMore": "更多 MCP",
"searchNpx": "搜索 MCP",
"install": "安裝", "install": "安裝",
"installError": "安裝相依套件失敗", "installError": "安裝相依套件失敗",
"installSuccess": "相依套件安裝成功", "installSuccess": "相依套件安裝成功",
@ -984,8 +982,8 @@
"jsonSaveSuccess": "JSON配置已儲存", "jsonSaveSuccess": "JSON配置已儲存",
"missingDependencies": "缺失,請安裝它以繼續", "missingDependencies": "缺失,請安裝它以繼續",
"name": "名稱", "name": "名稱",
"nameRequired": "請輸入伺服器名稱",
"noServers": "未設定伺服器", "noServers": "未設定伺服器",
"newServer": "MCP 伺服器",
"npx_list": { "npx_list": {
"actions": "操作", "actions": "操作",
"desc": "搜索並添加 npm 包作為 MCP 服務", "desc": "搜索並添加 npm 包作為 MCP 服務",
@ -1001,10 +999,13 @@
"usage": "用法", "usage": "用法",
"version": "版本" "version": "版本"
}, },
"errors": {
"32000": "MCP 伺服器啟動失敗,請根據教程檢查參數是否填寫完整"
},
"serverPlural": "伺服器", "serverPlural": "伺服器",
"serverSingular": "伺服器", "serverSingular": "伺服器",
"title": "MCP 伺服器", "title": "MCP 伺服器",
"toggleError": "切換失敗", "startError": "啟動失敗",
"type": "類型", "type": "類型",
"updateError": "更新伺服器失敗", "updateError": "更新伺服器失敗",
"updateSuccess": "伺服器更新成功", "updateSuccess": "伺服器更新成功",

View File

@ -886,10 +886,7 @@
"argsTooltip": "Κάθε παράμετρος σε μια γραμμή", "argsTooltip": "Κάθε παράμετρος σε μια γραμμή",
"baseUrlTooltip": "Σύνδεσμος Απομακρυσμένης διεύθυνσης URL", "baseUrlTooltip": "Σύνδεσμος Απομακρυσμένης διεύθυνσης URL",
"command": "Εντολή", "command": "Εντολή",
"commandRequired": "Παρακαλώ εισάγετε την εντολή",
"config_description": "Ρυθμίζει το πλαίσιο συντονισμού πρωτοκόλλων διακομιστή", "config_description": "Ρυθμίζει το πλαίσιο συντονισμού πρωτοκόλλων διακομιστή",
"confirmDelete": "Διαγραφή διακομιστή",
"confirmDeleteMessage": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτόν τον διακομιστή;",
"deleteError": "Αποτυχία διαγραφής διακομιστή", "deleteError": "Αποτυχία διαγραφής διακομιστή",
"deleteSuccess": "Ο διακομιστής διαγράφηκε επιτυχώς", "deleteSuccess": "Ο διακομιστής διαγράφηκε επιτυχώς",
"dependenciesInstall": "Εγκατάσταση εξαρτήσεων", "dependenciesInstall": "Εγκατάσταση εξαρτήσεων",
@ -910,7 +907,6 @@
"jsonSaveSuccess": "Η διαμορφωτική ρύθμιση JSON αποθηκεύτηκε επιτυχώς", "jsonSaveSuccess": "Η διαμορφωτική ρύθμιση JSON αποθηκεύτηκε επιτυχώς",
"missingDependencies": "Απο缺失, παρακαλώ εγκαταστήστε το για να συνεχίσετε", "missingDependencies": "Απο缺失, παρακαλώ εγκαταστήστε το για να συνεχίσετε",
"name": "Όνομα", "name": "Όνομα",
"nameRequired": "Παρακαλώ εισάγετε το όνομα του διακομιστή",
"noServers": "Δεν έχουν ρυθμιστεί διακομιστές", "noServers": "Δεν έχουν ρυθμιστεί διακομιστές",
"npx_list": { "npx_list": {
"actions": "Ενέργειες", "actions": "Ενέργειες",

View File

@ -886,10 +886,7 @@
"argsTooltip": "Cada argumento en una línea", "argsTooltip": "Cada argumento en una línea",
"baseUrlTooltip": "Dirección URL remota", "baseUrlTooltip": "Dirección URL remota",
"command": "Comando", "command": "Comando",
"commandRequired": "Por favor ingrese el comando",
"config_description": "Configurar modelo de contexto del protocolo del servidor", "config_description": "Configurar modelo de contexto del protocolo del servidor",
"confirmDelete": "Eliminar servidor",
"confirmDeleteMessage": "¿Está seguro de que desea eliminar este servidor?",
"deleteError": "Fallo al eliminar servidor", "deleteError": "Fallo al eliminar servidor",
"deleteSuccess": "Servidor eliminado exitosamente", "deleteSuccess": "Servidor eliminado exitosamente",
"dependenciesInstall": "Instalar dependencias", "dependenciesInstall": "Instalar dependencias",
@ -910,7 +907,6 @@
"jsonSaveSuccess": "Configuración JSON guardada exitosamente", "jsonSaveSuccess": "Configuración JSON guardada exitosamente",
"missingDependencies": "Faltan, instalelas para continuar", "missingDependencies": "Faltan, instalelas para continuar",
"name": "Nombre", "name": "Nombre",
"nameRequired": "Por favor ingrese el nombre del servidor",
"noServers": "No se han configurado servidores", "noServers": "No se han configurado servidores",
"npx_list": { "npx_list": {
"actions": "Acciones", "actions": "Acciones",

View File

@ -886,10 +886,7 @@
"argsTooltip": "Chaque argument sur une ligne", "argsTooltip": "Chaque argument sur une ligne",
"baseUrlTooltip": "Adresse URL distante", "baseUrlTooltip": "Adresse URL distante",
"command": "Commande", "command": "Commande",
"commandRequired": "Veuillez entrer une commande",
"config_description": "Configurer le modèle du protocole de contexte du serveur", "config_description": "Configurer le modèle du protocole de contexte du serveur",
"confirmDelete": "Supprimer le serveur",
"confirmDeleteMessage": "Êtes-vous sûr de vouloir supprimer ce serveur ?",
"deleteError": "Échec de la suppression du serveur", "deleteError": "Échec de la suppression du serveur",
"deleteSuccess": "Serveur supprimé avec succès", "deleteSuccess": "Serveur supprimé avec succès",
"dependenciesInstall": "Installer les dépendances", "dependenciesInstall": "Installer les dépendances",
@ -910,7 +907,6 @@
"jsonSaveSuccess": "Configuration JSON sauvegardée", "jsonSaveSuccess": "Configuration JSON sauvegardée",
"missingDependencies": "Manquantes, veuillez les installer pour continuer", "missingDependencies": "Manquantes, veuillez les installer pour continuer",
"name": "Nom", "name": "Nom",
"nameRequired": "Veuillez entrer le nom du serveur",
"noServers": "Aucun serveur configuré", "noServers": "Aucun serveur configuré",
"npx_list": { "npx_list": {
"actions": "Actions", "actions": "Actions",

View File

@ -886,10 +886,7 @@
"argsTooltip": "Cada argumento em uma linha", "argsTooltip": "Cada argumento em uma linha",
"baseUrlTooltip": "Endereço de URL remoto", "baseUrlTooltip": "Endereço de URL remoto",
"command": "Comando", "command": "Comando",
"commandRequired": "Digite o comando",
"config_description": "Configurar modelo de protocolo de contexto do servidor", "config_description": "Configurar modelo de protocolo de contexto do servidor",
"confirmDelete": "Excluir servidor",
"confirmDeleteMessage": "Tem certeza de que deseja excluir este servidor?",
"deleteError": "Falha ao excluir servidor", "deleteError": "Falha ao excluir servidor",
"deleteSuccess": "Servidor excluído com sucesso", "deleteSuccess": "Servidor excluído com sucesso",
"dependenciesInstall": "Instalar dependências", "dependenciesInstall": "Instalar dependências",
@ -910,7 +907,6 @@
"jsonSaveSuccess": "Configuração JSON salva com sucesso", "jsonSaveSuccess": "Configuração JSON salva com sucesso",
"missingDependencies": "Ausente, instale para continuar", "missingDependencies": "Ausente, instale para continuar",
"name": "Nome", "name": "Nome",
"nameRequired": "Digite o nome do servidor",
"noServers": "Nenhum servidor configurado", "noServers": "Nenhum servidor configurado",
"npx_list": { "npx_list": {
"actions": "Ações", "actions": "Ações",

View File

@ -618,9 +618,9 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
const toggelEnableMCP = (mcp: MCPServer) => { const toggelEnableMCP = (mcp: MCPServer) => {
setEnabledMCPs((prev) => { setEnabledMCPs((prev) => {
const exists = prev.some((item) => item.name === mcp.name) const exists = prev.some((item) => item.id === mcp.id)
if (exists) { if (exists) {
return prev.filter((item) => item.name !== mcp.name) return prev.filter((item) => item.id !== mcp.id)
} else { } else {
return [...prev, mcp] return [...prev, mcp]
} }

View File

@ -27,19 +27,14 @@ const MCPToolsButton: FC<Props> = ({ enabledMCPs, toggelEnableMCP, ToolbarButton
// Check if all active servers are enabled // Check if all active servers are enabled
const activeServers = mcpServers.filter((s) => s.isActive) const activeServers = mcpServers.filter((s) => s.isActive)
const anyEnable = activeServers.some((server) => const anyEnable = activeServers.some((server) => enabledMCPs.some((enabledServer) => enabledServer.id === server.id))
enabledMCPs.some((enabledServer) => enabledServer.name === server.name)
)
const enableAll = () => const enableAll = () => mcpServers.forEach(toggelEnableMCP)
mcpServers.forEach((s) => {
toggelEnableMCP(s)
})
const disableAll = () => const disableAll = () =>
mcpServers.forEach((s) => { mcpServers.forEach((s) => {
enabledMCPs.forEach((enabledServer) => { enabledMCPs.forEach((enabledServer) => {
if (enabledServer.name === s.name) { if (enabledServer.id === s.id) {
toggelEnableMCP(s) toggelEnableMCP(s)
} }
}) })
@ -64,32 +59,34 @@ const MCPToolsButton: FC<Props> = ({ enabledMCPs, toggelEnableMCP, ToolbarButton
</div> </div>
</div> </div>
</DropdownHeader> </DropdownHeader>
{mcpServers.length > 0 ? ( <DropdownBody>
mcpServers {mcpServers.length > 0 ? (
.filter((s) => s.isActive) mcpServers
.map((server) => ( .filter((s) => s.isActive)
<McpServerItems key={server.name} className="ant-dropdown-menu-item"> .map((server) => (
<div className="server-info"> <McpServerItems key={server.id} className="ant-dropdown-menu-item">
<div className="server-name">{server.name}</div> <div className="server-info">
{server.description && ( <div className="server-name">{server.name}</div>
<Tooltip title={server.description} placement="bottom"> {server.description && (
<div className="server-description">{truncateText(server.description)}</div> <Tooltip title={server.description} placement="bottom">
</Tooltip> <div className="server-description">{truncateText(server.description)}</div>
)} </Tooltip>
{server.baseUrl && <div className="server-url">{server.baseUrl}</div>} )}
</div> {server.baseUrl && <div className="server-url">{server.baseUrl}</div>}
<Switch </div>
size="small" <Switch
checked={enabledMCPs.some((s) => s.name === server.name)} size="small"
onChange={() => toggelEnableMCP(server)} checked={enabledMCPs.some((s) => s.id === server.id)}
/> onChange={() => toggelEnableMCP(server)}
</McpServerItems> />
)) </McpServerItems>
) : ( ))
<div className="ant-dropdown-menu-item-group"> ) : (
<div className="ant-dropdown-menu-item no-results">{t('settings.mcp.noServers')}</div> <div className="ant-dropdown-menu-item-group">
</div> <div className="ant-dropdown-menu-item no-results">{t('settings.mcp.noServers')}</div>
)} </div>
)}
</DropdownBody>
</div> </div>
) )
@ -106,7 +103,7 @@ const MCPToolsButton: FC<Props> = ({ enabledMCPs, toggelEnableMCP, ToolbarButton
overlayClassName="mention-models-dropdown"> overlayClassName="mention-models-dropdown">
<Tooltip placement="top" title={t('settings.mcp.title')} arrow> <Tooltip placement="top" title={t('settings.mcp.title')} arrow>
<ToolbarButton type="text" ref={dropdownRef}> <ToolbarButton type="text" ref={dropdownRef}>
<CodeOutlined style={{ color: enabledMCPs.length > 0 ? '#d97757' : 'var(--color-icon)' }} /> <CodeOutlined style={{ color: enabledMCPs.length > 0 ? 'var(--color-primary)' : 'var(--color-icon)' }} />
</ToolbarButton> </ToolbarButton>
</Tooltip> </Tooltip>
</Dropdown> </Dropdown>
@ -127,6 +124,10 @@ const McpServerItems = styled.div`
font-weight: 500; font-weight: 500;
font-size: 14px; font-size: 14px;
color: var(--color-text-1); color: var(--color-text-1);
max-width: 400px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
} }
.server-description { .server-description {
@ -177,4 +178,8 @@ const DropdownHeader = styled.div`
} }
` `
const DropdownBody = styled.div`
padding-bottom: 10px;
`
export default MCPToolsButton export default MCPToolsButton

View File

@ -100,7 +100,7 @@ const MessageTools: FC<Props> = ({ message }) => {
</MessageTitleLabel> </MessageTitleLabel>
), ),
children: isDone && result && ( children: isDone && result && (
<ToolResponseContainer style={{ fontFamily, fontSize }}> <ToolResponseContainer style={{ fontFamily, fontSize: '12px' }}>
<pre>{JSON.stringify(result, null, 2)}</pre> <pre>{JSON.stringify(result, null, 2)}</pre>
</ToolResponseContainer> </ToolResponseContainer>
) )
@ -129,9 +129,8 @@ const MessageTools: FC<Props> = ({ message }) => {
onCancel={() => setExpandedResponse(null)} onCancel={() => setExpandedResponse(null)}
footer={null} footer={null}
width="80%" width="80%"
styles={{ centered
body: { maxHeight: '80vh', overflow: 'auto' } styles={{ body: { maxHeight: '80vh', overflow: 'auto' } }}>
}}>
{expandedResponse && ( {expandedResponse && (
<ExpandedResponseContainer style={{ fontFamily, fontSize }}> <ExpandedResponseContainer style={{ fontFamily, fontSize }}>
<ActionButton <ActionButton
@ -157,7 +156,6 @@ const CollapseContainer = styled(Collapse)`
margin-bottom: 15px; margin-bottom: 15px;
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
.ant-collapse-header { .ant-collapse-header {
background-color: var(--color-bg-2); background-color: var(--color-bg-2);

View File

@ -3,7 +3,7 @@ import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import MinAppsPopover from '@renderer/components/Popups/MinAppsPopover' import MinAppsPopover from '@renderer/components/Popups/MinAppsPopover'
import SearchPopup from '@renderer/components/Popups/SearchPopup' import SearchPopup from '@renderer/components/Popups/SearchPopup'
import { isMac, isWindows } from '@renderer/config/constant' import { isMac } from '@renderer/config/constant'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
import { modelGenerating } from '@renderer/hooks/useRuntime' import { modelGenerating } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
@ -71,9 +71,7 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
</Tooltip> </Tooltip>
</NavbarLeft> </NavbarLeft>
)} )}
<NavbarRight <NavbarRight style={{ justifyContent: 'space-between', flex: 1 }} className="home-navbar-right">
style={{ justifyContent: 'space-between', paddingRight: isWindows ? 140 : 12, flex: 1 }}
className="home-navbar-right">
<HStack alignItems="center"> <HStack alignItems="center">
{!showAssistants && ( {!showAssistants && (
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={0.8}> <Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={0.8}>

View File

@ -6,13 +6,12 @@ import {
SearchOutlined, SearchOutlined,
SettingOutlined SettingOutlined
} from '@ant-design/icons' } from '@ant-design/icons'
import { Navbar, NavbarCenter, NavbarRight as NavbarRightFromComponents } from '@renderer/components/app/Navbar' import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navbar'
import DragableList from '@renderer/components/DragableList' import DragableList from '@renderer/components/DragableList'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import ListItem from '@renderer/components/ListItem' import ListItem from '@renderer/components/ListItem'
import PromptPopup from '@renderer/components/Popups/PromptPopup' import PromptPopup from '@renderer/components/Popups/PromptPopup'
import Scrollbar from '@renderer/components/Scrollbar' import Scrollbar from '@renderer/components/Scrollbar'
import { isWindows } from '@renderer/config/constant'
import { useKnowledgeBases } from '@renderer/hooks/useKnowledge' import { useKnowledgeBases } from '@renderer/hooks/useKnowledge'
import { useShortcut } from '@renderer/hooks/useShortcuts' import { useShortcut } from '@renderer/hooks/useShortcuts'
import { NavbarIcon } from '@renderer/pages/home/Navbar' import { NavbarIcon } from '@renderer/pages/home/Navbar'
@ -252,9 +251,4 @@ const NarrowIcon = styled(NavbarIcon)`
} }
` `
const NavbarRight = styled(NavbarRightFromComponents)`
min-width: auto;
padding-right: ${isWindows ? '140px' : 15};
`
export default KnowledgePage export default KnowledgePage

View File

@ -1,241 +0,0 @@
import { TopView } from '@renderer/components/TopView'
import { useAppSelector } from '@renderer/store'
import { MCPServer } from '@renderer/types'
import { Form, Input, Modal, Radio, Switch } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
interface ShowParams {
server?: MCPServer
create?: boolean
}
interface Props extends ShowParams {
resolve: (data: any) => void
}
interface MCPFormValues {
name: string
description?: string
serverType: 'sse' | 'stdio'
baseUrl?: string
command?: string
args?: string
env?: string
isActive: boolean
}
const PopupContainer: React.FC<Props> = ({ server, create, resolve }) => {
const [open, setOpen] = useState(true)
const { t } = useTranslation()
const [serverType, setServerType] = useState<'sse' | 'stdio'>('stdio')
const mcpServers = useAppSelector((state) => state.mcp.servers)
const [form] = Form.useForm<MCPFormValues>()
const [loading, setLoading] = useState(false)
useEffect(() => {
if (server) {
// Determine server type based on server properties
const serverType = server.baseUrl ? 'sse' : 'stdio'
setServerType(serverType)
form.setFieldsValue({
name: server.name,
description: server.description,
serverType: serverType,
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
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// Watch the serverType field to update the form layout dynamically
useEffect(() => {
const type = form.getFieldValue('serverType')
type && setServerType(type)
}, [form])
const onOK = async () => {
setLoading(true)
try {
const values = await form.validateFields()
const mcpServer: MCPServer = {
name: values.name,
description: values.description,
isActive: values.isActive
}
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() !== '') : []
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()
}
}
})
}
mcpServer.env = Object.keys(env).length > 0 ? env : undefined
}
if (server && !create) {
try {
await window.api.mcp.updateServer(mcpServer)
window.message.success(t('settings.mcp.updateSuccess'))
setLoading(false)
setOpen(false)
form.resetFields()
} catch (error: any) {
window.message.error(`${t('settings.mcp.updateError')}: ${error.message}`)
setLoading(false)
}
} else {
// Check for duplicate name
if (mcpServers.some((server: MCPServer) => server.name === mcpServer.name)) {
window.message.error(t('settings.mcp.duplicateName'))
setLoading(false)
return
}
try {
await window.api.mcp.addServer(mcpServer)
window.message.success(t('settings.mcp.addSuccess'))
setLoading(false)
setOpen(false)
form.resetFields()
} catch (error: any) {
window.message.error(`${t('settings.mcp.addError')}: ${error.message}`)
setLoading(false)
}
}
} catch (error: any) {
setLoading(false)
}
}
const onCancel = () => {
setOpen(false)
}
const onClose = () => {
resolve({})
}
AddMcpServerPopup.hide = onCancel
return (
<Modal
title={server ? t('settings.mcp.editServer') : t('settings.mcp.addServer')}
open={open}
onOk={onOK}
onCancel={onCancel}
afterClose={onClose}
confirmLoading={loading}
maskClosable={false}
width={600}
transitionName="ant-move-down"
centered
styles={{
body: {
maxHeight: '70vh',
overflowY: 'auto'
}
}}>
<Form form={form} layout="vertical">
<Form.Item
name="name"
label={t('settings.mcp.name')}
rules={[{ required: true, message: t('settings.mcp.nameRequired') }]}>
<Input disabled={!!server} placeholder={t('common.name')} />
</Form.Item>
<Form.Item name="description" label={t('settings.mcp.description')}>
<TextArea rows={2} placeholder={t('common.description')} />
</Form.Item>
<Form.Item name="serverType" label={t('settings.mcp.type')} rules={[{ required: true }]} initialValue="stdio">
<Radio.Group
onChange={(e) => setServerType(e.target.value)}
options={[
{ label: 'SSE (Server-Sent Events)', value: 'sse' },
{ label: 'STDIO (Standard Input/Output)', value: 'stdio' }
]}
/>
</Form.Item>
{serverType === 'sse' && (
<Form.Item
name="baseUrl"
label={t('settings.mcp.url')}
rules={[{ required: serverType === 'sse', message: t('settings.mcp.baseUrlRequired') }]}
tooltip={t('settings.mcp.baseUrlTooltip')}>
<Input placeholder="http://localhost:3000/sse" />
</Form.Item>
)}
{serverType === 'stdio' && (
<>
<Form.Item
name="command"
label={t('settings.mcp.command')}
rules={[{ required: serverType === 'stdio', message: t('settings.mcp.commandRequired') }]}>
<Input placeholder="uvx or npx" />
</Form.Item>
<Form.Item name="args" label={t('settings.mcp.args')} tooltip={t('settings.mcp.argsTooltip')}>
<TextArea rows={3} placeholder={`arg1\narg2`} style={{ fontFamily: 'monospace' }} />
</Form.Item>
<Form.Item name="env" label={t('settings.mcp.env')} tooltip={t('settings.mcp.envTooltip')}>
<TextArea rows={3} placeholder={`KEY1=value1\nKEY2=value2`} style={{ fontFamily: 'monospace' }} />
</Form.Item>
</>
)}
<Form.Item name="isActive" label={t('settings.mcp.active')} valuePropName="checked" initialValue={true}>
<Switch />
</Form.Item>
</Form>
</Modal>
)
}
const TopViewKey = 'AddMcpServerPopup'
export default class AddMcpServerPopup {
static topviewId = 0
static hide() {
TopView.hide(TopViewKey)
}
static show(props: ShowParams = {}) {
return new Promise<any>((resolve) => {
TopView.show(
<PopupContainer
{...props}
resolve={(v) => {
resolve(v)
TopView.hide(TopViewKey)
}}
/>,
TopViewKey
)
})
}
}

View File

@ -1,152 +0,0 @@
import { TopView } from '@renderer/components/TopView'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setMCPServers } from '@renderer/store/mcp'
import { MCPServer } from '@renderer/types'
import { Modal, Typography } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
interface Props {
resolve: (data: any) => void
}
const PopupContainer: React.FC<Props> = ({ resolve }) => {
const [open, setOpen] = useState(true)
const [jsonConfig, setJsonConfig] = useState('')
const [jsonSaving, setJsonSaving] = useState(false)
const [jsonError, setJsonError] = useState('')
const mcpServers = useAppSelector((state) => state.mcp.servers)
const dispatch = useAppDispatch()
const { t } = useTranslation()
const ipcRenderer = window.electron.ipcRenderer
useEffect(() => {
try {
const mcpServersObj: Record<string, any> = {}
mcpServers.forEach((server) => {
const { name, ...serverData } = server
mcpServersObj[name] = serverData
})
const standardFormat = {
mcpServers: mcpServersObj
}
const formattedJson = JSON.stringify(standardFormat, null, 2)
setJsonConfig(formattedJson)
setJsonError('')
} catch (error) {
console.error('Failed to format JSON:', error)
setJsonError(t('settings.mcp.jsonFormatError'))
}
}, [mcpServers, t])
const onOk = async () => {
setJsonSaving(true)
try {
if (!jsonConfig.trim()) {
dispatch(setMCPServers([]))
window.message.success(t('settings.mcp.jsonSaveSuccess'))
setJsonError('')
setJsonSaving(false)
return
}
const parsedConfig = JSON.parse(jsonConfig)
if (!parsedConfig.mcpServers || typeof parsedConfig.mcpServers !== 'object') {
throw new Error(t('settings.mcp.invalidMcpFormat'))
}
const serversArray: MCPServer[] = []
for (const [name, serverConfig] of Object.entries(parsedConfig.mcpServers)) {
const server: MCPServer = {
name,
isActive: false,
...(serverConfig as any)
}
serversArray.push(server)
}
dispatch(setMCPServers(serversArray))
ipcRenderer.send('mcp:servers-from-renderer', mcpServers)
window.message.success(t('settings.mcp.jsonSaveSuccess'))
setJsonError('')
setOpen(false)
} catch (error: any) {
console.error('Failed to save JSON config:', error)
setJsonError(error.message || t('settings.mcp.jsonSaveError'))
window.message.error(t('settings.mcp.jsonSaveError'))
} finally {
setJsonSaving(false)
}
}
const onCancel = () => {
setOpen(false)
}
const onClose = () => {
resolve({})
}
EditMcpJsonPopup.hide = onCancel
return (
<Modal
title={t('settings.mcp.editJson')}
open={open}
onOk={onOk}
onCancel={onCancel}
afterClose={onClose}
width={800}
height="80vh"
loading={jsonSaving}
transitionName="ant-move-down"
centered>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
<Typography.Text type="secondary">
{jsonError ? <span style={{ color: 'red' }}>{jsonError}</span> : ''}
</Typography.Text>
</div>
<TextArea
value={jsonConfig}
onChange={(e) => setJsonConfig(e.target.value)}
style={{
width: '100%',
fontFamily: 'monospace',
minHeight: '60vh',
marginBottom: '16px'
}}
onFocus={() => setJsonError('')}
/>
<Typography.Text type="secondary">{t('settings.mcp.jsonModeHint')}</Typography.Text>
</Modal>
)
}
const TopViewKey = 'EditMcpJsonPopup'
export default class EditMcpJsonPopup {
static topviewId = 0
static hide() {
TopView.hide(TopViewKey)
}
static show() {
return new Promise<any>((resolve) => {
TopView.show(
<PopupContainer
resolve={(v) => {
resolve(v)
TopView.hide(TopViewKey)
}}
/>,
TopViewKey
)
})
}
}

View File

@ -27,7 +27,7 @@ const InstallNpxUv: FC = () => {
setIsUvInstalled(true) setIsUvInstalled(true)
setIsInstallingUv(false) setIsInstallingUv(false)
} catch (error: any) { } catch (error: any) {
window.message.error(`${t('settings.mcp.installError')}: ${error.message}`) window.message.error({ content: `${t('settings.mcp.installError')}: ${error.message}`, key: 'mcp-install-error' })
setIsInstallingUv(false) setIsInstallingUv(false)
checkBinaries() checkBinaries()
} }
@ -40,7 +40,10 @@ const InstallNpxUv: FC = () => {
setIsBunInstalled(true) setIsBunInstalled(true)
setIsInstallingBun(false) setIsInstallingBun(false)
} catch (error: any) { } catch (error: any) {
window.message.error(`${t('settings.mcp.installError')}: ${error.message}`) window.message.error({
content: `${t('settings.mcp.installError')}: ${error.message}`,
key: 'mcp-install-error'
})
setIsInstallingBun(false) setIsInstallingBun(false)
checkBinaries() checkBinaries()
} }
@ -63,7 +66,7 @@ const InstallNpxUv: FC = () => {
style={{ padding: 8 }} style={{ padding: 8 }}
description={ description={
<SettingRow> <SettingRow>
<SettingSubtitle style={{ margin: 0 }}> <SettingSubtitle style={{ margin: 0, fontWeight: 'normal' }}>
{isUvInstalled ? 'UV Installed' : `UV ${t('settings.mcp.missingDependencies')}`} {isUvInstalled ? 'UV Installed' : `UV ${t('settings.mcp.missingDependencies')}`}
</SettingSubtitle> </SettingSubtitle>
<Button <Button
@ -85,7 +88,7 @@ const InstallNpxUv: FC = () => {
style={{ padding: 8 }} style={{ padding: 8 }}
description={ description={
<SettingRow> <SettingRow>
<SettingSubtitle style={{ margin: 0 }}> <SettingSubtitle style={{ margin: 0, fontWeight: 'normal' }}>
{isBunInstalled ? 'Bun Installed' : `Bun ${t('settings.mcp.missingDependencies')}`} {isBunInstalled ? 'Bun Installed' : `Bun ${t('settings.mcp.missingDependencies')}`}
</SettingSubtitle> </SettingSubtitle>
<Button <Button

View File

@ -0,0 +1,241 @@
import { useMCPServers } from '@renderer/hooks/useMCPServers'
import { MCPServer } from '@renderer/types'
import { Button, Flex, Form, Input, Radio, Switch } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { SettingContainer, SettingDivider, SettingTitle } from '..'
import InstallNpxUv from './InstallNpxUv'
interface Props {
server: MCPServer
}
interface MCPFormValues {
name: string
description?: string
serverType: 'sse' | 'stdio'
baseUrl?: string
command?: string
args?: string
env?: string
isActive: boolean
}
const McpSettings: React.FC<Props> = ({ server }) => {
const { t } = useTranslation()
const [serverType, setServerType] = useState<'sse' | 'stdio'>('stdio')
const [form] = Form.useForm<MCPFormValues>()
const [loading, setLoading] = useState(false)
const [isFormChanged, setIsFormChanged] = useState(false)
const [loadingServer, setLoadingServer] = useState<string | null>(null)
const { updateMCPServer } = useMCPServers()
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])
useEffect(() => {
const serverType = server.baseUrl ? 'sse' : 'stdio'
setServerType(serverType)
form.setFieldsValue({
name: server.name,
description: server.description,
serverType: serverType,
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')
: ''
})
}, [form, server])
// Watch the serverType field to update the form layout dynamically
useEffect(() => {
const type = form.getFieldValue('serverType')
type && setServerType(type)
}, [form])
const onSave = async () => {
setLoading(true)
try {
const values = await form.validateFields()
const mcpServer: MCPServer = {
id: server.id,
name: values.name,
description: values.description,
isActive: values.isActive
}
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() !== '') : []
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()
}
}
})
}
mcpServer.env = Object.keys(env).length > 0 ? env : undefined
}
try {
await window.api.mcp.listTools(mcpServer)
updateMCPServer({ ...mcpServer, isActive: true })
window.message.success({ content: t('settings.mcp.updateSuccess'), key: 'mcp-update-success' })
setLoading(false)
setIsFormChanged(false)
} catch (error: any) {
updateMCPServer({ ...mcpServer, isActive: false })
window.modal.error({
title: t('settings.mcp.updateError'),
content: error.message,
centered: true
})
setLoading(false)
}
} catch (error: any) {
setLoading(false)
}
}
const onFormValuesChange = () => {
setIsFormChanged(true)
}
const formatError = (error: any) => {
if (error.message.includes('32000')) {
return t('settings.mcp.errors.32000')
}
return error.message
}
const onToggleActive = async (active: boolean) => {
await form.validateFields()
setLoadingServer(server.id)
try {
if (active) {
await window.api.mcp.listTools(server)
}
updateMCPServer({ ...server, isActive: active })
} catch (error: any) {
window.modal.error({
title: t('settings.mcp.startError'),
content: formatError(error),
centered: true
})
console.error('[MCP] Error toggling server active', error)
} finally {
setLoadingServer(null)
}
}
return (
<SettingContainer style={{ background: 'transparent' }}>
<InstallNpxUv />
<SettingTitle>
<Flex align="center" gap={8}>
<ServerName>{server?.name}</ServerName>
</Flex>
<Switch
value={server.isActive}
key={server.id}
loading={loadingServer === server.id}
onChange={onToggleActive}
/>
</SettingTitle>
<SettingDivider />
<Form form={form} layout="vertical" onValuesChange={onFormValuesChange}>
<Form.Item name="name" label={t('settings.mcp.name')} rules={[{ required: true, message: '' }]}>
<Input placeholder={t('common.name')} />
</Form.Item>
<Form.Item name="description" label={t('settings.mcp.description')}>
<TextArea rows={2} placeholder={t('common.description')} />
</Form.Item>
<Form.Item name="serverType" label={t('settings.mcp.type')} rules={[{ required: true }]} initialValue="stdio">
<Radio.Group
onChange={(e) => setServerType(e.target.value)}
options={[
{ label: 'SSE', value: 'sse' },
{ label: 'STDIO', value: 'stdio' }
]}
/>
</Form.Item>
{serverType === 'sse' && (
<Form.Item
name="baseUrl"
label={t('settings.mcp.url')}
rules={[{ required: serverType === 'sse', message: '' }]}
tooltip={t('settings.mcp.baseUrlTooltip')}>
<Input placeholder="http://localhost:3000/sse" />
</Form.Item>
)}
{serverType === 'stdio' && (
<>
<Form.Item
name="command"
label={t('settings.mcp.command')}
rules={[{ required: serverType === 'stdio', message: '' }]}>
<Input placeholder="uvx or npx" />
</Form.Item>
<Form.Item
name="args"
label={t('settings.mcp.args')}
tooltip={t('settings.mcp.argsTooltip')}
rules={[{ required: serverType === 'stdio', message: '' }]}>
<TextArea rows={3} placeholder={`arg1\narg2`} style={{ fontFamily: 'monospace' }} />
</Form.Item>
<Form.Item name="env" label={t('settings.mcp.env')} tooltip={t('settings.mcp.envTooltip')}>
<TextArea rows={3} placeholder={`KEY1=value1\nKEY2=value2`} style={{ fontFamily: 'monospace' }} />
</Form.Item>
</>
)}
<Button type="primary" onClick={onSave} loading={loading} disabled={!isFormChanged}>
{t('common.save')}
</Button>
</Form>
</SettingContainer>
)
}
const ServerName = styled.span`
font-size: 14px;
font-weight: 500;
`
export default McpSettings

View File

@ -1,13 +1,15 @@
import { SearchOutlined } from '@ant-design/icons' import { PlusOutlined, SearchOutlined } from '@ant-design/icons'
import { nanoid } from '@reduxjs/toolkit'
import { HStack } from '@renderer/components/Layout'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { useMCPServers } from '@renderer/hooks/useMCPServers'
import type { MCPServer } from '@renderer/types' import type { MCPServer } from '@renderer/types'
import { Button, Input, Space, Spin, Table, Typography } from 'antd' import { Button, Input, Space, Spin, Table, Tag, Typography } from 'antd'
import { npxFinder } from 'npx-scope-finder' import { npxFinder } from 'npx-scope-finder'
import { type FC, useState } from 'react' import { type FC, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { SettingDivider, SettingGroup, SettingTitle } from '..' import { SettingDivider, SettingGroup, SettingTitle } from '..'
import AddMcpServerPopup from './AddMcpServerPopup'
interface SearchResult { interface SearchResult {
name: string name: string
@ -18,6 +20,8 @@ interface SearchResult {
fullName: string fullName: string
} }
const npmScopes = ['@mcpmarket', '@modelcontextprotocol', '@gongrzhe']
const NpxSearch: FC = () => { const NpxSearch: FC = () => {
const { theme } = useTheme() const { theme } = useTheme()
const { t } = useTranslation() const { t } = useTranslation()
@ -27,11 +31,16 @@ const NpxSearch: FC = () => {
const [npmScope, setNpmScope] = useState('@modelcontextprotocol') const [npmScope, setNpmScope] = useState('@modelcontextprotocol')
const [searchLoading, setSearchLoading] = useState(false) const [searchLoading, setSearchLoading] = useState(false)
const [searchResults, setSearchResults] = useState<SearchResult[]>([]) const [searchResults, setSearchResults] = useState<SearchResult[]>([])
const { addMCPServer } = useMCPServers()
// Add new function to handle npm scope search // Add new function to handle npm scope search
const handleNpmSearch = async () => { const handleNpmSearch = async () => {
if (!npmScope.trim()) { if (!npmScope.trim()) {
window.message.warning(t('settings.mcp.npx_list.scope_required')) window.message.warning({ content: t('settings.mcp.npx_list.scope_required'), key: 'mcp-npx-scope-required' })
return
}
if (searchLoading) {
return return
} }
@ -45,7 +54,7 @@ const NpxSearch: FC = () => {
const formattedResults = packages.map((pkg) => { const formattedResults = packages.map((pkg) => {
return { return {
key: pkg.name, key: pkg.name,
name: pkg.name || '', name: pkg.name?.split('/')[1] || '',
description: pkg.description || 'No description available', description: pkg.description || 'No description available',
version: pkg.version || 'Latest', version: pkg.version || 'Latest',
usage: `npx ${pkg.name}`, usage: `npx ${pkg.name}`,
@ -57,13 +66,16 @@ const NpxSearch: FC = () => {
setSearchResults(formattedResults) setSearchResults(formattedResults)
if (formattedResults.length === 0) { if (formattedResults.length === 0) {
window.message.info(t('settings.mcp.npx_list.no_packages')) window.message.info({ content: t('settings.mcp.npx_list.no_packages'), key: 'mcp-npx-no-packages' })
} }
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {
window.message.error(`${t('settings.mcp.npx_list.search_error')}: ${error.message}`) window.message.error({
content: `${t('settings.mcp.npx_list.search_error')}: ${error.message}`,
key: 'mcp-npx-search-error'
})
} else { } else {
window.message.error(t('settings.mcp.npx_list.search_error')) window.message.error({ content: t('settings.mcp.npx_list.search_error'), key: 'mcp-npx-search-error' })
} }
} finally { } finally {
setSearchLoading(false) setSearchLoading(false)
@ -91,6 +103,22 @@ const NpxSearch: FC = () => {
</Button> </Button>
</Space.Compact> </Space.Compact>
<HStack alignItems="center" mt="-5px" mb="5px">
{npmScopes.map((scope) => (
<Tag
key={scope}
onClick={() => {
if (!searchLoading) {
setNpmScope(scope)
setTimeout(handleNpmSearch, 100)
}
}}
style={{ cursor: searchLoading ? 'not-allowed' : 'pointer' }}>
{scope}
</Tag>
))}
</HStack>
{searchLoading ? ( {searchLoading ? (
<div style={{ textAlign: 'center', padding: '20px' }}> <div style={{ textAlign: 'center', padding: '20px' }}>
<Spin /> <Spin />
@ -127,31 +155,32 @@ const NpxSearch: FC = () => {
title: t('settings.mcp.npx_list.version'), title: t('settings.mcp.npx_list.version'),
dataIndex: 'version', dataIndex: 'version',
key: 'version', key: 'version',
width: '100px' width: '100px',
align: 'center'
}, },
{ {
title: t('settings.mcp.npx_list.actions'), title: t('settings.mcp.npx_list.actions'),
key: 'actions', key: 'actions',
width: '120px', width: '80px',
align: 'center',
render: (_, record: SearchResult) => ( render: (_, record: SearchResult) => (
<Button <Button
type="primary" type="primary"
icon={<PlusOutlined />}
size="small" size="small"
onClick={() => { onClick={() => {
// 创建一个临时的 MCP 服务器对象 // 创建一个临时的 MCP 服务器对象
const tempServer: MCPServer = { const tempServer: MCPServer = {
id: nanoid(),
name: record.name, name: record.name,
description: `${record.description}\n\n${t('settings.mcp.npx_list.usage')}: ${record.usage}\n${t('settings.mcp.npx_list.npm')}: ${record.npmLink}`, description: `${record.description}\n\n${t('settings.mcp.npx_list.usage')}: ${record.usage}\n${t('settings.mcp.npx_list.npm')}: ${record.npmLink}`,
command: 'npx', command: 'npx',
args: ['-y', record.fullName], args: ['-y', record.fullName],
isActive: true isActive: false
} }
addMCPServer(tempServer)
// 使用 showEditModal 函数设置表单值并显示弹窗 }}
AddMcpServerPopup.show({ server: tempServer, create: true }) />
}}>
{t('settings.mcp.addServer')}
</Button>
) )
} }
]} ]}

View File

@ -1,188 +1,187 @@
import { import { CodeOutlined, DeleteOutlined, ExportOutlined, PlusOutlined, SearchOutlined } from '@ant-design/icons'
DeleteOutlined, import { nanoid } from '@reduxjs/toolkit'
EditOutlined, import { NavbarRight } from '@renderer/components/app/Navbar'
LinkOutlined, import DragableList from '@renderer/components/DragableList'
PlusOutlined, import IndicatorLight from '@renderer/components/IndicatorLight'
QuestionCircleOutlined,
SearchOutlined
} from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import { useTheme } from '@renderer/context/ThemeProvider' import ListItem from '@renderer/components/ListItem'
import { useAppSelector } from '@renderer/store' import Scrollbar from '@renderer/components/Scrollbar'
import { useMCPServers } from '@renderer/hooks/useMCPServers'
import { EventEmitter } from '@renderer/services/EventService'
import { MCPServer } from '@renderer/types' import { MCPServer } from '@renderer/types'
import { Button, Space, Switch, Table, Tag, Tooltip, Typography } from 'antd' import { Button, Dropdown, MenuProps } from 'antd'
import { FC, useState } from 'react' import { isEmpty } from 'lodash'
import { FC, useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '..' import { SettingContainer } from '..'
import AddMcpServerPopup from './AddMcpServerPopup' import McpSettings from './McpSettings'
import EditMcpJsonPopup from './EditMcpJsonPopup'
import InstallNpxUv from './InstallNpxUv'
import NpxSearch from './NpxSearch' import NpxSearch from './NpxSearch'
const MCPSettings: FC = () => { const MCPSettings: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { theme } = useTheme() const { mcpServers, addMCPServer, updateMcpServers, deleteMCPServer } = useMCPServers()
const { Paragraph, Text } = Typography const [selectedMcpServer, setSelectedMcpServer] = useState<MCPServer | null>(mcpServers[0])
const mcpServers = useAppSelector((state) => state.mcp.servers) const [isNpxSearch, setIsNpxSearch] = useState(false)
const [loadingServer, setLoadingServer] = useState<string | null>(null)
const handleDelete = (serverName: string) => { useEffect(() => {
window.modal.confirm({ const unsub = EventEmitter.on('open-npx-search', () => setIsNpxSearch(true))
title: t('settings.mcp.confirmDelete'), return () => unsub()
content: t('settings.mcp.confirmDeleteMessage'), }, [])
okText: t('common.delete'),
okButtonProps: { danger: true },
cancelText: t('common.cancel'),
centered: true,
onOk: async () => {
try {
await window.api.mcp.deleteServer(serverName)
window.message.success(t('settings.mcp.deleteSuccess'))
} catch (error: any) {
window.message.error(`${t('settings.mcp.deleteError')}: ${error.message}`)
}
}
})
}
const handleToggleActive = async (name: string, isActive: boolean) => { const onAddMcpServer = async () => {
setLoadingServer(name) const newServer = {
try { id: nanoid(),
await window.api.mcp.setServerActive(name, isActive) name: t('settings.mcp.newServer'),
} catch (error: any) { description: '',
window.message.error(`${t('settings.mcp.toggleError')}: ${error.message}`) baseUrl: '',
} finally { command: '',
setLoadingServer(null) args: [],
env: {},
isActive: false
} }
addMCPServer(newServer)
window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-list' })
setSelectedMcpServer(newServer)
} }
const handleOpenMCPServers = () => { const onDeleteMcpServer = useCallback(
window.open('https://glama.ai/mcp/servers', '_blank') async (server: MCPServer) => {
} try {
await window.api.mcp.removeServer(server)
const columns = [ await deleteMCPServer(server.id)
{ window.message.success({ content: t('settings.mcp.deleteSuccess'), key: 'mcp-list' })
title: t('settings.mcp.name'), } catch (error: any) {
dataIndex: 'name', window.message.error({
key: 'name', content: `${t('settings.mcp.deleteError')}: ${error.message}`,
width: '300px', key: 'mcp-list'
render: (text: string, record: MCPServer) => <Text strong={record.isActive}>{text}</Text> })
},
{
title: t('settings.mcp.type'),
key: 'type',
width: '100px',
render: (_: any, record: MCPServer) => <Tag color="cyan">{record.baseUrl ? 'SSE' : 'STDIO'}</Tag>
},
{
title: t('settings.mcp.description'),
dataIndex: 'description',
key: 'description',
width: 'auto',
render: (text: string) => {
if (!text) {
return (
<Text type="secondary" italic>
{t('common.description')}
</Text>
)
}
return (
<Paragraph
className="selectable"
ellipsis={{
rows: 1,
expandable: 'collapsible',
symbol: t('common.more'),
onExpand: () => {}, // Empty callback required for proper functionality
tooltip: true
}}
style={{ marginBottom: 0 }}>
{text}
</Paragraph>
)
} }
}, },
{ [deleteMCPServer, t]
title: t('settings.mcp.active'), )
dataIndex: 'isActive',
key: 'isActive',
width: '100px',
render: (isActive: boolean, record: MCPServer) => (
<Switch
checked={isActive}
loading={loadingServer === record.name}
onChange={(checked) => handleToggleActive(record.name, checked)}
/>
)
},
{
title: t('settings.mcp.actions'),
key: 'actions',
width: '100px',
render: (_: any, record: MCPServer) => (
<Space>
<Tooltip title={t('common.edit')}>
<Button
type="primary"
ghost
icon={<EditOutlined />}
onClick={() => AddMcpServerPopup.show({ server: record })}
/>
</Tooltip>
<Tooltip title={t('common.delete')}>
<Button danger icon={<DeleteOutlined />} onClick={() => handleDelete(record.name)} />
</Tooltip>
</Space>
)
}
]
// Create a CSS class for inactive rows instead of using jsx global const getMenuItems = useCallback(
const inactiveRowStyle = { (server: MCPServer) => {
opacity: 0.7, const menus: MenuProps['items'] = [
backgroundColor: theme === 'dark' ? '#1a1a1a' : '#f5f5f5' {
} label: t('common.delete'),
danger: true,
key: 'delete',
icon: <DeleteOutlined />,
onClick: () => onDeleteMcpServer(server)
}
]
return menus
},
[onDeleteMcpServer, t]
)
useEffect(() => {
const _selectedMcpServer = mcpServers.find((server) => server.id === selectedMcpServer?.id)
setSelectedMcpServer(_selectedMcpServer || mcpServers[0])
}, [mcpServers, selectedMcpServer])
return ( return (
<SettingContainer theme={theme}> <Container>
<InstallNpxUv /> <McpList>
<SettingGroup theme={theme}> <ListItem
<SettingTitle> key="add"
{t('settings.mcp.title')} title={t('settings.mcp.addServer')}
<Tooltip title={t('settings.mcp.config_description')}> active={false}
<QuestionCircleOutlined style={{ marginLeft: 8, fontSize: 14 }} /> onClick={onAddMcpServer}
</Tooltip> icon={<PlusOutlined />}
</SettingTitle> titleStyle={{ fontWeight: 500 }}
<SettingDivider /> style={{ marginBottom: 5 }}
<HStack gap={15} alignItems="center">
<Button type="primary" icon={<PlusOutlined />} onClick={() => AddMcpServerPopup.show()}>
{t('settings.mcp.addServer')}
</Button>
<Button icon={<EditOutlined />} onClick={() => EditMcpJsonPopup.show()}>
{t('settings.mcp.editJson')}
</Button>
<Button icon={<SearchOutlined />} onClick={handleOpenMCPServers}>
{t('settings.mcp.findMore')} <LinkOutlined />
</Button>
</HStack>
<Table
dataSource={mcpServers}
columns={columns}
rowKey="name"
pagination={false}
size="small"
locale={{ emptyText: t('settings.mcp.noServers') }}
rowClassName={(record) => (!record.isActive ? 'inactive-row' : '')}
onRow={(record) => ({ style: !record.isActive ? inactiveRowStyle : {} })}
style={{ marginTop: 15 }}
/> />
</SettingGroup> <DragableList list={mcpServers} onUpdate={updateMcpServers}>
<NpxSearch /> {(server: MCPServer) => (
</SettingContainer> <Dropdown menu={{ items: getMenuItems(server) }} trigger={['contextMenu']} key={server.id}>
<div>
<ListItem
key={server.id}
title={server.name}
active={selectedMcpServer?.id === server.id}
onClick={() => {
setSelectedMcpServer(server)
setIsNpxSearch(false)
}}
titleStyle={{ fontWeight: 500 }}
icon={<CodeOutlined />}
rightContent={
<IndicatorLight
size={6}
color={server.isActive ? 'green' : 'var(--color-text-3)'}
animation={server.isActive}
shadow={false}
style={{ marginRight: 4 }}
/>
}
/>
</div>
</Dropdown>
)}
</DragableList>
</McpList>
{isNpxSearch || isEmpty(mcpServers) ? (
<SettingContainer>
<NpxSearch />
</SettingContainer>
) : (
selectedMcpServer && <McpSettings server={selectedMcpServer} />
)}
</Container>
) )
} }
export const McpSettingsNavbar = () => {
const { t } = useTranslation()
const onClick = () => window.open('https://mcp.so/', '_blank')
return (
<NavbarRight>
<HStack alignItems="center" gap={5}>
<Button
size="small"
type="text"
onClick={() => EventEmitter.emit('open-npx-search')}
icon={<SearchOutlined />}
className="nodrag"
style={{ fontSize: 14 }}>
{t('settings.mcp.searchNpx')}
</Button>
<Button
size="small"
type="text"
onClick={onClick}
icon={<ExportOutlined />}
className="nodrag"
style={{ fontSize: 14 }}>
{t('settings.mcp.findMore')}
</Button>
</HStack>
</NavbarRight>
)
}
const Container = styled(HStack)`
flex: 1;
`
const McpList = styled(Scrollbar)`
display: flex;
flex-direction: column;
gap: 5px;
width: var(--settings-width);
padding: 12px;
border-right: 0.5px solid var(--color-border);
height: calc(100vh - var(--navbar-height));
.iconfont {
color: var(--color-text-2);
line-height: 16px;
}
`
export default MCPSettings export default MCPSettings

View File

@ -21,7 +21,7 @@ import AboutSettings from './AboutSettings'
import DataSettings from './DataSettings/DataSettings' import DataSettings from './DataSettings/DataSettings'
import DisplaySettings from './DisplaySettings/DisplaySettings' import DisplaySettings from './DisplaySettings/DisplaySettings'
import GeneralSettings from './GeneralSettings' import GeneralSettings from './GeneralSettings'
import MCPSettings from './MCPSettings' import MCPSettings, { McpSettingsNavbar } from './MCPSettings'
import ProvidersList from './ProviderSettings' import ProvidersList from './ProviderSettings'
import QuickAssistantSettings from './QuickAssistantSettings' import QuickAssistantSettings from './QuickAssistantSettings'
import ShortcutSettings from './ShortcutSettings' import ShortcutSettings from './ShortcutSettings'
@ -37,6 +37,7 @@ const SettingsPage: FC = () => {
<Container> <Container>
<Navbar> <Navbar>
<NavbarCenter style={{ borderRight: 'none' }}>{t('settings.title')}</NavbarCenter> <NavbarCenter style={{ borderRight: 'none' }}>{t('settings.title')}</NavbarCenter>
{pathname === '/settings/mcp' && <McpSettingsNavbar />}
</Navbar> </Navbar>
<ContentContainer id="content-container"> <ContentContainer id="content-container">
<SettingMenus> <SettingMenus>

View File

@ -514,6 +514,7 @@ export default class OpenAIProvider extends BaseProvider {
for (const toolCall of toolCalls) { for (const toolCall of toolCalls) {
const mcpTool = openAIToolsToMcpTool(mcpTools, toolCall) const mcpTool = openAIToolsToMcpTool(mcpTools, toolCall)
if (!mcpTool) { if (!mcpTool) {
continue continue
} }

View File

@ -99,12 +99,15 @@ export async function fetchChatCompletion({
const lastUserMessage = findLast(messages, (m) => m.role === 'user') const lastUserMessage = findLast(messages, (m) => m.role === 'user')
// Get MCP tools // Get MCP tools
let mcpTools: MCPTool[] = [] const mcpTools: MCPTool[] = []
const enabledMCPs = lastUserMessage?.enabledMCPs const enabledMCPs = lastUserMessage?.enabledMCPs
if (enabledMCPs && enabledMCPs.length > 0) { if (enabledMCPs && enabledMCPs.length > 0) {
const allMCPTools = await window.api.mcp.listTools() for (const mcpServer of enabledMCPs) {
mcpTools = allMCPTools.filter((tool) => enabledMCPs.some((mcp) => mcp.name === tool.serverName)) const tools = await window.api.mcp.listTools(mcpServer)
console.debug('tools', tools)
mcpTools.push(...tools)
}
} }
await AI.completions({ await AI.completions({
@ -127,6 +130,7 @@ export async function fetchChatCompletion({
if (mcpToolResponse) { if (mcpToolResponse) {
message.metadata = { ...message.metadata, mcpTools: cloneDeep(mcpToolResponse) } message.metadata = { ...message.metadata, mcpTools: cloneDeep(mcpToolResponse) }
} }
if (generateImage && generateImage.images.length > 0) { if (generateImage && generateImage.images.length > 0) {
const existingImages = message.metadata?.generateImage?.images || [] const existingImages = message.metadata?.generateImage?.images || []
generateImage.images = [...existingImages, ...generateImage.images] generateImage.images = [...existingImages, ...generateImage.images]

View File

@ -42,7 +42,7 @@ const persistedReducer = persistReducer(
{ {
key: 'cherry-studio', key: 'cherry-studio',
storage, storage,
version: 85, version: 86,
blacklist: ['runtime', 'messages'], blacklist: ['runtime', 'messages'],
migrate migrate
}, },

View File

@ -13,19 +13,19 @@ const mcpSlice = createSlice({
state.servers = action.payload state.servers = action.payload
}, },
addMCPServer: (state, action: PayloadAction<MCPServer>) => { addMCPServer: (state, action: PayloadAction<MCPServer>) => {
state.servers.push(action.payload) state.servers.unshift(action.payload)
}, },
updateMCPServer: (state, action: PayloadAction<MCPServer>) => { updateMCPServer: (state, action: PayloadAction<MCPServer>) => {
const index = state.servers.findIndex((server) => server.name === action.payload.name) const index = state.servers.findIndex((server) => server.id === action.payload.id)
if (index !== -1) { if (index !== -1) {
state.servers[index] = action.payload state.servers[index] = action.payload
} }
}, },
deleteMCPServer: (state, action: PayloadAction<string>) => { deleteMCPServer: (state, action: PayloadAction<string>) => {
state.servers = state.servers.filter((server) => server.name !== action.payload) state.servers = state.servers.filter((server) => server.id !== action.payload)
}, },
setMCPServerActive: (state, action: PayloadAction<{ name: string; isActive: boolean }>) => { setMCPServerActive: (state, action: PayloadAction<{ id: string; isActive: boolean }>) => {
const index = state.servers.findIndex((server) => server.name === action.payload.name) const index = state.servers.findIndex((server) => server.id === action.payload.id)
if (index !== -1) { if (index !== -1) {
state.servers[index].isActive = action.payload.isActive state.servers[index].isActive = action.payload.isActive
} }

View File

@ -1,3 +1,4 @@
import { nanoid } from '@reduxjs/toolkit'
import { isMac } from '@renderer/config/constant' import { isMac } from '@renderer/config/constant'
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps' import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
import { SYSTEM_MODELS } from '@renderer/config/models' import { SYSTEM_MODELS } from '@renderer/config/models'
@ -807,6 +808,15 @@ const migrateConfig = {
delete state.settings.manualUpdateCheck delete state.settings.manualUpdateCheck
state.settings.gridPopoverTrigger = 'click' state.settings.gridPopoverTrigger = 'click'
return state return state
},
'86': (state: RootState) => {
if (state.mcp.servers) {
state.mcp.servers = state.mcp.servers.map((server) => ({
...server,
id: nanoid()
}))
}
return state
} }
} }

View File

@ -361,6 +361,7 @@ export interface MCPServerParameter {
} }
export interface MCPServer { export interface MCPServer {
id: string
name: string name: string
description?: string description?: string
baseUrl?: string baseUrl?: string
@ -380,6 +381,7 @@ export interface MCPToolInputSchema {
export interface MCPTool { export interface MCPTool {
id: string id: string
serverId: string
serverName: string serverName: string
name: string name: string
description?: string description?: string

View File

@ -1,5 +1,6 @@
import { Tool, ToolUnion, ToolUseBlock } from '@anthropic-ai/sdk/resources' import { Tool, ToolUnion, ToolUseBlock } from '@anthropic-ai/sdk/resources'
import { FunctionCall, FunctionDeclaration, SchemaType, Tool as geminiToool } from '@google/generative-ai' import { FunctionCall, FunctionDeclaration, SchemaType, Tool as geminiToool } from '@google/generative-ai'
import store from '@renderer/store'
import { MCPServer, MCPTool, MCPToolResponse } from '@renderer/types' import { MCPServer, MCPTool, MCPToolResponse } from '@renderer/types'
import { ChatCompletionMessageToolCall, ChatCompletionTool } from 'openai/resources' import { ChatCompletionMessageToolCall, ChatCompletionTool } from 'openai/resources'
@ -58,8 +59,9 @@ function filterPropertieAttributes(tool: MCPTool, filterNestedObj = false) {
export function mcpToolsToOpenAITools(mcpTools: MCPTool[]): Array<ChatCompletionTool> { export function mcpToolsToOpenAITools(mcpTools: MCPTool[]): Array<ChatCompletionTool> {
return mcpTools.map((tool) => ({ return mcpTools.map((tool) => ({
type: 'function', type: 'function',
name: tool.name,
function: { function: {
name: tool.id, name: tool.serverId,
description: tool.description, description: tool.description,
parameters: { parameters: {
type: 'object', type: 'object',
@ -73,11 +75,16 @@ export function openAIToolsToMcpTool(
mcpTools: MCPTool[] | undefined, mcpTools: MCPTool[] | undefined,
llmTool: ChatCompletionMessageToolCall llmTool: ChatCompletionMessageToolCall
): MCPTool | undefined { ): MCPTool | undefined {
if (!mcpTools) return undefined if (!mcpTools) {
const tool = mcpTools.find((tool) => tool.id === llmTool.function.name) return undefined
}
const tool = mcpTools.find((mcptool) => mcptool.serverId === llmTool.function.name)
if (!tool) { if (!tool) {
return undefined return undefined
} }
console.log( console.log(
`[MCP] OpenAI Tool to MCP Tool: ${tool.serverName} ${tool.name}`, `[MCP] OpenAI Tool to MCP Tool: ${tool.serverName} ${tool.name}`,
tool, tool,
@ -94,6 +101,7 @@ export function openAIToolsToMcpTool(
return { return {
id: tool.id, id: tool.id,
serverId: tool.serverId,
serverName: tool.serverName, serverName: tool.serverName,
name: tool.name, name: tool.name,
description: tool.description, description: tool.description,
@ -104,11 +112,18 @@ export function openAIToolsToMcpTool(
export async function callMCPTool(tool: MCPTool): Promise<any> { export async function callMCPTool(tool: MCPTool): Promise<any> {
console.log(`[MCP] Calling Tool: ${tool.serverName} ${tool.name}`, tool) console.log(`[MCP] Calling Tool: ${tool.serverName} ${tool.name}`, tool)
try { try {
const server = getMcpServerByTool(tool)
if (!server) {
throw new Error(`Server not found: ${tool.serverName}`)
}
const resp = await window.api.mcp.callTool({ const resp = await window.api.mcp.callTool({
client: tool.serverName, server,
name: tool.name, name: tool.name,
args: tool.inputSchema args: tool.inputSchema
}) })
console.log(`[MCP] Tool called: ${tool.serverName} ${tool.name}`, resp) console.log(`[MCP] Tool called: ${tool.serverName} ${tool.name}`, resp)
return resp return resp
} catch (e) { } catch (e) {
@ -227,3 +242,8 @@ export function filterMCPTools(
} }
return mcpTools return mcpTools
} }
export function getMcpServerByTool(tool: MCPTool) {
const servers = store.getState().mcp.servers
return servers.find((s) => s.id === tool.serverId)
}

58
src/utils/file.ts Normal file
View File

@ -0,0 +1,58 @@
import fs from 'fs'
import os from 'os'
import path from 'path'
/**
*
*/
export function getDownloadsPath(): string {
return path.join(os.homedir(), 'Downloads')
}
/**
*
* @param filename
* @returns
*/
export function readFileFromDownloads(filename: string): string {
const downloadsPath = getDownloadsPath()
const filePath = path.join(downloadsPath, filename)
try {
// 检查文件是否存在
if (!fs.existsSync(filePath)) {
throw new Error(`File not found: ${filename}`)
}
// 检查是否是文件而不是目录
const stats = fs.statSync(filePath)
if (stats.isDirectory()) {
throw new Error(`${filename} is a directory, not a file`)
}
// 读取文件内容
return fs.readFileSync(filePath, 'utf-8')
} catch (error) {
if (error instanceof Error) {
throw new Error(`Error reading file: ${error.message}`)
}
throw error
}
}
/**
*
* @returns
*/
export function listDownloadsFiles(): string[] {
const downloadsPath = getDownloadsPath()
try {
return fs.readdirSync(downloadsPath)
} catch (error) {
if (error instanceof Error) {
throw new Error(`Error listing downloads directory: ${error.message}`)
}
throw error
}
}

View File

@ -2369,12 +2369,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@modelcontextprotocol/sdk@npm:1.6.1": "@modelcontextprotocol/sdk@npm:^1.8.0":
version: 1.6.1 version: 1.8.0
resolution: "@modelcontextprotocol/sdk@npm:1.6.1" resolution: "@modelcontextprotocol/sdk@npm:1.8.0"
dependencies: dependencies:
content-type: "npm:^1.0.5" content-type: "npm:^1.0.5"
cors: "npm:^2.8.5" cors: "npm:^2.8.5"
cross-spawn: "npm:^7.0.3"
eventsource: "npm:^3.0.2" eventsource: "npm:^3.0.2"
express: "npm:^5.0.1" express: "npm:^5.0.1"
express-rate-limit: "npm:^7.5.0" express-rate-limit: "npm:^7.5.0"
@ -2382,24 +2383,7 @@ __metadata:
raw-body: "npm:^3.0.0" raw-body: "npm:^3.0.0"
zod: "npm:^3.23.8" zod: "npm:^3.23.8"
zod-to-json-schema: "npm:^3.24.1" zod-to-json-schema: "npm:^3.24.1"
checksum: 10c0/767aca8096c06aabfa9432fab6a4e7bafb671833b1bddb2797b8089e102a9d6ac0486e7a353b28df9984eff5c5291bde76cd5ad079b576ae70666cdff10c5b2a checksum: 10c0/aa453697a9be5e431bc473508654cc77887b35125366c9ec81815d9302872baf708332694c1d5a7ff7d06ac4c22d8446667c24caba78c505f643990b17d95820
languageName: node
linkType: hard
"@modelcontextprotocol/sdk@patch:@modelcontextprotocol/sdk@npm%3A1.6.1#~/.yarn/patches/@modelcontextprotocol-sdk-npm-1.6.1-b46313efe7.patch":
version: 1.6.1
resolution: "@modelcontextprotocol/sdk@patch:@modelcontextprotocol/sdk@npm%3A1.6.1#~/.yarn/patches/@modelcontextprotocol-sdk-npm-1.6.1-b46313efe7.patch::version=1.6.1&hash=9be799"
dependencies:
content-type: "npm:^1.0.5"
cors: "npm:^2.8.5"
eventsource: "npm:^3.0.2"
express: "npm:^5.0.1"
express-rate-limit: "npm:^7.5.0"
pkce-challenge: "npm:^4.1.0"
raw-body: "npm:^3.0.0"
zod: "npm:^3.23.8"
zod-to-json-schema: "npm:^3.24.1"
checksum: 10c0/4121a7d958bce44499feeb8dbe1405e3e778060d93666257cdda3f22ece1837b6e6b8ee81082c13213156b853e501ce02c199b1f6a074530f976e4bd3646ef12
languageName: node languageName: node
linkType: hard linkType: hard
@ -3182,15 +3166,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/chokidar@npm:^2.1.7":
version: 2.1.7
resolution: "@types/chokidar@npm:2.1.7"
dependencies:
chokidar: "npm:*"
checksum: 10c0/e296861b45a90da59a871cc09020e1a8b1111b4a954a2f104ea0a0be31f5b565a35710e9d54670288ca9bdf0c7e71d7d070aaf212db03ee14c1bda93db2f1086
languageName: node
linkType: hard
"@types/d3-color@npm:*": "@types/d3-color@npm:*":
version: 3.1.3 version: 3.1.3
resolution: "@types/d3-color@npm:3.1.3" resolution: "@types/d3-color@npm:3.1.3"
@ -3796,13 +3771,12 @@ __metadata:
"@hello-pangea/dnd": "npm:^16.6.0" "@hello-pangea/dnd": "npm:^16.6.0"
"@kangfenmao/keyv-storage": "npm:^0.1.0" "@kangfenmao/keyv-storage": "npm:^0.1.0"
"@langchain/community": "npm:^0.3.36" "@langchain/community": "npm:^0.3.36"
"@modelcontextprotocol/sdk": "patch:@modelcontextprotocol/sdk@npm%3A1.6.1#~/.yarn/patches/@modelcontextprotocol-sdk-npm-1.6.1-b46313efe7.patch" "@modelcontextprotocol/sdk": "npm:^1.8.0"
"@notionhq/client": "npm:^2.2.15" "@notionhq/client": "npm:^2.2.15"
"@reduxjs/toolkit": "npm:^2.2.5" "@reduxjs/toolkit": "npm:^2.2.5"
"@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch" "@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch"
"@tryfabric/martian": "npm:^1.2.4" "@tryfabric/martian": "npm:^1.2.4"
"@types/adm-zip": "npm:^0" "@types/adm-zip": "npm:^0"
"@types/chokidar": "npm:^2.1.7"
"@types/fs-extra": "npm:^11" "@types/fs-extra": "npm:^11"
"@types/lodash": "npm:^4.17.5" "@types/lodash": "npm:^4.17.5"
"@types/markdown-it": "npm:^14" "@types/markdown-it": "npm:^14"
@ -3821,7 +3795,6 @@ __metadata:
axios: "npm:^1.7.3" axios: "npm:^1.7.3"
babel-plugin-styled-components: "npm:^2.1.4" babel-plugin-styled-components: "npm:^2.1.4"
browser-image-compression: "npm:^2.0.2" browser-image-compression: "npm:^2.0.2"
chokidar: "npm:^4.0.3"
dayjs: "npm:^1.11.11" dayjs: "npm:^1.11.11"
dexie: "npm:^4.0.8" dexie: "npm:^4.0.8"
dexie-react-hooks: "npm:^1.1.7" dexie-react-hooks: "npm:^1.1.7"
@ -5015,7 +4988,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"chokidar@npm:*, chokidar@npm:^4.0.0, chokidar@npm:^4.0.3": "chokidar@npm:^4.0.0":
version: 4.0.3 version: 4.0.3
resolution: "chokidar@npm:4.0.3" resolution: "chokidar@npm:4.0.3"
dependencies: dependencies:
@ -12411,13 +12384,20 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"pkce-challenge@npm:^4.1.0": "pkce-challenge@npm:4.1.0":
version: 4.1.0 version: 4.1.0
resolution: "pkce-challenge@npm:4.1.0" resolution: "pkce-challenge@npm:4.1.0"
checksum: 10c0/7cdc45977eb9af6f561a6f48ffcf19bd3e6f0c651727d00feef1c501384b1ed3c32d92ee67636f02011168959aedf099003a7c0bed668e7943444b20558c54e4 checksum: 10c0/7cdc45977eb9af6f561a6f48ffcf19bd3e6f0c651727d00feef1c501384b1ed3c32d92ee67636f02011168959aedf099003a7c0bed668e7943444b20558c54e4
languageName: node languageName: node
linkType: hard linkType: hard
"pkce-challenge@patch:pkce-challenge@npm%3A4.1.0#~/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch":
version: 4.1.0
resolution: "pkce-challenge@patch:pkce-challenge@npm%3A4.1.0#~/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch::version=4.1.0&hash=3298c3"
checksum: 10c0/8d5a2ad2d6e826011a95e89081d8b2acc40a9e104dc7c7423b22d81520412c013a72157b7f6259650adf5bf796b97062476b7f4c90a7f6baa606ed124f57c0bc
languageName: node
linkType: hard
"pkg-up@npm:^3.1.0": "pkg-up@npm:^3.1.0":
version: 3.1.0 version: 3.1.0
resolution: "pkg-up@npm:3.1.0" resolution: "pkg-up@npm:3.1.0"