From e22d076d67f6c12d27baeb0b5015147b17acac8e Mon Sep 17 00:00:00 2001 From: LiuVaayne <10231735+vaayne@users.noreply.github.com> Date: Sun, 13 Apr 2025 21:08:57 +0800 Subject: [PATCH] feat(MCP): add resource management features and localization support (#4746) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(MCP): add resource management features and localization support * feat(MCP): enhance resource handling with improved error messages and response structure * fix(MCPToolsButton): add missing useEffect import for resource handling --------- Co-authored-by: 亢奋猫 --- packages/shared/IpcChannel.ts | 2 + src/main/ipc.ts | 2 + src/main/services/MCPService.ts | 93 +++++++++++++- src/preload/index.d.ts | 4 +- src/preload/index.ts | 3 + src/renderer/src/i18n/locales/en-us.json | 10 ++ src/renderer/src/i18n/locales/ja-jp.json | 10 ++ src/renderer/src/i18n/locales/ru-ru.json | 10 ++ src/renderer/src/i18n/locales/zh-cn.json | 10 ++ src/renderer/src/i18n/locales/zh-tw.json | 10 ++ .../src/pages/home/Inputbar/Inputbar.tsx | 11 +- .../pages/home/Inputbar/MCPToolsButton.tsx | 115 +++++++++++++++++- .../settings/MCPSettings/McpResource.tsx | 108 ++++++++++++++++ .../settings/MCPSettings/McpSettings.tsx | 33 ++++- src/renderer/src/types/index.ts | 16 +++ 15 files changed, 426 insertions(+), 11 deletions(-) create mode 100644 src/renderer/src/pages/settings/MCPSettings/McpResource.tsx diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index 3576b119..176f5a36 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -41,6 +41,8 @@ export enum IpcChannel { Mcp_CallTool = 'mcp:call-tool', Mcp_ListPrompts = 'mcp:list-prompts', Mcp_GetPrompt = 'mcp:get-prompt', + Mcp_ListResources = 'mcp:list-resources', + Mcp_GetResource = 'mcp:get-resource', Mcp_GetInstallInfo = 'mcp:get-install-info', Mcp_ServersChanged = 'mcp:servers-changed', Mcp_ServersUpdated = 'mcp:servers-updated', diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 18a61ffe..d5dd4936 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -275,6 +275,8 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { 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_ListResources, mcpService.listResources) + ipcMain.handle(IpcChannel.Mcp_GetResource, mcpService.getResource) ipcMain.handle(IpcChannel.Mcp_GetInstallInfo, mcpService.getInstallInfo) ipcMain.handle(IpcChannel.App_IsBinaryExist, (_, name: string) => isBinaryExists(name)) diff --git a/src/main/services/MCPService.ts b/src/main/services/MCPService.ts index 52105be8..e3b93889 100644 --- a/src/main/services/MCPService.ts +++ b/src/main/services/MCPService.ts @@ -10,7 +10,7 @@ import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js' import { getDefaultEnvironment, StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory' import { nanoid } from '@reduxjs/toolkit' -import { GetMCPPromptResponse, MCPPrompt, MCPServer, MCPTool } from '@types' +import { GetMCPPromptResponse, GetResourceResponse, MCPPrompt, MCPResource, MCPServer, MCPTool } from '@types' import { app } from 'electron' import Logger from 'electron-log' @@ -71,6 +71,8 @@ class McpService { this.callTool = this.callTool.bind(this) this.listPrompts = this.listPrompts.bind(this) this.getPrompt = this.getPrompt.bind(this) + this.listResources = this.listResources.bind(this) + this.getResource = this.getResource.bind(this) this.closeClient = this.closeClient.bind(this) this.removeServer = this.removeServer.bind(this) this.restartServer = this.restartServer.bind(this) @@ -117,9 +119,9 @@ class McpService { try { await inMemoryServer.connect(serverTransport) Logger.info(`[MCP] In-memory server started: ${server.name}`) - } catch (error) { + } catch (error: Error | any) { Logger.error(`[MCP] Error starting in-memory server: ${error}`) - throw new Error(`Failed to start in-memory server: ${error}`) + throw new Error(`Failed to start in-memory server: ${error.message}`) } // set the client transport to the client transport = clientTransport @@ -203,7 +205,7 @@ class McpService { return client } catch (error: any) { Logger.error(`[MCP] Error activating server ${server.name}:`, error) - throw error + throw new Error(`[MCP] Error activating server ${server.name}: ${error.message}`) } } @@ -385,6 +387,89 @@ class McpService { return await cachedGetPrompt(server, name, args) } + /** + * List resources available on an MCP server (implementation) + */ + private async listResourcesImpl(server: MCPServer): Promise { + Logger.info(`[MCP] Listing resources for server: ${server.name}`) + const client = await this.initClient(server) + try { + const result = await client.listResources() + const resources = result.resources || [] + const serverResources = (Array.isArray(resources) ? resources : []).map((resource: any) => ({ + ...resource, + serverId: server.id, + serverName: server.name + })) + return serverResources + } catch (error) { + Logger.error(`[MCP] Failed to list resources for server: ${server.name}`, error) + return [] + } + } + + /** + * List resources available on an MCP server with caching + */ + public async listResources(_: Electron.IpcMainInvokeEvent, server: MCPServer): Promise { + const cachedListResources = withCache<[MCPServer], MCPResource[]>( + this.listResourcesImpl.bind(this), + (server) => { + const serverKey = this.getServerKey(server) + return `mcp:list_resources:${serverKey}` + }, + 60 * 60 * 1000, // 60 minutes TTL + `[MCP] Resources from ${server.name}` + ) + return cachedListResources(server) + } + + /** + * Get a specific resource from an MCP server (implementation) + */ + private async getResourceImpl(server: MCPServer, uri: string): Promise { + Logger.info(`[MCP] Getting resource ${uri} from server: ${server.name}`) + const client = await this.initClient(server) + try { + const result = await client.readResource({ uri: uri }) + const contents: MCPResource[] = [] + if (result.contents && result.contents.length > 0) { + result.contents.forEach((content: any) => { + contents.push({ + ...content, + serverId: server.id, + serverName: server.name + }) + }) + } + return { + contents: contents + } + } catch (error: Error | any) { + Logger.error(`[MCP] Failed to get resource ${uri} from server: ${server.name}`, error) + throw new Error(`Failed to get resource ${uri} from server: ${server.name}: ${error.message}`) + } + } + + /** + * Get a specific resource from an MCP server with caching + */ + public async getResource( + _: Electron.IpcMainInvokeEvent, + { server, uri }: { server: MCPServer; uri: string } + ): Promise { + const cachedGetResource = withCache<[MCPServer, string], GetResourceResponse>( + this.getResourceImpl.bind(this), + (server, uri) => { + const serverKey = this.getServerKey(server) + return `mcp:get_resource:${serverKey}:${uri}` + }, + 30 * 60 * 1000, // 30 minutes TTL + `[MCP] Resource ${uri} from ${server.name}` + ) + return await cachedGetResource(server, uri) + } + /** * Get enhanced PATH including common tool locations */ diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 8ba792a3..0ebbc48f 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -1,7 +1,7 @@ import { ExtractChunkData } from '@cherrystudio/embedjs-interfaces' import { ElectronAPI } from '@electron-toolkit/preload' import type { FileMetadataResponse, ListFilesResponse, UploadFileResponse } from '@google/generative-ai/server' -import type { MCPServer, MCPTool } from '@renderer/types' +import type { GetMCPPromptResponse, MCPPrompt, MCPResource, MCPServer, MCPTool } from '@renderer/types' import { AppInfo, FileType, KnowledgeBaseParams, KnowledgeItem, LanguageVarious, WebDavConfig } from '@renderer/types' import type { LoaderReturn } from '@shared/config/types' import type { OpenDialogOptions } from 'electron' @@ -161,6 +161,8 @@ declare global { name: string args?: Record }) => Promise + listResources: (server: MCPServer) => Promise + getResource: ({ server, uri }: { server: MCPServer; uri: string }) => Promise getInstallInfo: () => Promise<{ dir: string; uvPath: string; bunPath: string }> } copilot: { diff --git a/src/preload/index.ts b/src/preload/index.ts index 0928a56d..664acd9c 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -135,6 +135,9 @@ const api = { listPrompts: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_ListPrompts, server), getPrompt: ({ server, name, args }: { server: MCPServer; name: string; args?: Record }) => ipcRenderer.invoke(IpcChannel.Mcp_GetPrompt, { server, name, args }), + listResources: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_ListResources, server), + getResource: ({ server, uri }: { server: MCPServer; uri: string }) => + ipcRenderer.invoke(IpcChannel.Mcp_GetResource, { server, uri }), getInstallInfo: () => ipcRenderer.invoke(IpcChannel.Mcp_GetInstallInfo) }, shell: { diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 438a4d1f..83b64321 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -1112,6 +1112,16 @@ "genericError": "Get prompt Error", "loadError": "Get prompts Error" }, + "resources": { + "noResourcesAvailable": "No resources available", + "availableResources": "Available Resources", + "uri": "URI", + "mimeType": "MIME Type", + "size": "Size", + "blob": "Blob", + "blobInvisible": "Blob Invisible", + "text": "Text" + }, "deleteServer": "Delete Server", "deleteServerConfirm": "Are you sure you want to delete this server?", "registry": "Package Registry", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 602bbe85..3c04c690 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -1111,6 +1111,16 @@ "genericError": "プロンプト取得エラー", "loadError": "プロンプト取得エラー" }, + "resources": { + "noResourcesAvailable": "利用可能なリソースはありません", + "availableResources": "利用可能なリソース", + "uri": "URI", + "mimeType": "MIMEタイプ", + "size": "サイズ", + "blob": "バイナリデータ", + "blobInvisible": "バイナリデータを非表示", + "text": "テキスト" + }, "deleteServer": "サーバーを削除", "deleteServerConfirm": "このサーバーを削除してもよろしいですか?", "registry": "パッケージ管理レジストリ", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 4e42ba17..beabbd0b 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -1111,6 +1111,16 @@ "genericError": "Ошибка получения подсказки", "loadError": "Ошибка получения подсказок" }, + "resources": { + "noResourcesAvailable": "Нет доступных ресурсов", + "availableResources": "Доступные ресурсы", + "uri": "URI", + "mimeType": "MIME-тип", + "size": "Размер", + "blob": "Двоичные данные", + "blobInvisible": "Скрытые двоичные данные", + "text": "Текст" + }, "deleteServer": "Удалить сервер", "deleteServerConfirm": "Вы уверены, что хотите удалить этот сервер?", "registry": "Реестр пакетов", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 07e63538..4071368e 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1112,6 +1112,16 @@ "genericError": "获取提示错误", "loadError": "获取提示失败" }, + "resources": { + "noResourcesAvailable": "无可用资源", + "availableResources": "可用资源", + "uri": "URI", + "mimeType": "MIME类型", + "size": "大小", + "blob": "二进制数据", + "blobInvisible": "隐藏二进制数据", + "text": "文本" + }, "deleteServer": "删除服务器", "deleteServerConfirm": "确定要删除此服务器吗?", "registry": "包管理源", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 1c03bad1..0700d760 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1111,6 +1111,16 @@ "genericError": "獲取提示錯誤", "loadError": "獲取提示失敗" }, + "resources": { + "noResourcesAvailable": "無可用資源", + "availableResources": "可用資源", + "uri": "URI", + "mimeType": "MIME類型", + "size": "大小", + "blob": "二進位數據", + "blobInvisible": "隱藏二進位數據", + "text": "文字" + }, "deleteServer": "刪除伺服器", "deleteServerConfirm": "確定要刪除此伺服器嗎?", "registry": "套件管理源", diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx index a170ece1..00d6cb75 100644 --- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx @@ -355,7 +355,7 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = } }, { - label: 'MCP Prompt', + label: `MCP ${t('settings.mcp.tabs.prompts')}`, description: '', icon: , isMenu: true, @@ -363,6 +363,15 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = mcpToolsButtonRef.current?.openPromptList() } }, + { + label: `MCP ${t('settings.mcp.tabs.resources')}`, + description: '', + icon: , + isMenu: true, + action: () => { + mcpToolsButtonRef.current?.openResourcesList() + } + }, { label: isVisionModel(model) ? t('chat.input.upload') : t('chat.input.upload.document'), description: '', diff --git a/src/renderer/src/pages/home/Inputbar/MCPToolsButton.tsx b/src/renderer/src/pages/home/Inputbar/MCPToolsButton.tsx index 8d20b91c..df947dd3 100644 --- a/src/renderer/src/pages/home/Inputbar/MCPToolsButton.tsx +++ b/src/renderer/src/pages/home/Inputbar/MCPToolsButton.tsx @@ -1,16 +1,17 @@ import { CodeOutlined, PlusOutlined } from '@ant-design/icons' import { QuickPanelListItem, useQuickPanel } from '@renderer/components/QuickPanel' import { useMCPServers } from '@renderer/hooks/useMCPServers' -import { MCPPrompt, MCPServer } from '@renderer/types' +import { MCPPrompt, MCPResource, MCPServer } from '@renderer/types' import { Form, Input, Modal, Tooltip } from 'antd' import { SquareTerminal } from 'lucide-react' -import { FC, useCallback, useImperativeHandle, useMemo } from 'react' +import { FC, useCallback, useEffect, useImperativeHandle, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router' export interface MCPToolsButtonRef { openQuickPanel: () => void openPromptList: () => void + openResourcesList: () => void } interface Props { @@ -168,6 +169,7 @@ const MCPToolsButton: FC = ({ const handlePromptSelect = useCallback( (prompt: MCPPrompt) => { + // Using a 10ms delay to ensure the modal or UI updates are fully rendered before executing the logic. setTimeout(async () => { const server = enabledMCPs.find((s) => s.id === prompt.serverId) if (server) { @@ -284,6 +286,112 @@ const MCPToolsButton: FC = ({ }) }, [promptList, quickPanel, t]) + const handleResourceSelect = useCallback( + (resource: MCPResource) => { + setTimeout(async () => { + const server = enabledMCPs.find((s) => s.id === resource.serverId) + if (server) { + try { + // Fetch the resource data + const response = await window.api.mcp.getResource({ + server, + uri: resource.uri + }) + console.log('Resource Data:', response) + + // Check if the response has the expected structure + if (response && response.contents && Array.isArray(response.contents)) { + // Process each resource in the contents array + for (const resourceData of response.contents) { + // Determine how to handle the resource based on its MIME type + if (resourceData.blob) { + // Handle binary data (images, etc.) + if (resourceData.mimeType?.startsWith('image/')) { + // Insert image as markdown + const imageMarkdown = `![${resourceData.name || 'Image'}](data:${resourceData.mimeType};base64,${resourceData.blob})` + insertPromptIntoTextArea(imageMarkdown) + } else { + // For other binary types, just mention it's available + const resourceInfo = `[${resourceData.name || resource.name} - ${resourceData.mimeType || t('settings.mcp.resources.blobInvisible')}]` + insertPromptIntoTextArea(resourceInfo) + } + } else if (resourceData.text) { + // Handle text data + insertPromptIntoTextArea(resourceData.text) + } else { + // Fallback for resources without content + const resourceInfo = `[${resourceData.name || resource.name} - ${resourceData.uri || resource.uri}]` + insertPromptIntoTextArea(resourceInfo) + } + } + } else { + // Handle legacy format or direct resource data + const resourceData = response + + // Determine how to handle the resource based on its MIME type + if (resourceData.blob) { + // Handle binary data (images, etc.) + if (resourceData.mimeType?.startsWith('image/')) { + // Insert image as markdown + const imageMarkdown = `![${resourceData.name || resource.name}](data:${resourceData.mimeType};base64,${resourceData.blob})` + insertPromptIntoTextArea(imageMarkdown) + } else { + // For other binary types, just mention it's available + const resourceInfo = `[${resourceData.name || resource.name} - ${resourceData.mimeType || t('settings.mcp.resources.blobInvisible')}]` + insertPromptIntoTextArea(resourceInfo) + } + } else if (resourceData.text) { + // Handle text data + insertPromptIntoTextArea(resourceData.text) + } else { + // Fallback for resources without content + const resourceInfo = `[${resourceData.name || resource.name} - ${resourceData.uri || resource.uri}]` + insertPromptIntoTextArea(resourceInfo) + } + } + } catch (error: Error | any) { + Modal.error({ + title: t('common.error'), + content: error.message || t('settings.mcp.resources.genericError') + }) + } + } + }, 10) + }, + [enabledMCPs, t, insertPromptIntoTextArea] + ) + const [resourcesList, setResourcesList] = useState([]) + + useEffect(() => { + const fetchResources = async () => { + const resources: MCPResource[] = [] + for (const server of enabledMCPs) { + const serverResources = await window.api.mcp.listResources(server) + resources.push(...serverResources) + } + setResourcesList( + resources.map((resource) => ({ + label: resource.name, + description: resource.description, + icon: , + action: () => handleResourceSelect(resource) + })) + ) + } + + fetchResources() + }, [handleResourceSelect, enabledMCPs]) + + const openResourcesList = useCallback(async () => { + const resources = resourcesList + quickPanel.open({ + title: t('settings.mcp.title'), + list: resources, + symbol: 'mcp-resource', + multiple: true + }) + }, [resourcesList, quickPanel, t]) + const handleOpenQuickPanel = useCallback(() => { if (quickPanel.isVisible && quickPanel.symbol === 'mcp') { quickPanel.close() @@ -294,7 +402,8 @@ const MCPToolsButton: FC = ({ useImperativeHandle(ref, () => ({ openQuickPanel, - openPromptList + openPromptList, + openResourcesList })) if (activedMcpServers.length === 0) { diff --git a/src/renderer/src/pages/settings/MCPSettings/McpResource.tsx b/src/renderer/src/pages/settings/MCPSettings/McpResource.tsx new file mode 100644 index 00000000..80660c98 --- /dev/null +++ b/src/renderer/src/pages/settings/MCPSettings/McpResource.tsx @@ -0,0 +1,108 @@ +import { MCPResource } from '@renderer/types' +import { Collapse, Descriptions, Empty, Flex, Tag, Typography } from 'antd' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +interface MCPResourcesSectionProps { + resources: MCPResource[] +} + +const MCPResourcesSection = ({ resources }: MCPResourcesSectionProps) => { + const { t } = useTranslation() + + // Format file size to human-readable format + const formatFileSize = (size?: number) => { + if (size === undefined) return 'Unknown size' + + const units = ['B', 'KB', 'MB', 'GB', 'TB'] + let formattedSize = size + let unitIndex = 0 + + while (formattedSize >= 1024 && unitIndex < units.length - 1) { + formattedSize /= 1024 + unitIndex++ + } + + return `${formattedSize.toFixed(2)} ${units[unitIndex]}` + } + + // Render resource properties + const renderResourceProperties = (resource: MCPResource) => { + return ( + + {resource.mimeType && ( + + {resource.mimeType} + + )} + {resource.size !== undefined && ( + + {formatFileSize(resource.size)} + + )} + {resource.text && ( + {resource.text} + )} + {resource.blob && ( + + {t('settings.mcp.resources.blobInvisible') || 'Binary data is not visible here.'} + + )} + + ) + } + + return ( +
+ {t('settings.mcp.resources.availableResources') || 'Available Resources'} + {resources.length > 0 ? ( + + {resources.map((resource) => ( + + + {`${resource.name} (${resource.uri})`} + + {resource.description && ( + + {resource.description.length > 100 + ? `${resource.description.substring(0, 100)}...` + : resource.description} + + )} + + }> + {renderResourceProperties(resource)} + + ))} + + ) : ( + + )} +
+ ) +} + +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 MCPResourcesSection diff --git a/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx b/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx index 499e7484..45a06953 100644 --- a/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx @@ -1,6 +1,6 @@ import { DeleteOutlined, SaveOutlined } from '@ant-design/icons' import { useMCPServers } from '@renderer/hooks/useMCPServers' -import { MCPPrompt, MCPServer, MCPTool } from '@renderer/types' +import { MCPPrompt, MCPResource, MCPServer, MCPTool } from '@renderer/types' import { Button, Flex, Form, Input, Radio, Switch, Tabs } from 'antd' import TextArea from 'antd/es/input/TextArea' import React, { useCallback, useEffect, useState } from 'react' @@ -10,6 +10,7 @@ import styled from 'styled-components' import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '..' import MCPPromptsSection from './McpPrompt' +import MCPResourcesSection from './McpResource' import MCPToolsSection from './McpTool' interface Props { @@ -42,7 +43,7 @@ const PipRegistry: Registry[] = [ { name: '腾讯云', url: 'https://mirrors.cloud.tencent.com/pypi/simple/' } ] -type TabKey = 'settings' | 'tools' | 'prompts' +type TabKey = 'settings' | 'tools' | 'prompts' | 'resources' const McpSettings: React.FC = ({ server }) => { const { t } = useTranslation() @@ -56,6 +57,7 @@ const McpSettings: React.FC = ({ server }) => { const [tools, setTools] = useState([]) const [prompts, setPrompts] = useState([]) + const [resources, setResources] = useState([]) const [isShowRegistry, setIsShowRegistry] = useState(false) const [registry, setRegistry] = useState() @@ -146,10 +148,29 @@ const McpSettings: React.FC = ({ server }) => { } } + const fetchResources = async () => { + if (server.isActive) { + try { + setLoadingServer(server.id) + const localResources = await window.api.mcp.listResources(server) + setResources(localResources) + } catch (error) { + window.message.error({ + content: t('settings.mcp.resources.loadError') + ' ' + formatError(error), + key: 'mcp-resources-error' + }) + setResources([]) + } finally { + setLoadingServer(null) + } + } + } + useEffect(() => { if (server.isActive) { fetchTools() fetchPrompts() + fetchResources() } // eslint-disable-next-line react-hooks/exhaustive-deps }, [server.id, server.isActive]) @@ -294,6 +315,9 @@ const McpSettings: React.FC = ({ server }) => { const localPrompts = await window.api.mcp.listPrompts(server) setPrompts(localPrompts) + + const localResources = await window.api.mcp.listResources(server) + setResources(localResources) } else { await window.api.mcp.stopServer(server) } @@ -466,6 +490,11 @@ const McpSettings: React.FC = ({ server }) => { key: 'prompts', label: t('settings.mcp.tabs.prompts'), children: + }, + { + key: 'resources', + label: t('settings.mcp.tabs.resources'), + children: } ) } diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index efb8004e..a3800506 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -444,6 +444,22 @@ export interface MCPToolResponse { response?: any } +export interface MCPResource { + serverId: string + serverName: string + uri: string + name: string + description?: string + mimeType?: string + size?: number + text?: string + blob?: string +} + +export interface GetResourceResponse { + contents: MCPResource[] +} + export interface QuickPhrase { id: string title: string