feat(MCP): add resource management features and localization support (#4746)
* 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: 亢奋猫 <kangfenmao@qq.com>
This commit is contained in:
parent
de7f806bbc
commit
e22d076d67
@ -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',
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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<MCPResource[]> {
|
||||
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<MCPResource[]> {
|
||||
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<GetResourceResponse> {
|
||||
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<GetResourceResponse> {
|
||||
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
|
||||
*/
|
||||
|
||||
4
src/preload/index.d.ts
vendored
4
src/preload/index.d.ts
vendored
@ -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<string, any>
|
||||
}) => Promise<GetMCPPromptResponse>
|
||||
listResources: (server: MCPServer) => Promise<MCPResource[]>
|
||||
getResource: ({ server, uri }: { server: MCPServer; uri: string }) => Promise<GetResourceResponse>
|
||||
getInstallInfo: () => Promise<{ dir: string; uvPath: string; bunPath: string }>
|
||||
}
|
||||
copilot: {
|
||||
|
||||
@ -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<string, any> }) =>
|
||||
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: {
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -1111,6 +1111,16 @@
|
||||
"genericError": "プロンプト取得エラー",
|
||||
"loadError": "プロンプト取得エラー"
|
||||
},
|
||||
"resources": {
|
||||
"noResourcesAvailable": "利用可能なリソースはありません",
|
||||
"availableResources": "利用可能なリソース",
|
||||
"uri": "URI",
|
||||
"mimeType": "MIMEタイプ",
|
||||
"size": "サイズ",
|
||||
"blob": "バイナリデータ",
|
||||
"blobInvisible": "バイナリデータを非表示",
|
||||
"text": "テキスト"
|
||||
},
|
||||
"deleteServer": "サーバーを削除",
|
||||
"deleteServerConfirm": "このサーバーを削除してもよろしいですか?",
|
||||
"registry": "パッケージ管理レジストリ",
|
||||
|
||||
@ -1111,6 +1111,16 @@
|
||||
"genericError": "Ошибка получения подсказки",
|
||||
"loadError": "Ошибка получения подсказок"
|
||||
},
|
||||
"resources": {
|
||||
"noResourcesAvailable": "Нет доступных ресурсов",
|
||||
"availableResources": "Доступные ресурсы",
|
||||
"uri": "URI",
|
||||
"mimeType": "MIME-тип",
|
||||
"size": "Размер",
|
||||
"blob": "Двоичные данные",
|
||||
"blobInvisible": "Скрытые двоичные данные",
|
||||
"text": "Текст"
|
||||
},
|
||||
"deleteServer": "Удалить сервер",
|
||||
"deleteServerConfirm": "Вы уверены, что хотите удалить этот сервер?",
|
||||
"registry": "Реестр пакетов",
|
||||
|
||||
@ -1112,6 +1112,16 @@
|
||||
"genericError": "获取提示错误",
|
||||
"loadError": "获取提示失败"
|
||||
},
|
||||
"resources": {
|
||||
"noResourcesAvailable": "无可用资源",
|
||||
"availableResources": "可用资源",
|
||||
"uri": "URI",
|
||||
"mimeType": "MIME类型",
|
||||
"size": "大小",
|
||||
"blob": "二进制数据",
|
||||
"blobInvisible": "隐藏二进制数据",
|
||||
"text": "文本"
|
||||
},
|
||||
"deleteServer": "删除服务器",
|
||||
"deleteServerConfirm": "确定要删除此服务器吗?",
|
||||
"registry": "包管理源",
|
||||
|
||||
@ -1111,6 +1111,16 @@
|
||||
"genericError": "獲取提示錯誤",
|
||||
"loadError": "獲取提示失敗"
|
||||
},
|
||||
"resources": {
|
||||
"noResourcesAvailable": "無可用資源",
|
||||
"availableResources": "可用資源",
|
||||
"uri": "URI",
|
||||
"mimeType": "MIME類型",
|
||||
"size": "大小",
|
||||
"blob": "二進位數據",
|
||||
"blobInvisible": "隱藏二進位數據",
|
||||
"text": "文字"
|
||||
},
|
||||
"deleteServer": "刪除伺服器",
|
||||
"deleteServerConfirm": "確定要刪除此伺服器嗎?",
|
||||
"registry": "套件管理源",
|
||||
|
||||
@ -355,7 +355,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'MCP Prompt',
|
||||
label: `MCP ${t('settings.mcp.tabs.prompts')}`,
|
||||
description: '',
|
||||
icon: <CodeOutlined />,
|
||||
isMenu: true,
|
||||
@ -363,6 +363,15 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
mcpToolsButtonRef.current?.openPromptList()
|
||||
}
|
||||
},
|
||||
{
|
||||
label: `MCP ${t('settings.mcp.tabs.resources')}`,
|
||||
description: '',
|
||||
icon: <CodeOutlined />,
|
||||
isMenu: true,
|
||||
action: () => {
|
||||
mcpToolsButtonRef.current?.openResourcesList()
|
||||
}
|
||||
},
|
||||
{
|
||||
label: isVisionModel(model) ? t('chat.input.upload') : t('chat.input.upload.document'),
|
||||
description: '',
|
||||
|
||||
@ -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<Props> = ({
|
||||
|
||||
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<Props> = ({
|
||||
})
|
||||
}, [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 = ``
|
||||
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 = ``
|
||||
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<QuickPanelListItem[]>([])
|
||||
|
||||
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: <CodeOutlined />,
|
||||
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<Props> = ({
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
openQuickPanel,
|
||||
openPromptList
|
||||
openPromptList,
|
||||
openResourcesList
|
||||
}))
|
||||
|
||||
if (activedMcpServers.length === 0) {
|
||||
|
||||
108
src/renderer/src/pages/settings/MCPSettings/McpResource.tsx
Normal file
108
src/renderer/src/pages/settings/MCPSettings/McpResource.tsx
Normal file
@ -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 (
|
||||
<Descriptions column={1} size="small" bordered>
|
||||
{resource.mimeType && (
|
||||
<Descriptions.Item label={t('settings.mcp.resources.mimeType') || 'MIME Type'}>
|
||||
<Tag color="blue">{resource.mimeType}</Tag>
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
{resource.size !== undefined && (
|
||||
<Descriptions.Item label={t('settings.mcp.resources.size') || 'Size'}>
|
||||
{formatFileSize(resource.size)}
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
{resource.text && (
|
||||
<Descriptions.Item label={t('settings.mcp.resources.text') || 'Text'}>{resource.text}</Descriptions.Item>
|
||||
)}
|
||||
{resource.blob && (
|
||||
<Descriptions.Item label={t('settings.mcp.resources.blob') || 'Binary Data'}>
|
||||
{t('settings.mcp.resources.blobInvisible') || 'Binary data is not visible here.'}
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
</Descriptions>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<SectionTitle>{t('settings.mcp.resources.availableResources') || 'Available Resources'}</SectionTitle>
|
||||
{resources.length > 0 ? (
|
||||
<Collapse bordered={false} ghost>
|
||||
{resources.map((resource) => (
|
||||
<Collapse.Panel
|
||||
key={resource.uri}
|
||||
header={
|
||||
<Flex vertical align="flex-start" style={{ width: '100%' }}>
|
||||
<Flex align="center" style={{ width: '100%' }}>
|
||||
<Typography.Text strong>{`${resource.name} (${resource.uri})`}</Typography.Text>
|
||||
</Flex>
|
||||
{resource.description && (
|
||||
<Typography.Text type="secondary" style={{ fontSize: '13px', marginTop: 4 }}>
|
||||
{resource.description.length > 100
|
||||
? `${resource.description.substring(0, 100)}...`
|
||||
: resource.description}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</Flex>
|
||||
}>
|
||||
<SelectableContent>{renderResourceProperties(resource)}</SelectableContent>
|
||||
</Collapse.Panel>
|
||||
))}
|
||||
</Collapse>
|
||||
) : (
|
||||
<Empty
|
||||
description={t('settings.mcp.resources.noResourcesAvailable') || 'No resources available'}
|
||||
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 MCPResourcesSection
|
||||
@ -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<Props> = ({ server }) => {
|
||||
const { t } = useTranslation()
|
||||
@ -56,6 +57,7 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
||||
|
||||
const [tools, setTools] = useState<MCPTool[]>([])
|
||||
const [prompts, setPrompts] = useState<MCPPrompt[]>([])
|
||||
const [resources, setResources] = useState<MCPResource[]>([])
|
||||
const [isShowRegistry, setIsShowRegistry] = useState(false)
|
||||
const [registry, setRegistry] = useState<Registry[]>()
|
||||
|
||||
@ -146,10 +148,29 @@ const McpSettings: React.FC<Props> = ({ 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<Props> = ({ 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<Props> = ({ server }) => {
|
||||
key: 'prompts',
|
||||
label: t('settings.mcp.tabs.prompts'),
|
||||
children: <MCPPromptsSection prompts={prompts} />
|
||||
},
|
||||
{
|
||||
key: 'resources',
|
||||
label: t('settings.mcp.tabs.resources'),
|
||||
children: <MCPResourcesSection resources={resources} />
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user