feat: mcp auto server (#3996)
* feat: add configuration file management to MCPService - Introduced methods to ensure the existence of a configuration file, load configurations from it, and save server configurations. - Updated the MCPService class to handle server configurations more effectively, improving initialization and error handling. - Added dependency on chokidar for file system watching. * feat: enhance MCPService configuration handling - Improved configuration management by adding compatibility for both old and new server formats. - Updated methods to ensure configuration file existence, load configurations, and save server data more effectively. - Refined server initialization logic to handle updates and notifications to Redux more efficiently. - Removed unnecessary waiting for server data from Redux during initialization. * feat: enhance MCPService default configuration handling - Added logic to create a default configuration if none exists, improving the initialization process. - Implemented migration of server configurations from Redux to file, ensuring data consistency. - Updated methods to handle nested server structures and improved error handling during server updates. * refactor: clean up MCPService by removing redundant console logs and unused updateServerInRedux method - Eliminated unnecessary console log statements to improve code readability. - Removed the unused updateServerInRedux method, streamlining the MCPService class. - Maintained existing functionality while enhancing code clarity.
This commit is contained in:
parent
bbc7b20183
commit
41191f6132
@ -71,6 +71,7 @@
|
|||||||
"@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",
|
||||||
@ -109,6 +110,7 @@
|
|||||||
"@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",
|
||||||
|
|||||||
@ -1,28 +1,38 @@
|
|||||||
|
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 { 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 type { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
||||||
import type { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
|
import type { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
|
||||||
import type { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
import type { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
||||||
import { MCPServer, MCPTool } from '@types'
|
import { MCPServer, MCPTool } from '@types'
|
||||||
|
import { app } from 'electron'
|
||||||
import log from 'electron-log'
|
import log from 'electron-log'
|
||||||
import { EventEmitter } from 'events'
|
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
import { CacheService } from './CacheService'
|
import { CacheService } from './CacheService'
|
||||||
import { windowService } from './WindowService'
|
import { windowService } from './WindowService'
|
||||||
|
|
||||||
|
interface ActiveServer {
|
||||||
|
client: Client
|
||||||
|
server: MCPServer
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service for managing Model Context Protocol servers and tools
|
* Service for managing Model Context Protocol servers and tools
|
||||||
*/
|
*/
|
||||||
export default class MCPService extends EventEmitter {
|
export default class MCPService extends EventEmitter {
|
||||||
private servers: MCPServer[] = []
|
private servers: MCPServer[] = []
|
||||||
private activeServers: Map<string, any> = new Map()
|
private activeServers: Map<string, ActiveServer> = new Map()
|
||||||
private clients: { [key: string]: any } = {}
|
private clients: { [key: string]: Client } = {}
|
||||||
private Client: typeof Client | undefined
|
private Client: typeof Client | undefined
|
||||||
private stdioTransport: typeof StdioClientTransport | undefined
|
private stdioTransport: typeof StdioClientTransport | undefined
|
||||||
private sseTransport: typeof SSEClientTransport | undefined
|
private sseTransport: typeof SSEClientTransport | undefined
|
||||||
private initialized = false
|
private initialized = false
|
||||||
private initPromise: Promise<void> | null = null
|
private initPromise: Promise<void> | null = null
|
||||||
|
private configPath: string
|
||||||
|
|
||||||
// Simplified server loading state management
|
// Simplified server loading state management
|
||||||
private readyState = {
|
private readyState = {
|
||||||
@ -33,6 +43,8 @@ export default class MCPService extends EventEmitter {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
|
const userDataPath = app.getPath('userData')
|
||||||
|
this.configPath = join(userDataPath, 'cherry-mcp-servers.json')
|
||||||
this.createServerLoadingPromise()
|
this.createServerLoadingPromise()
|
||||||
this.init().catch((err) => this.logError('Failed to initialize MCP service', err))
|
this.init().catch((err) => this.logError('Failed to initialize MCP service', err))
|
||||||
}
|
}
|
||||||
@ -46,23 +58,112 @@ export default class MCPService extends EventEmitter {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async ensureConfigExists(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await fs.access(this.configPath)
|
||||||
|
} catch {
|
||||||
|
const defaultServers = {
|
||||||
|
name: 'mcp-auto-install',
|
||||||
|
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[]> {
|
||||||
|
try {
|
||||||
|
const data = await fs.readFile(this.configPath, 'utf-8')
|
||||||
|
const config = JSON.parse(data)
|
||||||
|
|
||||||
|
if (config.mcpServers && typeof config.mcpServers === 'object') {
|
||||||
|
console.log('读写读写读写', config)
|
||||||
|
return Object.entries(config.mcpServers).map(([name, serverData]) => ({
|
||||||
|
name,
|
||||||
|
...(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
|
* Set servers received from Redux and trigger initialization if needed
|
||||||
*/
|
*/
|
||||||
public setServers(servers: MCPServer[]): void {
|
public setServers(servers: any): void {
|
||||||
|
// 如果已初始化,则更新服务器列表并保存到文件
|
||||||
this.servers = servers
|
this.servers = servers
|
||||||
log.info(`[MCP] Received ${servers.length} servers from Redux`)
|
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`)
|
||||||
|
|
||||||
// Mark servers as loaded and resolve the waiting promise
|
// 如果未初始化,则标记已加载并解决 Promise
|
||||||
if (!this.readyState.serversLoaded && this.readyState.resolve) {
|
if (!this.readyState.serversLoaded && this.readyState.resolve) {
|
||||||
this.readyState.serversLoaded = true
|
this.readyState.serversLoaded = true
|
||||||
this.readyState.resolve()
|
this.readyState.resolve()
|
||||||
this.readyState.resolve = null
|
this.readyState.resolve = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize if not already initialized
|
// 初始化服务
|
||||||
if (!this.initialized) {
|
// this.init().catch((err) => this.logError('Failed to initialize MCP service', err))
|
||||||
this.init().catch((err) => this.logError('Failed to initialize MCP service', err))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,20 +171,14 @@ export default class MCPService extends EventEmitter {
|
|||||||
* Initialize the MCP service if not already initialized
|
* Initialize the MCP service if not already initialized
|
||||||
*/
|
*/
|
||||||
public async init(): Promise<void> {
|
public async init(): Promise<void> {
|
||||||
// If already initialized, return immediately
|
|
||||||
if (this.initialized) return
|
if (this.initialized) return
|
||||||
|
|
||||||
// If initialization is in progress, return that promise
|
|
||||||
if (this.initPromise) return this.initPromise
|
if (this.initPromise) return this.initPromise
|
||||||
|
|
||||||
this.initPromise = (async () => {
|
this.initPromise = (async () => {
|
||||||
try {
|
try {
|
||||||
log.info('[MCP] Starting initialization')
|
log.info('[MCP] Starting initialization')
|
||||||
|
|
||||||
// Wait for servers to be loaded from Redux
|
// 加载 SDK 组件
|
||||||
await this.waitForServers()
|
|
||||||
|
|
||||||
// Load SDK components in parallel for better performance
|
|
||||||
const [Client, StdioTransport, SSETransport] = await Promise.all([
|
const [Client, StdioTransport, SSETransport] = await Promise.all([
|
||||||
this.importClient(),
|
this.importClient(),
|
||||||
this.importStdioClientTransport(),
|
this.importStdioClientTransport(),
|
||||||
@ -94,16 +189,35 @@ export default class MCPService extends EventEmitter {
|
|||||||
this.stdioTransport = StdioTransport
|
this.stdioTransport = StdioTransport
|
||||||
this.sseTransport = SSETransport
|
this.sseTransport = SSETransport
|
||||||
|
|
||||||
// Mark as initialized before loading servers
|
// 等待Redux初始化完成后再加载配置
|
||||||
this.initialized = true
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
// Load active servers
|
// 标记为已初始化并解决 readyState 的 Promise
|
||||||
|
this.initialized = true
|
||||||
|
if (this.readyState.resolve) {
|
||||||
|
this.readyState.serversLoaded = true
|
||||||
|
this.readyState.resolve()
|
||||||
|
this.readyState.resolve = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载活跃服务器
|
||||||
await this.loadActiveServers()
|
await this.loadActiveServers()
|
||||||
log.info('[MCP] Initialization successfully')
|
log.info('[MCP] Initialization successfully')
|
||||||
|
|
||||||
return
|
return
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.initialized = false // Reset flag on error
|
this.initialized = false
|
||||||
log.error('[MCP] Failed to initialize:', err)
|
log.error('[MCP] Failed to initialize:', err)
|
||||||
throw err
|
throw err
|
||||||
} finally {
|
} finally {
|
||||||
@ -114,21 +228,10 @@ export default class MCPService extends EventEmitter {
|
|||||||
return this.initPromise
|
return this.initPromise
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Wait for servers to be loaded from Redux
|
|
||||||
*/
|
|
||||||
private async waitForServers(): Promise<void> {
|
|
||||||
if (!this.readyState.serversLoaded && this.readyState.promise) {
|
|
||||||
log.info('[MCP] Waiting for servers data from Redux...')
|
|
||||||
await this.readyState.promise
|
|
||||||
log.info('[MCP] Servers received, continuing initialization')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to create consistent error logging functions
|
* Helper to create consistent error logging functions
|
||||||
*/
|
*/
|
||||||
private logError(message: string, err?: any): void {
|
private logError(message: string, err?: unknown): void {
|
||||||
log.error(`[MCP] ${message}`, err)
|
log.error(`[MCP] ${message}`, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -532,6 +635,7 @@ export default class MCPService extends EventEmitter {
|
|||||||
* Load all active servers
|
* Load all active servers
|
||||||
*/
|
*/
|
||||||
private async loadActiveServers(): Promise<void> {
|
private async loadActiveServers(): Promise<void> {
|
||||||
|
console.log('loadActiveServers', this.servers)
|
||||||
const activeServers = this.servers.filter((server) => server.isActive)
|
const activeServers = this.servers.filter((server) => server.isActive)
|
||||||
|
|
||||||
if (activeServers.length === 0) {
|
if (activeServers.length === 0) {
|
||||||
@ -603,11 +707,11 @@ export default class MCPService extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 只添加不存在的路径
|
// 只添加不存在的路径
|
||||||
newPaths.forEach((path) => {
|
for (const path of newPaths) {
|
||||||
if (path && !existingPaths.has(path)) {
|
if (path && !existingPaths.has(path)) {
|
||||||
existingPaths.add(path)
|
existingPaths.add(path)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
// 转换回字符串
|
// 转换回字符串
|
||||||
return Array.from(existingPaths).join(pathSeparator)
|
return Array.from(existingPaths).join(pathSeparator)
|
||||||
|
|||||||
13
yarn.lock
13
yarn.lock
@ -3182,6 +3182,15 @@ __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"
|
||||||
@ -3793,6 +3802,7 @@ __metadata:
|
|||||||
"@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"
|
||||||
@ -3811,6 +3821,7 @@ __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"
|
||||||
@ -5004,7 +5015,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"chokidar@npm:^4.0.0":
|
"chokidar@npm:*, chokidar@npm:^4.0.0, chokidar@npm:^4.0.3":
|
||||||
version: 4.0.3
|
version: 4.0.3
|
||||||
resolution: "chokidar@npm:4.0.3"
|
resolution: "chokidar@npm:4.0.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user