feat: support MCP sse client (#2880)
* ✨ feat: add Model Context Protocol (MCP) server configuration (main) - Added `@modelcontextprotocol/sdk` dependency for MCP integration. - Introduced MCP server configuration UI in settings with add, edit, delete, and activation functionalities. - Created `useMCPServers` hook to manage MCP server state and actions. - Added i18n support for MCP settings with translation keys. - Integrated MCP settings into the application's settings navigation and routing. - Implemented Redux state management for MCP servers. - Updated `yarn.lock` with new dependencies and their resolutions. * 🌟 feat: implement mcp service and integrate with ipc handlers - Added `MCPService` class to manage Model Context Protocol servers. - Implemented various handlers in `ipc.ts` for managing MCP servers including listing, adding, updating, deleting, and activating/deactivating servers. - Integrated MCP related types into existing type declarations for consistency across the application. - Updated `preload` to expose new MCP related APIs to the renderer process. - Enhanced `MCPSettings` component to interact directly with the new MCP service for adding, updating, deleting servers and setting their active states. - Introduced selectors in the MCP Redux slice for fetching active and all servers from the store. - Moved MCP types to a centralized location in `@renderer/types` for reuse across different parts of the application. * feat: enhance MCPService initialization to prevent recursive calls and improve error handling * feat: enhance MCP integration by adding MCPTool type and updating related methods * feat: implement streaming support for tool calls in OpenAIProvider and enhance message processing * feat: Enhance MCPServer and MCPTool interfaces with optional properties and unique IDs * fix(mcp): Refactor SSE transport initialization to use URL object * fix(OpenAIProvider): correct inputSchema properties reference in tool parameters * feat(MCPSettings): enhance server settings UI with new fields and improved layout * feat(MCPSettings): add multilingual support for MCP server settings * fix: remove unnecessary console log statements
This commit is contained in:
parent
05b3810d4a
commit
a1ae55b29d
@ -2,6 +2,7 @@ import { MCPServer, MCPTool } from '@types'
|
|||||||
import log from 'electron-log'
|
import log from 'electron-log'
|
||||||
import Store from 'electron-store'
|
import Store from 'electron-store'
|
||||||
import { EventEmitter } from 'events'
|
import { EventEmitter } from 'events'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
const store = new Store()
|
const store = new Store()
|
||||||
|
|
||||||
@ -9,7 +10,8 @@ export default class MCPService extends EventEmitter {
|
|||||||
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: any
|
||||||
private Transport: any
|
private stoioTransport: any
|
||||||
|
private sseTransport: any
|
||||||
private initialized = false
|
private initialized = false
|
||||||
private initPromise: Promise<void> | null = null
|
private initPromise: Promise<void> | null = null
|
||||||
|
|
||||||
@ -35,7 +37,8 @@ export default class MCPService extends EventEmitter {
|
|||||||
try {
|
try {
|
||||||
log.info('[MCP] Starting initialization')
|
log.info('[MCP] Starting initialization')
|
||||||
this.Client = await this.importClient()
|
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
|
// Mark as initialized before loading servers to prevent recursive initialization
|
||||||
this.initialized = true
|
this.initialized = true
|
||||||
@ -64,7 +67,7 @@ export default class MCPService extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async importTransport() {
|
private async importStdioClientTransport() {
|
||||||
try {
|
try {
|
||||||
const { StdioClientTransport } = await import('@modelcontextprotocol/sdk/client/stdio.js')
|
const { StdioClientTransport } = await import('@modelcontextprotocol/sdk/client/stdio.js')
|
||||||
return StdioClientTransport
|
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<MCPServer[]> {
|
public async listAvailableServices(): Promise<MCPServer[]> {
|
||||||
await this.ensureInitialized()
|
await this.ensureInitialized()
|
||||||
return this.getServersFromStore()
|
return this.getServersFromStore()
|
||||||
@ -175,13 +188,18 @@ export default class MCPService extends EventEmitter {
|
|||||||
public async activate(server: MCPServer): Promise<void> {
|
public async activate(server: MCPServer): Promise<void> {
|
||||||
await this.ensureInitialized()
|
await this.ensureInitialized()
|
||||||
try {
|
try {
|
||||||
const { name, command, args, env } = server
|
const { name, baseUrl, command, args, env } = server
|
||||||
|
|
||||||
if (this.clients[name]) {
|
if (this.clients[name]) {
|
||||||
log.info(`[MCP] Server ${name} is already running`)
|
log.info(`[MCP] Server ${name} is already running`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let transport: any = null
|
||||||
|
|
||||||
|
if (baseUrl) {
|
||||||
|
transport = new this.sseTransport(new URL(baseUrl))
|
||||||
|
} 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
|
||||||
@ -192,6 +210,16 @@ export default class MCPService extends EventEmitter {
|
|||||||
PATH: process.env.PATH
|
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(
|
const client = new this.Client(
|
||||||
{
|
{
|
||||||
name: name,
|
name: name,
|
||||||
@ -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)
|
await client.connect(transport)
|
||||||
this.clients[name] = client
|
this.clients[name] = client
|
||||||
this.activeServers.set(name, { client, server })
|
this.activeServers.set(name, { client, server })
|
||||||
@ -248,6 +269,8 @@ export default class MCPService extends EventEmitter {
|
|||||||
}
|
}
|
||||||
const { tools } = await this.clients[serverName].listTools()
|
const { tools } = await this.clients[serverName].listTools()
|
||||||
return tools.map((tool: any) => {
|
return tools.map((tool: any) => {
|
||||||
|
tool.serverName = serverName
|
||||||
|
tool.id = uuidv4()
|
||||||
return tool
|
return tool
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@ -259,6 +282,7 @@ export default class MCPService extends EventEmitter {
|
|||||||
allTools = allTools.concat(
|
allTools = allTools.concat(
|
||||||
tools.map((tool: MCPTool) => {
|
tools.map((tool: MCPTool) => {
|
||||||
tool.serverName = clientName
|
tool.serverName = clientName
|
||||||
|
tool.id = uuidv4()
|
||||||
return tool
|
return tool
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
@ -867,6 +867,8 @@
|
|||||||
"addServer": "Add Server",
|
"addServer": "Add Server",
|
||||||
"editServer": "Edit Server",
|
"editServer": "Edit Server",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
|
"type": "Type",
|
||||||
|
"url": "URL",
|
||||||
"command": "Command",
|
"command": "Command",
|
||||||
"args": "Arguments",
|
"args": "Arguments",
|
||||||
"argsTooltip": "Each argument on a new line",
|
"argsTooltip": "Each argument on a new line",
|
||||||
|
|||||||
@ -859,6 +859,34 @@
|
|||||||
"blacklist_tooltip": "以下の形式を使用してください(改行区切り)\nexample.com\nhttps://www.example.com\nhttps://example.com\n*://*.example.com",
|
"blacklist_tooltip": "以下の形式を使用してください(改行区切り)\nexample.com\nhttps://www.example.com\nhttps://example.com\n*://*.example.com",
|
||||||
"search_max_result": "検索結果の数",
|
"search_max_result": "検索結果の数",
|
||||||
"search_result_default": "デフォルト"
|
"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": {
|
"translate": {
|
||||||
|
|||||||
@ -859,6 +859,34 @@
|
|||||||
"blacklist_tooltip": "Пожалуйста, используйте следующий формат (разделенный переносами строк)\nexample.com\nhttps://www.example.com\nhttps://example.com\n*://*.example.com",
|
"blacklist_tooltip": "Пожалуйста, используйте следующий формат (разделенный переносами строк)\nexample.com\nhttps://www.example.com\nhttps://example.com\n*://*.example.com",
|
||||||
"search_max_result": "Количество результатов поиска",
|
"search_max_result": "Количество результатов поиска",
|
||||||
"search_result_default": "По умолчанию"
|
"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": {
|
"translate": {
|
||||||
|
|||||||
@ -859,6 +859,34 @@
|
|||||||
"title": "Tavily"
|
"title": "Tavily"
|
||||||
},
|
},
|
||||||
"title": "网络搜索"
|
"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": {
|
"translate": {
|
||||||
|
|||||||
@ -859,7 +859,35 @@
|
|||||||
"search_max_result": "搜索結果個數",
|
"search_max_result": "搜索結果個數",
|
||||||
"search_result_default": "預設"
|
"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": {
|
"translate": {
|
||||||
"any.language": "任意語言",
|
"any.language": "任意語言",
|
||||||
|
|||||||
@ -3,18 +3,20 @@ import { useTheme } from '@renderer/context/ThemeProvider'
|
|||||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||||
import { addMCPServer, deleteMCPServer, setMCPServerActive, updateMCPServer } from '@renderer/store/mcp'
|
import { addMCPServer, deleteMCPServer, setMCPServerActive, updateMCPServer } from '@renderer/store/mcp'
|
||||||
import { MCPServer } from '@renderer/types'
|
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 TextArea from 'antd/es/input/TextArea'
|
||||||
import { FC, useState } from 'react'
|
import { FC, useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '.'
|
import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '.'
|
||||||
|
|
||||||
interface MCPFormValues {
|
interface MCPFormValues {
|
||||||
name: string
|
name: string
|
||||||
command: string
|
|
||||||
description?: string
|
description?: string
|
||||||
args: string
|
serverType: 'sse' | 'stdio'
|
||||||
|
baseUrl?: string
|
||||||
|
command?: string
|
||||||
|
args?: string
|
||||||
env?: string
|
env?: string
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
}
|
}
|
||||||
@ -30,20 +32,37 @@ const MCPSettings: FC = () => {
|
|||||||
const [editingServer, setEditingServer] = useState<MCPServer | null>(null)
|
const [editingServer, setEditingServer] = useState<MCPServer | null>(null)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [form] = Form.useForm<MCPFormValues>()
|
const [form] = Form.useForm<MCPFormValues>()
|
||||||
|
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 = () => {
|
const showAddModal = () => {
|
||||||
form.resetFields()
|
form.resetFields()
|
||||||
|
form.setFieldsValue({ serverType: 'stdio', isActive: true })
|
||||||
|
setServerType('stdio')
|
||||||
setEditingServer(null)
|
setEditingServer(null)
|
||||||
setIsModalVisible(true)
|
setIsModalVisible(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const showEditModal = (server: MCPServer) => {
|
const showEditModal = (server: MCPServer) => {
|
||||||
setEditingServer(server)
|
setEditingServer(server)
|
||||||
|
// Determine server type based on server properties
|
||||||
|
const serverType = server.baseUrl ? 'sse' : 'stdio'
|
||||||
|
setServerType(serverType)
|
||||||
|
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
name: server.name,
|
name: server.name,
|
||||||
command: server.command,
|
|
||||||
description: server.description,
|
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
|
env: server.env
|
||||||
? Object.entries(server.env)
|
? Object.entries(server.env)
|
||||||
.map(([key, value]) => `${key}=${value}`)
|
.map(([key, value]) => `${key}=${value}`)
|
||||||
@ -64,7 +83,17 @@ const MCPSettings: FC = () => {
|
|||||||
form
|
form
|
||||||
.validateFields()
|
.validateFields()
|
||||||
.then((values) => {
|
.then((values) => {
|
||||||
const args = values.args ? values.args.split('\n').filter((arg) => arg.trim() !== '') : []
|
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> = {}
|
const env: Record<string, string> = {}
|
||||||
if (values.env) {
|
if (values.env) {
|
||||||
@ -77,14 +106,7 @@ const MCPSettings: FC = () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
mcpServer.env = Object.keys(env).length > 0 ? env : undefined
|
||||||
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 (editingServer) {
|
if (editingServer) {
|
||||||
@ -167,26 +189,49 @@ const MCPSettings: FC = () => {
|
|||||||
title: t('settings.mcp.name'),
|
title: t('settings.mcp.name'),
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
key: 'name',
|
key: 'name',
|
||||||
width: '20%',
|
width: '10%',
|
||||||
render: (text: string, record: MCPServer) => <Text strong={record.isActive}>{text}</Text>
|
render: (text: string, record: MCPServer) => <Text strong={record.isActive}>{text}</Text>
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: t('settings.mcp.type'),
|
||||||
|
key: 'type',
|
||||||
|
width: '5%',
|
||||||
|
render: (_: any, record: MCPServer) => <Tag color="cyan">{record.baseUrl ? 'SSE' : 'STDIO'}</Tag>
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: t('settings.mcp.description'),
|
title: t('settings.mcp.description'),
|
||||||
dataIndex: 'description',
|
dataIndex: 'description',
|
||||||
key: 'description',
|
key: 'description',
|
||||||
width: '40%',
|
width: '50%',
|
||||||
render: (text: string) =>
|
render: (text: string) => {
|
||||||
text || (
|
if (!text) {
|
||||||
|
return (
|
||||||
<Text type="secondary" italic>
|
<Text type="secondary" italic>
|
||||||
{t('common.description')}
|
{t('common.description')}
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paragraph
|
||||||
|
ellipsis={{
|
||||||
|
rows: 1,
|
||||||
|
expandable: 'collapsible',
|
||||||
|
symbol: 'more',
|
||||||
|
onExpand: () => {}, // Empty callback required for proper functionality
|
||||||
|
tooltip: true
|
||||||
|
}}
|
||||||
|
style={{ marginBottom: 0 }}>
|
||||||
|
{text}
|
||||||
|
</Paragraph>
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('settings.mcp.active'),
|
title: t('settings.mcp.active'),
|
||||||
dataIndex: 'isActive',
|
dataIndex: 'isActive',
|
||||||
key: 'isActive',
|
key: 'isActive',
|
||||||
width: '15%',
|
width: '5%',
|
||||||
render: (isActive: boolean, record: MCPServer) => (
|
render: (isActive: boolean, record: MCPServer) => (
|
||||||
<Switch checked={isActive} onChange={(checked) => handleToggleActive(record.name, checked)} />
|
<Switch checked={isActive} onChange={(checked) => handleToggleActive(record.name, checked)} />
|
||||||
)
|
)
|
||||||
@ -194,7 +239,7 @@ const MCPSettings: FC = () => {
|
|||||||
{
|
{
|
||||||
title: t('settings.mcp.actions'),
|
title: t('settings.mcp.actions'),
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
width: '25%',
|
width: '10%',
|
||||||
render: (_: any, record: MCPServer) => (
|
render: (_: any, record: MCPServer) => (
|
||||||
<Space>
|
<Space>
|
||||||
<Tooltip title={t('common.edit')}>
|
<Tooltip title={t('common.edit')}>
|
||||||
@ -271,20 +316,48 @@ const MCPSettings: FC = () => {
|
|||||||
<TextArea rows={2} placeholder={t('common.description')} />
|
<TextArea rows={2} placeholder={t('common.description')} />
|
||||||
</Form.Item>
|
</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
|
<Form.Item
|
||||||
name="command"
|
name="command"
|
||||||
label={t('settings.mcp.command')}
|
label={t('settings.mcp.command')}
|
||||||
rules={[{ required: true, message: t('settings.mcp.commandRequired') }]}>
|
rules={[{ required: serverType === 'stdio', message: t('settings.mcp.commandRequired') }]}>
|
||||||
<Input placeholder="python script.py" />
|
<Input placeholder="uvx or npx" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item name="args" label={t('settings.mcp.args')} tooltip={t('settings.mcp.argsTooltip')}>
|
<Form.Item name="args" label={t('settings.mcp.args')} tooltip={t('settings.mcp.argsTooltip')}>
|
||||||
<TextArea rows={3} placeholder="{--param1}\n{--param2 value}" style={{ fontFamily: 'monospace' }} />
|
<TextArea rows={3} placeholder="arg1\narg2" style={{ fontFamily: 'monospace' }} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item name="env" label={t('settings.mcp.env')} tooltip={t('settings.mcp.envTooltip')}>
|
<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' }} />
|
<TextArea rows={3} placeholder="KEY1=value1\nKEY2=value2" style={{ fontFamily: 'monospace' }} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<Form.Item name="isActive" label={t('settings.mcp.active')} valuePropName="checked" initialValue={true}>
|
<Form.Item name="isActive" label={t('settings.mcp.active')} valuePropName="checked" initialValue={true}>
|
||||||
<Switch />
|
<Switch />
|
||||||
|
|||||||
@ -230,30 +230,27 @@ export default class OpenAIProvider extends BaseProvider {
|
|||||||
return mcpTools.map((tool) => ({
|
return mcpTools.map((tool) => ({
|
||||||
type: 'function',
|
type: 'function',
|
||||||
function: {
|
function: {
|
||||||
name: `mcp.${tool.serverName}.${tool.name}`,
|
name: tool.id,
|
||||||
description: tool.description,
|
description: tool.description,
|
||||||
parameters: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: tool.inputSchema
|
properties: tool.inputSchema.properties
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
private openAIToolsToMcpTool(tool: ChatCompletionMessageToolCall): MCPTool | undefined {
|
private openAIToolsToMcpTool(
|
||||||
const parts = tool.function.name.split('.')
|
mcpTools: MCPTool[] | undefined,
|
||||||
if (parts[0] !== 'mcp') {
|
llmTool: ChatCompletionMessageToolCall
|
||||||
console.log('Invalid tool name', tool.function.name)
|
): MCPTool | undefined {
|
||||||
|
if (!mcpTools) return undefined
|
||||||
|
const tool = mcpTools.find((tool) => tool.id === llmTool.function.name)
|
||||||
|
if (!tool) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
const serverName = parts[1]
|
tool.inputSchema = JSON.parse(llmTool.function.arguments)
|
||||||
const name = parts[2]
|
return tool
|
||||||
|
|
||||||
return {
|
|
||||||
serverName: serverName,
|
|
||||||
name: name,
|
|
||||||
inputSchema: JSON.parse(tool.function.arguments)
|
|
||||||
} as MCPTool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async completions({ messages, assistant, onChunk, onFilterMessages, mcpTools }: CompletionsParams): Promise<void> {
|
async completions({ messages, assistant, onChunk, onFilterMessages, mcpTools }: CompletionsParams): Promise<void> {
|
||||||
@ -328,7 +325,7 @@ export default class OpenAIProvider extends BaseProvider {
|
|||||||
const { abortController, cleanup } = this.createAbortController(lastUserMessage?.id)
|
const { abortController, cleanup } = this.createAbortController(lastUserMessage?.id)
|
||||||
const { signal } = abortController
|
const { signal } = abortController
|
||||||
|
|
||||||
const tools = mcpTools ? this.mcpToolsToOpenAITools(mcpTools) : undefined
|
const tools = mcpTools && mcpTools.length > 0 ? this.mcpToolsToOpenAITools(mcpTools) : undefined
|
||||||
|
|
||||||
const reqMessages: ChatCompletionMessageParam[] = [systemMessage, ...userMessages].filter(
|
const reqMessages: ChatCompletionMessageParam[] = [systemMessage, ...userMessages].filter(
|
||||||
Boolean
|
Boolean
|
||||||
@ -401,11 +398,9 @@ export default class OpenAIProvider extends BaseProvider {
|
|||||||
} as ChatCompletionAssistantMessageParam)
|
} as ChatCompletionAssistantMessageParam)
|
||||||
|
|
||||||
for (const toolCall of toolCalls) {
|
for (const toolCall of toolCalls) {
|
||||||
const mcpTool = this.openAIToolsToMcpTool(toolCall)
|
const mcpTool = this.openAIToolsToMcpTool(mcpTools, toolCall)
|
||||||
console.log('mcpTool', JSON.stringify(mcpTool, null, 2))
|
|
||||||
|
|
||||||
if (!mcpTool) {
|
if (!mcpTool) {
|
||||||
console.log('Invalid tool', toolCall)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -415,9 +410,6 @@ export default class OpenAIProvider extends BaseProvider {
|
|||||||
args: mcpTool.inputSchema
|
args: mcpTool.inputSchema
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log(`Tool ${mcpTool.serverName} - ${mcpTool.name} Call Response:`)
|
|
||||||
console.log(toolCallResponse)
|
|
||||||
|
|
||||||
reqMessages.push({
|
reqMessages.push({
|
||||||
role: 'tool',
|
role: 'tool',
|
||||||
content: JSON.stringify(toolCallResponse, null, 2),
|
content: JSON.stringify(toolCallResponse, null, 2),
|
||||||
|
|||||||
@ -78,9 +78,6 @@ export async function fetchChatCompletion({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const allMCPTools = await window.api.mcp.listTools()
|
const allMCPTools = await window.api.mcp.listTools()
|
||||||
if (allMCPTools.length > 0) {
|
|
||||||
console.log('Available MCP tools:', allMCPTools)
|
|
||||||
}
|
|
||||||
await AI.completions({
|
await AI.completions({
|
||||||
messages: filterUsefulMessages(messages),
|
messages: filterUsefulMessages(messages),
|
||||||
assistant,
|
assistant,
|
||||||
|
|||||||
@ -315,9 +315,10 @@ export interface MCPServerParameter {
|
|||||||
|
|
||||||
export interface MCPServer {
|
export interface MCPServer {
|
||||||
name: string
|
name: string
|
||||||
command: string
|
|
||||||
description?: string
|
description?: string
|
||||||
args: string[]
|
baseUrl?: string
|
||||||
|
command?: string
|
||||||
|
args?: string[]
|
||||||
env?: Record<string, string>
|
env?: Record<string, string>
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
}
|
}
|
||||||
@ -331,6 +332,7 @@ export interface MCPToolInputSchema {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface MCPTool {
|
export interface MCPTool {
|
||||||
|
id: string
|
||||||
serverName: string
|
serverName: string
|
||||||
name: string
|
name: string
|
||||||
description?: string
|
description?: string
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user