feat(MCPService, ModelSettings): Enhance path handling and model filtering

- Add enhanced PATH generation for MCP service across different platforms
- Improve model filtering with new function calling model type
- Refactor MCP service type definitions and transport initialization
- Add platform-specific path handling for various development environments
This commit is contained in:
kangfenmao 2025-03-12 18:25:04 +08:00
parent aa75f90294
commit aae12a21ac
3 changed files with 80 additions and 16 deletions

View File

@ -1,3 +1,7 @@
import { isLinux, isMac, isWin } from '@main/constant'
import type { Client } from '@modelcontextprotocol/sdk/client/index.js'
import type { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
import type { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
import { MCPServer, MCPTool } from '@types' import { MCPServer, MCPTool } from '@types'
import log from 'electron-log' import log from 'electron-log'
import { EventEmitter } from 'events' import { EventEmitter } from 'events'
@ -12,9 +16,9 @@ export default class MCPService extends EventEmitter {
private servers: MCPServer[] = [] private servers: MCPServer[] = []
private activeServers: Map<string, any> = new Map() private activeServers: Map<string, any> = new Map()
private clients: { [key: string]: any } = {} private clients: { [key: string]: any } = {}
private Client: any private Client: typeof Client | undefined
private stoioTransport: any private stdioTransport: typeof StdioClientTransport | undefined
private sseTransport: any private sseTransport: typeof SSEClientTransport | undefined
private initialized = false private initialized = false
private initPromise: Promise<void> | null = null private initPromise: Promise<void> | null = null
@ -84,7 +88,7 @@ export default class MCPService extends EventEmitter {
]) ])
this.Client = Client this.Client = Client
this.stoioTransport = StdioTransport this.stdioTransport = StdioTransport
this.sseTransport = SSETransport this.sseTransport = SSETransport
// Mark as initialized before loading servers // Mark as initialized before loading servers
@ -295,35 +299,33 @@ export default class MCPService extends EventEmitter {
return return
} }
let transport: any = null let transport: StdioClientTransport | SSEClientTransport
try { try {
// Create appropriate transport based on configuration // Create appropriate transport based on configuration
if (baseUrl) { if (baseUrl) {
transport = new this.sseTransport(new URL(baseUrl)) transport = new this.sseTransport!(new URL(baseUrl))
} else if (command) { } else if (command) {
let cmd: string = command let cmd: string = command
if (command === 'npx') { if (command === 'npx') {
cmd = process.platform === 'win32' ? `${command}.cmd` : command cmd = process.platform === 'win32' ? `${command}.cmd` : command
} }
const mergedEnv = { transport = new this.stdioTransport!({
...env,
PATH: process.env.PATH
}
transport = new this.stoioTransport({
command: cmd, command: cmd,
args, args,
stderr: process.platform === 'win32' ? 'pipe' : 'inherit', stderr: 'pipe',
env: mergedEnv 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 // Create and connect client
const client = new this.Client({ name, version: '1.0.0' }, { capabilities: {} }) const client = new this.Client!({ name, version: '1.0.0' }, { capabilities: {} })
await client.connect(transport) await client.connect(transport)
@ -491,4 +493,61 @@ export default class MCPService extends EventEmitter {
log.info(`[MCP] Loaded and activated ${Object.keys(this.clients).length} servers`) log.info(`[MCP] Loaded and activated ${Object.keys(this.clients).length} 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`)
}
// 只添加不存在的路径
newPaths.forEach((path) => {
if (path && !existingPaths.has(path)) {
existingPaths.add(path)
}
})
// 转换回字符串
return Array.from(existingPaths).join(pathSeparator)
}
} }

View File

@ -4,6 +4,7 @@ import ModelTags from '@renderer/components/ModelTags'
import { import {
getModelLogo, getModelLogo,
isEmbeddingModel, isEmbeddingModel,
isFunctionCallingModel,
isReasoningModel, isReasoningModel,
isVisionModel, isVisionModel,
isWebSearchModel, isWebSearchModel,
@ -55,6 +56,7 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
) { ) {
return false return false
} }
switch (filterType) { switch (filterType) {
case 'reasoning': case 'reasoning':
return isReasoningModel(model) return isReasoningModel(model)
@ -66,6 +68,8 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
return isFreeModel(model) return isFreeModel(model)
case 'embedding': case 'embedding':
return isEmbeddingModel(model) return isEmbeddingModel(model)
case 'function_calling':
return isFunctionCallingModel(model)
default: default:
return true return true
} }
@ -159,6 +163,7 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
<Radio.Button value="websearch">{t('models.websearch')}</Radio.Button> <Radio.Button value="websearch">{t('models.websearch')}</Radio.Button>
<Radio.Button value="free">{t('models.free')}</Radio.Button> <Radio.Button value="free">{t('models.free')}</Radio.Button>
<Radio.Button value="embedding">{t('models.embedding')}</Radio.Button> <Radio.Button value="embedding">{t('models.embedding')}</Radio.Button>
<Radio.Button value="function_calling">{t('models.function_calling')}</Radio.Button>
</Radio.Group> </Radio.Group>
</Center> </Center>
<Search <Search

View File

@ -113,7 +113,7 @@ const ModelEditContent: FC<ModelEditContentProps> = ({ model, onUpdateModel, ope
...(isVisionModel(model) ? ['vision'] : []), ...(isVisionModel(model) ? ['vision'] : []),
...(isEmbeddingModel(model) ? ['embedding'] : []), ...(isEmbeddingModel(model) ? ['embedding'] : []),
...(isReasoningModel(model) ? ['reasoning'] : []), ...(isReasoningModel(model) ? ['reasoning'] : []),
...(isFunctionCallingModel(model) ? ['tools'] : []) ...(isFunctionCallingModel(model) ? ['function_calling'] : [])
] as ModelType[] ] as ModelType[]
// 合并现有选择和默认类型 // 合并现有选择和默认类型