Feat/mcp support MCP prompt (#4675)
* Add MCP prompt listing and retrieval functionality * Add generic caching mechanism for MCP service methods Refactor caching strategy by implementing a higher-order withCache function to centralize cache logic and reduce code duplication. Separate implementation details from caching concerns in listTools, listPrompts and getPrompt methods. # Conflicts: # src/main/services/MCPService.ts * Add MCP prompts listing feature - Add IPC handlers for listing and getting prompts - Create UI component to display available prompts in settings tab - Improve error handling in MCP service methods * fix(McpService): add error handling for tool and prompt listing methods * feat(MCPSettings): enhance prompts and tools sections with improved UI and reset functionality * feat(i18n): add tabs and prompts sections to localization files * feat(MCPToolsButton): add MCP prompt list functionality to Inputbar * feat(McpSettings, NpxSearch): improve user feedback with success messages on server addition * feat(MCPService, MCPToolsButton): enhance prompt handling with caching and improved selection logic * feat(MCPToolsButton): enhance prompt handling with argument support and error management --------- Co-authored-by: Teo <cheesen.xu@gmail.com>
This commit is contained in:
parent
7c39116351
commit
a70ca190ba
@ -39,6 +39,8 @@ export enum IpcChannel {
|
|||||||
Mcp_StopServer = 'mcp:stop-server',
|
Mcp_StopServer = 'mcp:stop-server',
|
||||||
Mcp_ListTools = 'mcp:list-tools',
|
Mcp_ListTools = 'mcp:list-tools',
|
||||||
Mcp_CallTool = 'mcp:call-tool',
|
Mcp_CallTool = 'mcp:call-tool',
|
||||||
|
Mcp_ListPrompts = 'mcp:list-prompts',
|
||||||
|
Mcp_GetPrompt = 'mcp:get-prompt',
|
||||||
Mcp_GetInstallInfo = 'mcp:get-install-info',
|
Mcp_GetInstallInfo = 'mcp:get-install-info',
|
||||||
Mcp_ServersChanged = 'mcp:servers-changed',
|
Mcp_ServersChanged = 'mcp:servers-changed',
|
||||||
Mcp_ServersUpdated = 'mcp:servers-updated',
|
Mcp_ServersUpdated = 'mcp:servers-updated',
|
||||||
|
|||||||
@ -262,6 +262,8 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
|||||||
ipcMain.handle(IpcChannel.Mcp_StopServer, mcpService.stopServer)
|
ipcMain.handle(IpcChannel.Mcp_StopServer, mcpService.stopServer)
|
||||||
ipcMain.handle(IpcChannel.Mcp_ListTools, mcpService.listTools)
|
ipcMain.handle(IpcChannel.Mcp_ListTools, mcpService.listTools)
|
||||||
ipcMain.handle(IpcChannel.Mcp_CallTool, mcpService.callTool)
|
ipcMain.handle(IpcChannel.Mcp_CallTool, mcpService.callTool)
|
||||||
|
ipcMain.handle(IpcChannel.Mcp_ListPrompts, mcpService.listPrompts)
|
||||||
|
ipcMain.handle(IpcChannel.Mcp_GetPrompt, mcpService.getPrompt)
|
||||||
ipcMain.handle(IpcChannel.Mcp_GetInstallInfo, mcpService.getInstallInfo)
|
ipcMain.handle(IpcChannel.Mcp_GetInstallInfo, mcpService.getInstallInfo)
|
||||||
|
|
||||||
ipcMain.handle(IpcChannel.App_IsBinaryExist, (_, name: string) => isBinaryExists(name))
|
ipcMain.handle(IpcChannel.App_IsBinaryExist, (_, name: string) => isBinaryExists(name))
|
||||||
|
|||||||
@ -10,13 +10,47 @@ import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
|
|||||||
import { getDefaultEnvironment, StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
import { getDefaultEnvironment, StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
||||||
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory'
|
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory'
|
||||||
import { nanoid } from '@reduxjs/toolkit'
|
import { nanoid } from '@reduxjs/toolkit'
|
||||||
import { MCPServer, MCPTool } from '@types'
|
import { GetMCPPromptResponse, MCPPrompt, MCPServer, MCPTool } from '@types'
|
||||||
import { app } from 'electron'
|
import { app } from 'electron'
|
||||||
import Logger from 'electron-log'
|
import Logger from 'electron-log'
|
||||||
|
|
||||||
import { CacheService } from './CacheService'
|
import { CacheService } from './CacheService'
|
||||||
import { StreamableHTTPClientTransport, type StreamableHTTPClientTransportOptions } from './MCPStreamableHttpClient'
|
import { StreamableHTTPClientTransport, type StreamableHTTPClientTransportOptions } from './MCPStreamableHttpClient'
|
||||||
|
|
||||||
|
// Generic type for caching wrapped functions
|
||||||
|
type CachedFunction<T extends unknown[], R> = (...args: T) => Promise<R>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Higher-order function to add caching capability to any async function
|
||||||
|
* @param fn The original function to be wrapped with caching
|
||||||
|
* @param getCacheKey Function to generate a cache key from the function arguments
|
||||||
|
* @param ttl Time to live for the cache entry in milliseconds
|
||||||
|
* @param logPrefix Prefix for log messages
|
||||||
|
* @returns The wrapped function with caching capability
|
||||||
|
*/
|
||||||
|
function withCache<T extends unknown[], R>(
|
||||||
|
fn: (...args: T) => Promise<R>,
|
||||||
|
getCacheKey: (...args: T) => string,
|
||||||
|
ttl: number,
|
||||||
|
logPrefix: string
|
||||||
|
): CachedFunction<T, R> {
|
||||||
|
return async (...args: T): Promise<R> => {
|
||||||
|
const cacheKey = getCacheKey(...args)
|
||||||
|
|
||||||
|
if (CacheService.has(cacheKey)) {
|
||||||
|
Logger.info(`${logPrefix} loaded from cache`)
|
||||||
|
const cachedData = CacheService.get<R>(cacheKey)
|
||||||
|
if (cachedData) {
|
||||||
|
return cachedData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await fn(...args)
|
||||||
|
CacheService.set(cacheKey, result, ttl)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class McpService {
|
class McpService {
|
||||||
private clients: Map<string, Client> = new Map()
|
private clients: Map<string, Client> = new Map()
|
||||||
|
|
||||||
@ -35,6 +69,8 @@ class McpService {
|
|||||||
this.initClient = this.initClient.bind(this)
|
this.initClient = this.initClient.bind(this)
|
||||||
this.listTools = this.listTools.bind(this)
|
this.listTools = this.listTools.bind(this)
|
||||||
this.callTool = this.callTool.bind(this)
|
this.callTool = this.callTool.bind(this)
|
||||||
|
this.listPrompts = this.listPrompts.bind(this)
|
||||||
|
this.getPrompt = this.getPrompt.bind(this)
|
||||||
this.closeClient = this.closeClient.bind(this)
|
this.closeClient = this.closeClient.bind(this)
|
||||||
this.removeServer = this.removeServer.bind(this)
|
this.removeServer = this.removeServer.bind(this)
|
||||||
this.restartServer = this.restartServer.bind(this)
|
this.restartServer = this.restartServer.bind(this)
|
||||||
@ -216,31 +252,40 @@ class McpService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async listTools(_: Electron.IpcMainInvokeEvent, server: MCPServer) {
|
private async listToolsImpl(server: MCPServer): Promise<MCPTool[]> {
|
||||||
const client = await this.initClient(server)
|
|
||||||
const serverKey = this.getServerKey(server)
|
|
||||||
const cacheKey = `mcp:list_tool:${serverKey}`
|
|
||||||
if (CacheService.has(cacheKey)) {
|
|
||||||
Logger.info(`[MCP] Tools from ${server.name} loaded from cache`)
|
|
||||||
const cachedTools = CacheService.get<MCPTool[]>(cacheKey)
|
|
||||||
if (cachedTools && cachedTools.length > 0) {
|
|
||||||
return cachedTools
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Logger.info(`[MCP] Listing tools for server: ${server.name}`)
|
Logger.info(`[MCP] Listing tools for server: ${server.name}`)
|
||||||
const { tools } = await client.listTools()
|
const client = await this.initClient(server)
|
||||||
const serverTools: MCPTool[] = []
|
try {
|
||||||
tools.map((tool: any) => {
|
const { tools } = await client.listTools()
|
||||||
const serverTool: MCPTool = {
|
const serverTools: MCPTool[] = []
|
||||||
...tool,
|
tools.map((tool: any) => {
|
||||||
id: `f${nanoid()}`,
|
const serverTool: MCPTool = {
|
||||||
serverId: server.id,
|
...tool,
|
||||||
serverName: server.name
|
id: `f${nanoid()}`,
|
||||||
}
|
serverId: server.id,
|
||||||
serverTools.push(serverTool)
|
serverName: server.name
|
||||||
})
|
}
|
||||||
CacheService.set(cacheKey, serverTools, 5 * 60 * 1000)
|
serverTools.push(serverTool)
|
||||||
return serverTools
|
})
|
||||||
|
return serverTools
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(`[MCP] Failed to list tools for server: ${server.name}`, error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async listTools(_: Electron.IpcMainInvokeEvent, server: MCPServer) {
|
||||||
|
const cachedListTools = withCache<[MCPServer], MCPTool[]>(
|
||||||
|
this.listToolsImpl.bind(this),
|
||||||
|
(server) => {
|
||||||
|
const serverKey = this.getServerKey(server)
|
||||||
|
return `mcp:list_tool:${serverKey}`
|
||||||
|
},
|
||||||
|
5 * 60 * 1000, // 5 minutes TTL
|
||||||
|
`[MCP] Tools from ${server.name}`
|
||||||
|
)
|
||||||
|
|
||||||
|
return cachedListTools(server)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -270,6 +315,76 @@ class McpService {
|
|||||||
return { dir, uvPath, bunPath }
|
return { dir, uvPath, bunPath }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List prompts available on an MCP server
|
||||||
|
*/
|
||||||
|
private async listPromptsImpl(server: MCPServer): Promise<MCPPrompt[]> {
|
||||||
|
Logger.info(`[MCP] Listing prompts for server: ${server.name}`)
|
||||||
|
const client = await this.initClient(server)
|
||||||
|
try {
|
||||||
|
const { prompts } = await client.listPrompts()
|
||||||
|
const serverPrompts = prompts.map((prompt: any) => ({
|
||||||
|
...prompt,
|
||||||
|
id: `p${nanoid()}`,
|
||||||
|
serverId: server.id,
|
||||||
|
serverName: server.name
|
||||||
|
}))
|
||||||
|
return serverPrompts
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(`[MCP] Failed to list prompts for server: ${server.name}`, error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List prompts available on an MCP server with caching
|
||||||
|
*/
|
||||||
|
public async listPrompts(_: Electron.IpcMainInvokeEvent, server: MCPServer): Promise<MCPPrompt[]> {
|
||||||
|
const cachedListPrompts = withCache<[MCPServer], MCPPrompt[]>(
|
||||||
|
this.listPromptsImpl.bind(this),
|
||||||
|
(server) => {
|
||||||
|
const serverKey = this.getServerKey(server)
|
||||||
|
return `mcp:list_prompts:${serverKey}`
|
||||||
|
},
|
||||||
|
60 * 60 * 1000, // 60 minutes TTL
|
||||||
|
`[MCP] Prompts from ${server.name}`
|
||||||
|
)
|
||||||
|
return cachedListPrompts(server)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific prompt from an MCP server (implementation)
|
||||||
|
*/
|
||||||
|
private async getPromptImpl(
|
||||||
|
server: MCPServer,
|
||||||
|
name: string,
|
||||||
|
args?: Record<string, any>
|
||||||
|
): Promise<GetMCPPromptResponse> {
|
||||||
|
Logger.info(`[MCP] Getting prompt ${name} from server: ${server.name}`)
|
||||||
|
const client = await this.initClient(server)
|
||||||
|
return await client.getPrompt({ name, arguments: args })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific prompt from an MCP server with caching
|
||||||
|
*/
|
||||||
|
public async getPrompt(
|
||||||
|
_: Electron.IpcMainInvokeEvent,
|
||||||
|
{ server, name, args }: { server: MCPServer; name: string; args?: Record<string, any> }
|
||||||
|
): Promise<GetMCPPromptResponse> {
|
||||||
|
const cachedGetPrompt = withCache<[MCPServer, string, Record<string, any> | undefined], GetMCPPromptResponse>(
|
||||||
|
this.getPromptImpl.bind(this),
|
||||||
|
(server, name, args) => {
|
||||||
|
const serverKey = this.getServerKey(server)
|
||||||
|
const argsKey = args ? JSON.stringify(args) : 'no-args'
|
||||||
|
return `mcp:get_prompt:${serverKey}:${name}:${argsKey}`
|
||||||
|
},
|
||||||
|
30 * 60 * 1000, // 30 minutes TTL
|
||||||
|
`[MCP] Prompt ${name} from ${server.name}`
|
||||||
|
)
|
||||||
|
return await cachedGetPrompt(server, name, args)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get enhanced PATH including common tool locations
|
* Get enhanced PATH including common tool locations
|
||||||
*/
|
*/
|
||||||
|
|||||||
10
src/preload/index.d.ts
vendored
10
src/preload/index.d.ts
vendored
@ -151,6 +151,16 @@ declare global {
|
|||||||
stopServer: (server: MCPServer) => Promise<void>
|
stopServer: (server: MCPServer) => Promise<void>
|
||||||
listTools: (server: MCPServer) => Promise<MCPTool[]>
|
listTools: (server: MCPServer) => Promise<MCPTool[]>
|
||||||
callTool: ({ server, name, args }: { server: MCPServer; name: string; args: any }) => Promise<any>
|
callTool: ({ server, name, args }: { server: MCPServer; name: string; args: any }) => Promise<any>
|
||||||
|
listPrompts: (server: MCPServer) => Promise<MCPPrompt[]>
|
||||||
|
getPrompt: ({
|
||||||
|
server,
|
||||||
|
name,
|
||||||
|
args
|
||||||
|
}: {
|
||||||
|
server: MCPServer
|
||||||
|
name: string
|
||||||
|
args?: Record<string, any>
|
||||||
|
}) => Promise<GetMCPPromptResponse>
|
||||||
getInstallInfo: () => Promise<{ dir: string; uvPath: string; bunPath: string }>
|
getInstallInfo: () => Promise<{ dir: string; uvPath: string; bunPath: string }>
|
||||||
}
|
}
|
||||||
copilot: {
|
copilot: {
|
||||||
|
|||||||
@ -130,8 +130,11 @@ const api = {
|
|||||||
restartServer: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_RestartServer, server),
|
restartServer: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_RestartServer, server),
|
||||||
stopServer: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_StopServer, server),
|
stopServer: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_StopServer, server),
|
||||||
listTools: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_ListTools, server),
|
listTools: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_ListTools, server),
|
||||||
callTool: ({ server, name, args }: { server: MCPServer; name: string; args: any }) =>
|
callTool: ({ server, name, args }: { server: MCPServer; name: string; args?: Record<string, any> }) =>
|
||||||
ipcRenderer.invoke(IpcChannel.Mcp_CallTool, { server, name, args }),
|
ipcRenderer.invoke(IpcChannel.Mcp_CallTool, { server, name, args }),
|
||||||
|
listPrompts: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_ListPrompts, server),
|
||||||
|
getPrompt: ({ server, name, args }: { server: MCPServer; name: string; args?: Record<string, any> }) =>
|
||||||
|
ipcRenderer.invoke(IpcChannel.Mcp_GetPrompt, { server, name, args }),
|
||||||
getInstallInfo: () => ipcRenderer.invoke(IpcChannel.Mcp_GetInstallInfo)
|
getInstallInfo: () => ipcRenderer.invoke(IpcChannel.Mcp_GetInstallInfo)
|
||||||
},
|
},
|
||||||
shell: {
|
shell: {
|
||||||
|
|||||||
@ -1089,11 +1089,24 @@
|
|||||||
"url": "URL",
|
"url": "URL",
|
||||||
"editMcpJson": "Edit MCP Configuration",
|
"editMcpJson": "Edit MCP Configuration",
|
||||||
"installHelp": "Get Installation Help",
|
"installHelp": "Get Installation Help",
|
||||||
|
"tabs": {
|
||||||
|
"general": "General",
|
||||||
|
"tools": "Tools",
|
||||||
|
"prompts": "Prompts",
|
||||||
|
"resources": "Resources"
|
||||||
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
"inputSchema": "Input Schema",
|
"inputSchema": "Input Schema",
|
||||||
"availableTools": "Available Tools",
|
"availableTools": "Available Tools",
|
||||||
"noToolsAvailable": "No tools available"
|
"noToolsAvailable": "No tools available"
|
||||||
},
|
},
|
||||||
|
"prompts": {
|
||||||
|
"availablePrompts": "Available Prompts",
|
||||||
|
"noPromptsAvailable": "No prompts available",
|
||||||
|
"arguments": "Arguments",
|
||||||
|
"requiredField": "Required Field",
|
||||||
|
"genericError": "Get prompt Error"
|
||||||
|
},
|
||||||
"deleteServer": "Delete Server",
|
"deleteServer": "Delete Server",
|
||||||
"deleteServerConfirm": "Are you sure you want to delete this server?",
|
"deleteServerConfirm": "Are you sure you want to delete this server?",
|
||||||
"registry": "Package Registry",
|
"registry": "Package Registry",
|
||||||
|
|||||||
@ -1088,10 +1088,23 @@
|
|||||||
},
|
},
|
||||||
"editMcpJson": "MCP 設定を編集",
|
"editMcpJson": "MCP 設定を編集",
|
||||||
"installHelp": "インストールヘルプを取得",
|
"installHelp": "インストールヘルプを取得",
|
||||||
|
"tabs": {
|
||||||
|
"general": "一般",
|
||||||
|
"tools": "ツール",
|
||||||
|
"prompts": "プロンプト",
|
||||||
|
"resources": "リソース"
|
||||||
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
"inputSchema": "入力スキーマ",
|
"inputSchema": "入力スキーマ",
|
||||||
"availableTools": "利用可能なツール",
|
"availableTools": "利用可能なツール",
|
||||||
"noToolsAvailable": "利用可能なツールはありません"
|
"noToolsAvailable": "利用可能なツールなし"
|
||||||
|
},
|
||||||
|
"prompts": {
|
||||||
|
"availablePrompts": "利用可能なプロンプト",
|
||||||
|
"noPromptsAvailable": "利用可能なプロンプトはありません",
|
||||||
|
"arguments": "引数",
|
||||||
|
"requiredField": "必須フィールド",
|
||||||
|
"genericError": "プロンプト取得エラー"
|
||||||
},
|
},
|
||||||
"deleteServer": "サーバーを削除",
|
"deleteServer": "サーバーを削除",
|
||||||
"deleteServerConfirm": "このサーバーを削除してもよろしいですか?",
|
"deleteServerConfirm": "このサーバーを削除してもよろしいですか?",
|
||||||
|
|||||||
@ -1088,10 +1088,23 @@
|
|||||||
"url": "URL",
|
"url": "URL",
|
||||||
"editMcpJson": "Редактировать MCP",
|
"editMcpJson": "Редактировать MCP",
|
||||||
"installHelp": "Получить помощь по установке",
|
"installHelp": "Получить помощь по установке",
|
||||||
|
"tabs": {
|
||||||
|
"general": "Общие",
|
||||||
|
"tools": "Инструменты",
|
||||||
|
"prompts": "Подсказки",
|
||||||
|
"resources": "Ресурсы"
|
||||||
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
"inputSchema": "входные параметры",
|
"inputSchema": "Схема ввода",
|
||||||
"availableTools": "доступные инструменты",
|
"availableTools": "Доступные инструменты",
|
||||||
"noToolsAvailable": "нет доступных инструментов"
|
"noToolsAvailable": "Нет доступных инструментов"
|
||||||
|
},
|
||||||
|
"prompts": {
|
||||||
|
"availablePrompts": "Доступные подсказки",
|
||||||
|
"noPromptsAvailable": "Нет доступных подсказок",
|
||||||
|
"arguments": "Аргументы",
|
||||||
|
"requiredField": "Обязательное поле",
|
||||||
|
"genericError": "Ошибка получения подсказки"
|
||||||
},
|
},
|
||||||
"deleteServer": "Удалить сервер",
|
"deleteServer": "Удалить сервер",
|
||||||
"deleteServerConfirm": "Вы уверены, что хотите удалить этот сервер?",
|
"deleteServerConfirm": "Вы уверены, что хотите удалить этот сервер?",
|
||||||
|
|||||||
@ -1089,10 +1089,23 @@
|
|||||||
"url": "URL",
|
"url": "URL",
|
||||||
"editMcpJson": "编辑 MCP 配置",
|
"editMcpJson": "编辑 MCP 配置",
|
||||||
"installHelp": "获取安装帮助",
|
"installHelp": "获取安装帮助",
|
||||||
|
"tabs": {
|
||||||
|
"general": "通用",
|
||||||
|
"tools": "工具",
|
||||||
|
"prompts": "提示",
|
||||||
|
"resources": "资源"
|
||||||
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
"inputSchema": "输入参数",
|
"inputSchema": "输入模式",
|
||||||
"availableTools": "可用工具",
|
"availableTools": "可用工具",
|
||||||
"noToolsAvailable": "没有可用工具"
|
"noToolsAvailable": "无可用工具"
|
||||||
|
},
|
||||||
|
"prompts": {
|
||||||
|
"availablePrompts": "可用提示",
|
||||||
|
"noPromptsAvailable": "无可用提示",
|
||||||
|
"arguments": "参数",
|
||||||
|
"requiredField": "必填字段",
|
||||||
|
"genericError": "获取提示错误"
|
||||||
},
|
},
|
||||||
"deleteServer": "删除服务器",
|
"deleteServer": "删除服务器",
|
||||||
"deleteServerConfirm": "确定要删除此服务器吗?",
|
"deleteServerConfirm": "确定要删除此服务器吗?",
|
||||||
|
|||||||
@ -1088,10 +1088,23 @@
|
|||||||
"url": "URL",
|
"url": "URL",
|
||||||
"editMcpJson": "編輯 MCP 配置",
|
"editMcpJson": "編輯 MCP 配置",
|
||||||
"installHelp": "獲取安裝幫助",
|
"installHelp": "獲取安裝幫助",
|
||||||
|
"tabs": {
|
||||||
|
"general": "通用",
|
||||||
|
"tools": "工具",
|
||||||
|
"prompts": "提示",
|
||||||
|
"resources": "資源"
|
||||||
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
"inputSchema": "輸入參數",
|
"inputSchema": "輸入模式",
|
||||||
"availableTools": "可用工具",
|
"availableTools": "可用工具",
|
||||||
"noToolsAvailable": "沒有可用工具"
|
"noToolsAvailable": "無可用工具"
|
||||||
|
},
|
||||||
|
"prompts": {
|
||||||
|
"availablePrompts": "可用提示",
|
||||||
|
"noPromptsAvailable": "無可用提示",
|
||||||
|
"arguments": "參數",
|
||||||
|
"requiredField": "必填欄位",
|
||||||
|
"genericError": "獲取提示錯誤"
|
||||||
},
|
},
|
||||||
"deleteServer": "刪除伺服器",
|
"deleteServer": "刪除伺服器",
|
||||||
"deleteServerConfirm": "確定要刪除此伺服器嗎?",
|
"deleteServerConfirm": "確定要刪除此伺服器嗎?",
|
||||||
|
|||||||
@ -358,6 +358,15 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
mcpToolsButtonRef.current?.openQuickPanel()
|
mcpToolsButtonRef.current?.openQuickPanel()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'MCP Prompt',
|
||||||
|
description: '',
|
||||||
|
icon: <CodeOutlined />,
|
||||||
|
isMenu: true,
|
||||||
|
action: () => {
|
||||||
|
mcpToolsButtonRef.current?.openPromptList()
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: isVisionModel(model) ? t('chat.input.upload') : t('chat.input.upload.document'),
|
label: isVisionModel(model) ? t('chat.input.upload') : t('chat.input.upload.document'),
|
||||||
description: '',
|
description: '',
|
||||||
@ -960,6 +969,8 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
enabledMCPs={enabledMCPs}
|
enabledMCPs={enabledMCPs}
|
||||||
toggelEnableMCP={toggelEnableMCP}
|
toggelEnableMCP={toggelEnableMCP}
|
||||||
ToolbarButton={ToolbarButton}
|
ToolbarButton={ToolbarButton}
|
||||||
|
setInputValue={setText}
|
||||||
|
resizeTextArea={resizeTextArea}
|
||||||
/>
|
/>
|
||||||
<GenerateImageButton
|
<GenerateImageButton
|
||||||
model={model}
|
model={model}
|
||||||
|
|||||||
@ -1,28 +1,40 @@
|
|||||||
import { CodeOutlined, PlusOutlined } from '@ant-design/icons'
|
import { CodeOutlined, PlusOutlined } from '@ant-design/icons'
|
||||||
import { QuickPanelListItem, useQuickPanel } from '@renderer/components/QuickPanel'
|
import { QuickPanelListItem, useQuickPanel } from '@renderer/components/QuickPanel'
|
||||||
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
||||||
import { MCPServer } from '@renderer/types'
|
import { MCPPrompt, MCPServer } from '@renderer/types'
|
||||||
import { Tooltip } from 'antd'
|
import { Form, Input, Modal, Tooltip } from 'antd'
|
||||||
import { FC, useCallback, useImperativeHandle, useMemo } from 'react'
|
import { FC, useCallback, useImperativeHandle, useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useNavigate } from 'react-router'
|
import { useNavigate } from 'react-router'
|
||||||
|
|
||||||
export interface MCPToolsButtonRef {
|
export interface MCPToolsButtonRef {
|
||||||
openQuickPanel: () => void
|
openQuickPanel: () => void
|
||||||
|
openPromptList: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
ref?: React.RefObject<MCPToolsButtonRef | null>
|
ref?: React.RefObject<MCPToolsButtonRef | null>
|
||||||
enabledMCPs: MCPServer[]
|
enabledMCPs: MCPServer[]
|
||||||
|
setInputValue: React.Dispatch<React.SetStateAction<string>>
|
||||||
|
resizeTextArea: () => void
|
||||||
toggelEnableMCP: (server: MCPServer) => void
|
toggelEnableMCP: (server: MCPServer) => void
|
||||||
ToolbarButton: any
|
ToolbarButton: any
|
||||||
}
|
}
|
||||||
|
|
||||||
const MCPToolsButton: FC<Props> = ({ ref, enabledMCPs, toggelEnableMCP, ToolbarButton }) => {
|
const MCPToolsButton: FC<Props> = ({
|
||||||
|
ref,
|
||||||
|
setInputValue,
|
||||||
|
resizeTextArea,
|
||||||
|
enabledMCPs,
|
||||||
|
toggelEnableMCP,
|
||||||
|
ToolbarButton
|
||||||
|
}) => {
|
||||||
const { activedMcpServers } = useMCPServers()
|
const { activedMcpServers } = useMCPServers()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const quickPanel = useQuickPanel()
|
const quickPanel = useQuickPanel()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
// Create form instance at the top level
|
||||||
|
const [form] = Form.useForm()
|
||||||
|
|
||||||
const availableMCPs = activedMcpServers.filter((server) => enabledMCPs.some((s) => s.id === server.id))
|
const availableMCPs = activedMcpServers.filter((server) => enabledMCPs.some((s) => s.id === server.id))
|
||||||
|
|
||||||
@ -56,6 +68,220 @@ const MCPToolsButton: FC<Props> = ({ ref, enabledMCPs, toggelEnableMCP, ToolbarB
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [menuItems, quickPanel, t])
|
}, [menuItems, quickPanel, t])
|
||||||
|
// Extract and format all content from the prompt response
|
||||||
|
const extractPromptContent = useCallback((response: any): string | null => {
|
||||||
|
// Handle string response (backward compatibility)
|
||||||
|
if (typeof response === 'string') {
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle GetMCPPromptResponse format
|
||||||
|
if (response && Array.isArray(response.messages)) {
|
||||||
|
let formattedContent = ''
|
||||||
|
|
||||||
|
for (const message of response.messages) {
|
||||||
|
if (!message.content) continue
|
||||||
|
|
||||||
|
// Add role prefix if available
|
||||||
|
const rolePrefix = message.role ? `**${message.role.charAt(0).toUpperCase() + message.role.slice(1)}:** ` : ''
|
||||||
|
|
||||||
|
// Process different content types
|
||||||
|
switch (message.content.type) {
|
||||||
|
case 'text':
|
||||||
|
// Add formatted text content with role
|
||||||
|
formattedContent += `${rolePrefix}${message.content.text}\n\n`
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'image':
|
||||||
|
// Format image as markdown with proper attribution
|
||||||
|
if (message.content.data && message.content.mimeType) {
|
||||||
|
const imageData = message.content.data
|
||||||
|
const mimeType = message.content.mimeType
|
||||||
|
// Include role if available
|
||||||
|
if (rolePrefix) {
|
||||||
|
formattedContent += `${rolePrefix}\n`
|
||||||
|
}
|
||||||
|
formattedContent += `\n\n`
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'audio':
|
||||||
|
// Add indicator for audio content with role
|
||||||
|
formattedContent += `${rolePrefix}[Audio content available]\n\n`
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'resource':
|
||||||
|
// Add indicator for resource content with role
|
||||||
|
if (message.content.text) {
|
||||||
|
formattedContent += `${rolePrefix}${message.content.text}\n\n`
|
||||||
|
} else {
|
||||||
|
formattedContent += `${rolePrefix}[Resource content available]\n\n`
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Add text content if available with role, otherwise show placeholder
|
||||||
|
if (message.content.text) {
|
||||||
|
formattedContent += `${rolePrefix}${message.content.text}\n\n`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return formattedContent.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback handling for single message format
|
||||||
|
if (response && response.messages && response.messages.length > 0) {
|
||||||
|
const message = response.messages[0]
|
||||||
|
if (message.content && message.content.text) {
|
||||||
|
const rolePrefix = message.role ? `**${message.role.charAt(0).toUpperCase() + message.role.slice(1)}:** ` : ''
|
||||||
|
return `${rolePrefix}${message.content.text}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Helper function to insert prompt into text area
|
||||||
|
const insertPromptIntoTextArea = useCallback(
|
||||||
|
(promptText: string) => {
|
||||||
|
setInputValue((prev) => {
|
||||||
|
const textArea = document.querySelector('.inputbar textarea') as HTMLTextAreaElement
|
||||||
|
if (!textArea) return prev + promptText // Fallback if we can't find the textarea
|
||||||
|
|
||||||
|
const cursorPosition = textArea.selectionStart
|
||||||
|
const selectionStart = cursorPosition
|
||||||
|
const selectionEndPosition = cursorPosition + promptText.length
|
||||||
|
const newText = prev.slice(0, cursorPosition) + promptText + prev.slice(cursorPosition)
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
textArea.focus()
|
||||||
|
textArea.setSelectionRange(selectionStart, selectionEndPosition)
|
||||||
|
resizeTextArea()
|
||||||
|
}, 10)
|
||||||
|
return newText
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[setInputValue, resizeTextArea]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handlePromptSelect = useCallback(
|
||||||
|
(prompt: MCPPrompt) => {
|
||||||
|
setTimeout(async () => {
|
||||||
|
const server = enabledMCPs.find((s) => s.id === prompt.serverId)
|
||||||
|
if (server) {
|
||||||
|
try {
|
||||||
|
// Check if the prompt has arguments
|
||||||
|
if (prompt.arguments && prompt.arguments.length > 0) {
|
||||||
|
// Reset form when opening a new modal
|
||||||
|
form.resetFields()
|
||||||
|
|
||||||
|
Modal.confirm({
|
||||||
|
title: `${t('settings.mcp.prompts.arguments')}: ${prompt.name}`,
|
||||||
|
content: (
|
||||||
|
<Form form={form} layout="vertical">
|
||||||
|
{prompt.arguments.map((arg, index) => (
|
||||||
|
<Form.Item
|
||||||
|
key={index}
|
||||||
|
name={arg.name}
|
||||||
|
label={`${arg.name}${arg.required ? ' *' : ''}`}
|
||||||
|
tooltip={arg.description}
|
||||||
|
rules={
|
||||||
|
arg.required ? [{ required: true, message: t('settings.mcp.prompts.requiredField') }] : []
|
||||||
|
}>
|
||||||
|
<Input placeholder={arg.description || arg.name} />
|
||||||
|
</Form.Item>
|
||||||
|
))}
|
||||||
|
</Form>
|
||||||
|
),
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
// Validate and get form values
|
||||||
|
const values = await form.validateFields()
|
||||||
|
|
||||||
|
const response = await window.api.mcp.getPrompt({
|
||||||
|
server,
|
||||||
|
name: prompt.name,
|
||||||
|
args: values
|
||||||
|
})
|
||||||
|
|
||||||
|
// Extract and format prompt content from the response
|
||||||
|
const promptContent = extractPromptContent(response)
|
||||||
|
if (promptContent) {
|
||||||
|
insertPromptIntoTextArea(promptContent)
|
||||||
|
} else {
|
||||||
|
throw new Error('Invalid prompt response format')
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve()
|
||||||
|
} catch (error: Error | any) {
|
||||||
|
if (error.errorFields) {
|
||||||
|
// This is a form validation error, handled by Ant Design
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
Modal.error({
|
||||||
|
title: t('common.error'),
|
||||||
|
content: error.message || t('settings.mcp.prompts.genericError')
|
||||||
|
})
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
okText: t('common.confirm'),
|
||||||
|
cancelText: t('common.cancel')
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// If no arguments, get the prompt directly
|
||||||
|
const response = await window.api.mcp.getPrompt({
|
||||||
|
server,
|
||||||
|
name: prompt.name
|
||||||
|
})
|
||||||
|
|
||||||
|
// Extract and format prompt content from the response
|
||||||
|
const promptContent = extractPromptContent(response)
|
||||||
|
if (promptContent) {
|
||||||
|
insertPromptIntoTextArea(promptContent)
|
||||||
|
} else {
|
||||||
|
throw new Error('Invalid prompt response format')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: Error | any) {
|
||||||
|
Modal.error({
|
||||||
|
title: t('common.error'),
|
||||||
|
content: error.message || t('settings.mcp.prompt.genericError')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 10)
|
||||||
|
},
|
||||||
|
[enabledMCPs, form, t, extractPromptContent, insertPromptIntoTextArea] // Add form to dependencies
|
||||||
|
)
|
||||||
|
|
||||||
|
const promptList = useMemo(async () => {
|
||||||
|
const prompts: MCPPrompt[] = []
|
||||||
|
|
||||||
|
for (const server of enabledMCPs) {
|
||||||
|
const serverPrompts = await window.api.mcp.listPrompts(server)
|
||||||
|
prompts.push(...serverPrompts)
|
||||||
|
}
|
||||||
|
|
||||||
|
return prompts.map((prompt) => ({
|
||||||
|
label: prompt.name,
|
||||||
|
description: prompt.description,
|
||||||
|
icon: <CodeOutlined />,
|
||||||
|
action: () => handlePromptSelect(prompt)
|
||||||
|
}))
|
||||||
|
}, [handlePromptSelect, enabledMCPs])
|
||||||
|
|
||||||
|
const openPromptList = useCallback(async () => {
|
||||||
|
const prompts = await promptList
|
||||||
|
quickPanel.open({
|
||||||
|
title: t('settings.mcp.title'),
|
||||||
|
list: prompts,
|
||||||
|
symbol: 'mcp-prompt',
|
||||||
|
multiple: true
|
||||||
|
})
|
||||||
|
}, [promptList, quickPanel, t])
|
||||||
|
|
||||||
const handleOpenQuickPanel = useCallback(() => {
|
const handleOpenQuickPanel = useCallback(() => {
|
||||||
if (quickPanel.isVisible && quickPanel.symbol === 'mcp') {
|
if (quickPanel.isVisible && quickPanel.symbol === 'mcp') {
|
||||||
@ -66,7 +292,8 @@ const MCPToolsButton: FC<Props> = ({ ref, enabledMCPs, toggelEnableMCP, ToolbarB
|
|||||||
}, [openQuickPanel, quickPanel])
|
}, [openQuickPanel, quickPanel])
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
openQuickPanel
|
openQuickPanel,
|
||||||
|
openPromptList
|
||||||
}))
|
}))
|
||||||
|
|
||||||
if (activedMcpServers.length === 0) {
|
if (activedMcpServers.length === 0) {
|
||||||
|
|||||||
96
src/renderer/src/pages/settings/MCPSettings/McpPrompt.tsx
Normal file
96
src/renderer/src/pages/settings/MCPSettings/McpPrompt.tsx
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import { MCPPrompt } from '@renderer/types'
|
||||||
|
import { Collapse, Descriptions, Empty, Flex, Tag, Tooltip, Typography } from 'antd'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
interface MCPPromptsSectionProps {
|
||||||
|
prompts: MCPPrompt[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const MCPPromptsSection = ({ prompts }: MCPPromptsSectionProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
// Render prompt arguments
|
||||||
|
const renderPromptArguments = (prompt: MCPPrompt) => {
|
||||||
|
if (!prompt.arguments || prompt.arguments.length === 0) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ marginTop: 12 }}>
|
||||||
|
<Typography.Title level={5}>{t('settings.mcp.tools.inputSchema')}:</Typography.Title>
|
||||||
|
<Descriptions bordered size="small" column={1} style={{ marginTop: 8 }}>
|
||||||
|
{prompt.arguments.map((arg, index) => (
|
||||||
|
<Descriptions.Item
|
||||||
|
key={index}
|
||||||
|
label={
|
||||||
|
<Flex align="center" gap={8}>
|
||||||
|
<Typography.Text strong>{arg.name}</Typography.Text>
|
||||||
|
{arg.required && (
|
||||||
|
<Tooltip title="Required field">
|
||||||
|
<Tag color="red">Required</Tag>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
}>
|
||||||
|
<Flex vertical gap={4}>
|
||||||
|
{arg.description && (
|
||||||
|
<Typography.Paragraph type="secondary" style={{ marginBottom: 0, marginTop: 4 }}>
|
||||||
|
{arg.description}
|
||||||
|
</Typography.Paragraph>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</Descriptions.Item>
|
||||||
|
))}
|
||||||
|
</Descriptions>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Section>
|
||||||
|
<SectionTitle>{t('settings.mcp.prompts.availablePrompts')}</SectionTitle>
|
||||||
|
{prompts.length > 0 ? (
|
||||||
|
<Collapse bordered={false} ghost>
|
||||||
|
{prompts.map((prompt) => (
|
||||||
|
<Collapse.Panel
|
||||||
|
key={prompt.id || prompt.name}
|
||||||
|
header={
|
||||||
|
<Flex vertical align="flex-start">
|
||||||
|
<Flex align="center" style={{ width: '100%' }}>
|
||||||
|
<Typography.Text strong>{prompt.name}</Typography.Text>
|
||||||
|
</Flex>
|
||||||
|
{prompt.description && (
|
||||||
|
<Typography.Text type="secondary" style={{ fontSize: '13px', marginTop: 4 }}>
|
||||||
|
{prompt.description}
|
||||||
|
</Typography.Text>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
}>
|
||||||
|
<SelectableContent>{renderPromptArguments(prompt)}</SelectableContent>
|
||||||
|
</Collapse.Panel>
|
||||||
|
))}
|
||||||
|
</Collapse>
|
||||||
|
) : (
|
||||||
|
<Empty description={t('settings.mcp.prompts.noPromptsAvailable')} image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||||
|
)}
|
||||||
|
</Section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Section = styled.div`
|
||||||
|
margin-top: 8px;
|
||||||
|
padding-top: 8px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const SectionTitle = styled.h3`
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
`
|
||||||
|
|
||||||
|
const SelectableContent = styled.div`
|
||||||
|
user-select: text;
|
||||||
|
padding: 0 12px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default MCPPromptsSection
|
||||||
@ -1,13 +1,14 @@
|
|||||||
import { DeleteOutlined, SaveOutlined } from '@ant-design/icons'
|
import { DeleteOutlined, SaveOutlined } from '@ant-design/icons'
|
||||||
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
||||||
import { MCPServer, MCPTool } from '@renderer/types'
|
import { MCPPrompt, MCPServer, MCPTool } from '@renderer/types'
|
||||||
import { Button, Flex, Form, Input, Radio, Switch } from 'antd'
|
import { Button, Flex, Form, Input, Radio, Switch, Tabs } from 'antd'
|
||||||
import TextArea from 'antd/es/input/TextArea'
|
import TextArea from 'antd/es/input/TextArea'
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '..'
|
import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '..'
|
||||||
|
import MCPPromptsSection from './McpPrompt'
|
||||||
import MCPToolsSection from './McpTool'
|
import MCPToolsSection from './McpTool'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -40,6 +41,8 @@ const PipRegistry: Registry[] = [
|
|||||||
{ name: '腾讯云', url: 'https://mirrors.cloud.tencent.com/pypi/simple/' }
|
{ name: '腾讯云', url: 'https://mirrors.cloud.tencent.com/pypi/simple/' }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
type TabKey = 'settings' | 'tools' | 'prompts'
|
||||||
|
|
||||||
const McpSettings: React.FC<Props> = ({ server }) => {
|
const McpSettings: React.FC<Props> = ({ server }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { deleteMCPServer, updateMCPServer } = useMCPServers()
|
const { deleteMCPServer, updateMCPServer } = useMCPServers()
|
||||||
@ -48,8 +51,10 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
|||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [isFormChanged, setIsFormChanged] = useState(false)
|
const [isFormChanged, setIsFormChanged] = useState(false)
|
||||||
const [loadingServer, setLoadingServer] = useState<string | null>(null)
|
const [loadingServer, setLoadingServer] = useState<string | null>(null)
|
||||||
|
const [activeTab, setActiveTab] = useState<TabKey>('settings')
|
||||||
|
|
||||||
const [tools, setTools] = useState<MCPTool[]>([])
|
const [tools, setTools] = useState<MCPTool[]>([])
|
||||||
|
const [prompts, setPrompts] = useState<MCPPrompt[]>([])
|
||||||
const [isShowRegistry, setIsShowRegistry] = useState(false)
|
const [isShowRegistry, setIsShowRegistry] = useState(false)
|
||||||
const [registry, setRegistry] = useState<Registry[]>()
|
const [registry, setRegistry] = useState<Registry[]>()
|
||||||
|
|
||||||
@ -121,9 +126,28 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetchPrompts = async () => {
|
||||||
|
if (server.isActive) {
|
||||||
|
try {
|
||||||
|
setLoadingServer(server.id)
|
||||||
|
const localPrompts = await window.api.mcp.listPrompts(server)
|
||||||
|
setPrompts(localPrompts)
|
||||||
|
} catch (error) {
|
||||||
|
window.message.error({
|
||||||
|
content: t('settings.mcp.promptsLoadError') + formatError(error),
|
||||||
|
key: 'mcp-prompts-error'
|
||||||
|
})
|
||||||
|
setPrompts([])
|
||||||
|
} finally {
|
||||||
|
setLoadingServer(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (server.isActive) {
|
if (server.isActive) {
|
||||||
fetchTools()
|
fetchTools()
|
||||||
|
fetchPrompts()
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [server.id, server.isActive])
|
}, [server.id, server.isActive])
|
||||||
@ -264,6 +288,9 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
|||||||
if (active) {
|
if (active) {
|
||||||
const localTools = await window.api.mcp.listTools(server)
|
const localTools = await window.api.mcp.listTools(server)
|
||||||
setTools(localTools)
|
setTools(localTools)
|
||||||
|
|
||||||
|
const localPrompts = await window.api.mcp.listPrompts(server)
|
||||||
|
setPrompts(localPrompts)
|
||||||
} else {
|
} else {
|
||||||
await window.api.mcp.stopServer(server)
|
await window.api.mcp.stopServer(server)
|
||||||
}
|
}
|
||||||
@ -309,35 +336,16 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
|||||||
[server, updateMCPServer]
|
[server, updateMCPServer]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
const tabs = [
|
||||||
<SettingContainer>
|
{
|
||||||
<SettingGroup style={{ marginBottom: 0 }}>
|
key: 'settings',
|
||||||
<SettingTitle>
|
label: t('settings.mcp.tabs.general'),
|
||||||
<Flex justify="space-between" align="center" gap={5} style={{ marginRight: 10 }}>
|
children: (
|
||||||
<ServerName className="text-nowrap">{server?.name}</ServerName>
|
|
||||||
{!(server.type === 'inMemory') && (
|
|
||||||
<Button danger icon={<DeleteOutlined />} type="text" onClick={() => onDeleteMcpServer(server)} />
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
<Flex align="center" gap={16}>
|
|
||||||
<Switch
|
|
||||||
value={server.isActive}
|
|
||||||
key={server.id}
|
|
||||||
loading={loadingServer === server.id}
|
|
||||||
onChange={onToggleActive}
|
|
||||||
/>
|
|
||||||
<Button type="primary" icon={<SaveOutlined />} onClick={onSave} loading={loading} disabled={!isFormChanged}>
|
|
||||||
{t('common.save')}
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
</SettingTitle>
|
|
||||||
<SettingDivider />
|
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
onValuesChange={() => setIsFormChanged(true)}
|
onValuesChange={() => setIsFormChanged(true)}
|
||||||
style={{
|
style={{
|
||||||
// height: 'calc(100vh - var(--navbar-height) - 315px)',
|
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
width: 'calc(100% + 10px)',
|
width: 'calc(100% + 10px)',
|
||||||
paddingRight: '10px'
|
paddingRight: '10px'
|
||||||
@ -440,7 +448,58 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
{server.isActive && <MCPToolsSection tools={tools} server={server} onToggleTool={handleToggleTool} />}
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
if (server.isActive) {
|
||||||
|
tabs.push(
|
||||||
|
{
|
||||||
|
key: 'tools',
|
||||||
|
label: t('settings.mcp.tabs.tools'),
|
||||||
|
children: <MCPToolsSection tools={tools} server={server} onToggleTool={handleToggleTool} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'prompts',
|
||||||
|
label: t('settings.mcp.tabs.prompts'),
|
||||||
|
children: <MCPPromptsSection prompts={prompts} />
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingContainer>
|
||||||
|
<SettingGroup style={{ marginBottom: 0 }}>
|
||||||
|
<SettingTitle>
|
||||||
|
<Flex justify="space-between" align="center" gap={5} style={{ marginRight: 10 }}>
|
||||||
|
<ServerName className="text-nowrap">{server?.name}</ServerName>
|
||||||
|
<Button danger icon={<DeleteOutlined />} type="text" onClick={() => onDeleteMcpServer(server)} />
|
||||||
|
</Flex>
|
||||||
|
<Flex align="center" gap={16}>
|
||||||
|
<Switch
|
||||||
|
value={server.isActive}
|
||||||
|
key={server.id}
|
||||||
|
loading={loadingServer === server.id}
|
||||||
|
onChange={onToggleActive}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<SaveOutlined />}
|
||||||
|
onClick={onSave}
|
||||||
|
loading={loading}
|
||||||
|
disabled={!isFormChanged || activeTab !== 'settings'}>
|
||||||
|
{t('common.save')}
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</SettingTitle>
|
||||||
|
<SettingDivider />
|
||||||
|
|
||||||
|
<Tabs
|
||||||
|
defaultActiveKey="settings"
|
||||||
|
items={tabs}
|
||||||
|
onChange={(key) => setActiveTab(key as TabKey)}
|
||||||
|
style={{ marginTop: 8 }}
|
||||||
|
/>
|
||||||
</SettingGroup>
|
</SettingGroup>
|
||||||
</SettingContainer>
|
</SettingContainer>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -112,7 +112,7 @@ const MCPToolsSection = ({ tools, server, onToggleTool }: MCPToolsSectionProps)
|
|||||||
</Flex>
|
</Flex>
|
||||||
{tool.description && (
|
{tool.description && (
|
||||||
<Typography.Text type="secondary" style={{ fontSize: '13px', marginTop: 4 }}>
|
<Typography.Text type="secondary" style={{ fontSize: '13px', marginTop: 4 }}>
|
||||||
{tool.description}
|
{tool.description.length > 100 ? `${tool.description.substring(0, 100)}...` : tool.description}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
@ -138,7 +138,6 @@ const MCPToolsSection = ({ tools, server, onToggleTool }: MCPToolsSectionProps)
|
|||||||
|
|
||||||
const Section = styled.div`
|
const Section = styled.div`
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
border-top: 1px solid var(--color-border);
|
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|||||||
@ -180,6 +180,7 @@ const NpxSearch: FC = () => {
|
|||||||
|
|
||||||
if (buildInServer) {
|
if (buildInServer) {
|
||||||
addMCPServer(buildInServer)
|
addMCPServer(buildInServer)
|
||||||
|
window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-add-server' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,6 +193,7 @@ const NpxSearch: FC = () => {
|
|||||||
isActive: false,
|
isActive: false,
|
||||||
type: record.type
|
type: record.type
|
||||||
})
|
})
|
||||||
|
window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-add-server' })
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@ -402,6 +402,34 @@ export interface MCPTool {
|
|||||||
inputSchema: MCPToolInputSchema
|
inputSchema: MCPToolInputSchema
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MCPPromptArguments {
|
||||||
|
name: string
|
||||||
|
description?: string
|
||||||
|
required?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MCPPrompt {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
description?: string
|
||||||
|
arguments?: MCPPromptArguments[]
|
||||||
|
serverId: string
|
||||||
|
serverName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetMCPPromptResponse {
|
||||||
|
description?: string
|
||||||
|
messages: {
|
||||||
|
role: string
|
||||||
|
content: {
|
||||||
|
type: 'text' | 'image' | 'audio' | 'resource'
|
||||||
|
text?: string
|
||||||
|
data?: string
|
||||||
|
mimeType?: string
|
||||||
|
}
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
export interface MCPConfig {
|
export interface MCPConfig {
|
||||||
servers: MCPServer[]
|
servers: MCPServer[]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user