diff --git a/src/main/services/mcp.ts b/src/main/services/mcp.ts index 29fe3462..09cdb75a 100644 --- a/src/main/services/mcp.ts +++ b/src/main/services/mcp.ts @@ -2,6 +2,7 @@ import { MCPServer, MCPTool } from '@types' import log from 'electron-log' import Store from 'electron-store' import { EventEmitter } from 'events' +import { v4 as uuidv4 } from 'uuid' const store = new Store() @@ -9,7 +10,8 @@ export default class MCPService extends EventEmitter { private activeServers: Map = new Map() private clients: { [key: string]: any } = {} private Client: any - private Transport: any + private stoioTransport: any + private sseTransport: any private initialized = false private initPromise: Promise | null = null @@ -35,7 +37,8 @@ export default class MCPService extends EventEmitter { try { log.info('[MCP] Starting initialization') this.Client = await this.importClient() - this.Transport = await this.importTransport() + this.stoioTransport = await this.importStdioClientTransport() + this.sseTransport = await this.importSSEClientTransport() // Mark as initialized before loading servers to prevent recursive initialization this.initialized = true @@ -64,7 +67,7 @@ export default class MCPService extends EventEmitter { } } - private async importTransport() { + private async importStdioClientTransport() { try { const { StdioClientTransport } = await import('@modelcontextprotocol/sdk/client/stdio.js') return StdioClientTransport @@ -74,6 +77,16 @@ export default class MCPService extends EventEmitter { } } + private async importSSEClientTransport() { + try { + const { SSEClientTransport } = await import('@modelcontextprotocol/sdk/client/sse.js') + return SSEClientTransport + } catch (err) { + log.error('[MCP] Failed to import Transport:', err) + throw err + } + } + public async listAvailableServices(): Promise { await this.ensureInitialized() return this.getServersFromStore() @@ -175,21 +188,36 @@ export default class MCPService extends EventEmitter { public async activate(server: MCPServer): Promise { await this.ensureInitialized() try { - const { name, command, args, env } = server + const { name, baseUrl, command, args, env } = server if (this.clients[name]) { log.info(`[MCP] Server ${name} is already running`) return } - let cmd: string = command - if (command === 'npx') { - cmd = process.platform === 'win32' ? `${command}.cmd` : command - } + let transport: any = null - const mergedEnv = { - ...env, - PATH: process.env.PATH + if (baseUrl) { + transport = new this.sseTransport(new URL(baseUrl)) + } else if (command) { + let cmd: string = command + if (command === 'npx') { + cmd = process.platform === 'win32' ? `${command}.cmd` : command + } + + const mergedEnv = { + ...env, + PATH: process.env.PATH + } + + transport = new this.stoioTransport({ + command: cmd, + args, + stderr: process.platform === 'win32' ? 'pipe' : 'inherit', + env: mergedEnv + }) + } else { + throw new Error('Either baseUrl or command must be provided') } const client = new this.Client( @@ -202,13 +230,6 @@ export default class MCPService extends EventEmitter { } ) - const transport = new this.Transport({ - command: cmd, - args, - stderr: process.platform === 'win32' ? 'pipe' : 'inherit', - env: mergedEnv - }) - await client.connect(transport) this.clients[name] = client this.activeServers.set(name, { client, server }) @@ -248,6 +269,8 @@ export default class MCPService extends EventEmitter { } const { tools } = await this.clients[serverName].listTools() return tools.map((tool: any) => { + tool.serverName = serverName + tool.id = uuidv4() return tool }) } else { @@ -259,6 +282,7 @@ export default class MCPService extends EventEmitter { allTools = allTools.concat( tools.map((tool: MCPTool) => { tool.serverName = clientName + tool.id = uuidv4() return tool }) ) diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index ca825ab3..9f04acb4 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -867,6 +867,8 @@ "addServer": "Add Server", "editServer": "Edit Server", "name": "Name", + "type": "Type", + "url": "URL", "command": "Command", "args": "Arguments", "argsTooltip": "Each argument on a new line", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 1c5dca56..5854f164 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -859,6 +859,34 @@ "blacklist_tooltip": "以下の形式を使用してください(改行区切り)\nexample.com\nhttps://www.example.com\nhttps://example.com\n*://*.example.com", "search_max_result": "検索結果の数", "search_result_default": "デフォルト" + }, + "mcp": { + "title": "MCP サーバー", + "config_description": "モデルコンテキストプロトコルサーバーの設定", + "description": "説明", + "addServer": "サーバーを追加", + "editServer": "サーバーを編集", + "name": "名前", + "type": "タイプ", + "url": "URL", + "command": "コマンド", + "args": "引数", + "argsTooltip": "1行に1つの引数を入力してください", + "env": "環境変数", + "envTooltip": "形式: KEY=value, 1行に1つ", + "active": "有効", + "actions": "操作", + "noServers": "サーバーが設定されていません", + "nameRequired": "サーバー名を入力してください", + "commandRequired": "コマンドを入力してください", + "confirmDelete": "サーバーを削除", + "confirmDeleteMessage": "本当にこのサーバーを削除しますか?", + "addSuccess": "サーバーが正常に追加されました", + "updateSuccess": "サーバーが正常に更新されました", + "deleteSuccess": "サーバーが正常に削除されました", + "duplicateName": "同じ名前のサーバーが既に存在します", + "serverSingular": "サーバー", + "serverPlural": "サーバー" } }, "translate": { diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index b3708f23..c3e8409b 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -859,6 +859,34 @@ "blacklist_tooltip": "Пожалуйста, используйте следующий формат (разделенный переносами строк)\nexample.com\nhttps://www.example.com\nhttps://example.com\n*://*.example.com", "search_max_result": "Количество результатов поиска", "search_result_default": "По умолчанию" + }, + "mcp": { + "title": "Серверы MCP", + "config_description": "Настройка серверов протокола контекста модели", + "description": "Описание", + "addServer": "Добавить сервер", + "editServer": "Редактировать сервер", + "name": "Имя", + "type": "Тип", + "url": "URL", + "command": "Команда", + "args": "Аргументы", + "argsTooltip": "Каждый аргумент с новой строки", + "env": "Переменные окружения", + "envTooltip": "Формат: KEY=value, по одной на строку", + "active": "Активен", + "actions": "Действия", + "noServers": "Серверы не настроены", + "nameRequired": "Пожалуйста, введите имя сервера", + "commandRequired": "Пожалуйста, введите команду", + "confirmDelete": "Удалить сервер", + "confirmDeleteMessage": "Вы уверены, что хотите удалить этот сервер?", + "addSuccess": "Сервер успешно добавлен", + "updateSuccess": "Сервер успешно обновлен", + "deleteSuccess": "Сервер успешно удален", + "duplicateName": "Сервер с таким именем уже существует", + "serverSingular": "сервер", + "serverPlural": "серверы" } }, "translate": { diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 90e9bbe6..7a889203 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -859,6 +859,34 @@ "title": "Tavily" }, "title": "网络搜索" + }, + "mcp": { + "title": "MCP 服务器", + "config_description": "配置模型上下文协议服务器", + "description": "描述", + "addServer": "添加服务器", + "editServer": "编辑服务器", + "name": "名称", + "type": "类型", + "url": "URL", + "command": "命令", + "args": "参数", + "argsTooltip": "每个参数占一行", + "env": "环境变量", + "envTooltip": "格式:KEY=value,每行一个", + "active": "启用", + "actions": "操作", + "noServers": "未配置服务器", + "nameRequired": "请输入服务器名称", + "commandRequired": "请输入命令", + "confirmDelete": "删除服务器", + "confirmDeleteMessage": "您确定要删除该服务器吗?", + "addSuccess": "服务器添加成功", + "updateSuccess": "服务器更新成功", + "deleteSuccess": "服务器删除成功", + "duplicateName": "已存在同名服务器", + "serverSingular": "服务器", + "serverPlural": "服务器" } }, "translate": { diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index fb3d7484..9a6a4255 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -859,7 +859,35 @@ "search_max_result": "搜索結果個數", "search_result_default": "預設" }, - "display.assistant.title": "助手設定" + "display.assistant.title": "助手設定", + "mcp": { + "title": "MCP 伺服器", + "config_description": "配置模型上下文協議伺服器", + "description": "描述", + "addServer": "新增伺服器", + "editServer": "編輯伺服器", + "name": "名稱", + "type": "類型", + "url": "URL", + "command": "指令", + "args": "參數", + "argsTooltip": "每個參數佔一行", + "env": "環境變數", + "envTooltip": "格式:KEY=value,每行一個", + "active": "啟用", + "actions": "操作", + "noServers": "未配置伺服器", + "nameRequired": "請輸入伺服器名稱", + "commandRequired": "請輸入指令", + "confirmDelete": "刪除伺服器", + "confirmDeleteMessage": "您確定要刪除該伺服器嗎?", + "addSuccess": "伺服器新增成功", + "updateSuccess": "伺服器更新成功", + "deleteSuccess": "伺服器刪除成功", + "duplicateName": "已存在同名伺服器", + "serverSingular": "伺服器", + "serverPlural": "伺服器" + } }, "translate": { "any.language": "任意語言", diff --git a/src/renderer/src/pages/settings/MCPSettings.tsx b/src/renderer/src/pages/settings/MCPSettings.tsx index cdc1dc98..9e9bba71 100644 --- a/src/renderer/src/pages/settings/MCPSettings.tsx +++ b/src/renderer/src/pages/settings/MCPSettings.tsx @@ -3,18 +3,20 @@ import { useTheme } from '@renderer/context/ThemeProvider' import { useAppDispatch, useAppSelector } from '@renderer/store' import { addMCPServer, deleteMCPServer, setMCPServerActive, updateMCPServer } from '@renderer/store/mcp' import { MCPServer } from '@renderer/types' -import { Button, Card, Form, Input, message, Modal, Space, Switch, Table, Tooltip, Typography } from 'antd' +import { Button, Card, Form, Input, message, Modal, Radio, Space, Switch, Table, Tag, Tooltip, Typography } from 'antd' import TextArea from 'antd/es/input/TextArea' -import { FC, useState } from 'react' +import { FC, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '.' interface MCPFormValues { name: string - command: string description?: string - args: string + serverType: 'sse' | 'stdio' + baseUrl?: string + command?: string + args?: string env?: string isActive: boolean } @@ -30,20 +32,37 @@ const MCPSettings: FC = () => { const [editingServer, setEditingServer] = useState(null) const [loading, setLoading] = useState(false) const [form] = Form.useForm() + const [serverType, setServerType] = useState<'sse' | 'stdio'>('stdio') + + // Watch the serverType field to update the form layout dynamically + useEffect(() => { + const type = form.getFieldValue('serverType') + if (type) { + setServerType(type) + } + }, [form]) const showAddModal = () => { form.resetFields() + form.setFieldsValue({ serverType: 'stdio', isActive: true }) + setServerType('stdio') setEditingServer(null) setIsModalVisible(true) } const showEditModal = (server: MCPServer) => { setEditingServer(server) + // Determine server type based on server properties + const serverType = server.baseUrl ? 'sse' : 'stdio' + setServerType(serverType) + form.setFieldsValue({ name: server.name, - command: server.command, description: server.description, - args: server.args.join('\n'), + 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}`) @@ -64,29 +83,32 @@ const MCPSettings: FC = () => { form .validateFields() .then((values) => { - const args = values.args ? values.args.split('\n').filter((arg) => arg.trim() !== '') : [] - - const env: Record = {} - if (values.env) { - values.env.split('\n').forEach((line) => { - if (line.trim()) { - const [key, value] = line.split('=') - if (key && value) { - env[key.trim()] = value.trim() - } - } - }) - } - const mcpServer: MCPServer = { name: values.name, - command: values.command, description: values.description, - args, - env: Object.keys(env).length > 0 ? env : undefined, 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 = {} + if (values.env) { + values.env.split('\n').forEach((line) => { + if (line.trim()) { + const [key, value] = line.split('=') + if (key && value) { + env[key.trim()] = value.trim() + } + } + }) + } + mcpServer.env = Object.keys(env).length > 0 ? env : undefined + } + if (editingServer) { window.api.mcp .updateServer(mcpServer) @@ -167,26 +189,49 @@ const MCPSettings: FC = () => { title: t('settings.mcp.name'), dataIndex: 'name', key: 'name', - width: '20%', + width: '10%', render: (text: string, record: MCPServer) => {text} }, + { + title: t('settings.mcp.type'), + key: 'type', + width: '5%', + render: (_: any, record: MCPServer) => {record.baseUrl ? 'SSE' : 'STDIO'} + }, { title: t('settings.mcp.description'), dataIndex: 'description', key: 'description', - width: '40%', - render: (text: string) => - text || ( - - {t('common.description')} - + width: '50%', + render: (text: string) => { + if (!text) { + return ( + + {t('common.description')} + + ) + } + + return ( + {}, // Empty callback required for proper functionality + tooltip: true + }} + style={{ marginBottom: 0 }}> + {text} + ) + } }, { title: t('settings.mcp.active'), dataIndex: 'isActive', key: 'isActive', - width: '15%', + width: '5%', render: (isActive: boolean, record: MCPServer) => ( handleToggleActive(record.name, checked)} /> ) @@ -194,7 +239,7 @@ const MCPSettings: FC = () => { { title: t('settings.mcp.actions'), key: 'actions', - width: '25%', + width: '10%', render: (_: any, record: MCPServer) => ( @@ -272,19 +317,47 @@ const MCPSettings: FC = () => { - + name="serverType" + label={t('settings.mcp.type')} + rules={[{ required: true }]} + initialValue="stdio"> + setServerType(e.target.value)} + options={[ + { label: 'SSE (Server-Sent Events)', value: 'sse' }, + { label: 'STDIO (Standard Input/Output)', value: 'stdio' } + ]} + /> - -