Merge branch 'main' into local-pr-3734
This commit is contained in:
@@ -1,26 +0,0 @@
|
||||
diff --git a/dist/cjs/client/stdio.js b/dist/cjs/client/stdio.js
|
||||
index 2ada8771c5f76673b5021d6453c6bdd7e0b88013..89f6ea9ca7de86294d3d966f3454b98e19bfe534 100644
|
||||
--- a/dist/cjs/client/stdio.js
|
||||
+++ b/dist/cjs/client/stdio.js
|
||||
@@ -68,7 +68,7 @@ class StdioClientTransport {
|
||||
this._process = (0, node_child_process_1.spawn)(this._serverParams.command, (_a = this._serverParams.args) !== null && _a !== void 0 ? _a : [], {
|
||||
env: (_b = this._serverParams.env) !== null && _b !== void 0 ? _b : getDefaultEnvironment(),
|
||||
stdio: ["pipe", "pipe", (_c = this._serverParams.stderr) !== null && _c !== void 0 ? _c : "inherit"],
|
||||
- shell: false,
|
||||
+ shell: process.platform === 'win32' ? true : false,
|
||||
signal: this._abortController.signal,
|
||||
windowsHide: node_process_1.default.platform === "win32" && isElectron(),
|
||||
cwd: this._serverParams.cwd,
|
||||
diff --git a/dist/esm/client/stdio.js b/dist/esm/client/stdio.js
|
||||
index 387c982fd40fd8db9790a78e1a05c9ecb81501c0..7b7e60a306bca73149609015a27e904a0a68ca02 100644
|
||||
--- a/dist/esm/client/stdio.js
|
||||
+++ b/dist/esm/client/stdio.js
|
||||
@@ -61,7 +61,7 @@ export class StdioClientTransport {
|
||||
this._process = spawn(this._serverParams.command, (_a = this._serverParams.args) !== null && _a !== void 0 ? _a : [], {
|
||||
env: (_b = this._serverParams.env) !== null && _b !== void 0 ? _b : getDefaultEnvironment(),
|
||||
stdio: ["pipe", "pipe", (_c = this._serverParams.stderr) !== null && _c !== void 0 ? _c : "inherit"],
|
||||
- shell: false,
|
||||
+ shell: process.platform === 'win32' ? true : false,
|
||||
signal: this._abortController.signal,
|
||||
windowsHide: process.platform === "win32" && isElectron(),
|
||||
cwd: this._serverParams.cwd,
|
||||
@@ -0,0 +1,18 @@
|
||||
diff --git a/dist/index.node.js b/dist/index.node.js
|
||||
index bb108cbc210af5b99e864fd1dd8c555e948ecf7a..8ef8c1aab59215c21d161c0e52125724528ecab8 100644
|
||||
--- a/dist/index.node.js
|
||||
+++ b/dist/index.node.js
|
||||
@@ -1,8 +1,11 @@
|
||||
let crypto;
|
||||
crypto =
|
||||
globalThis.crypto?.webcrypto ?? // Node.js 16 REPL has globalThis.crypto as node:crypto
|
||||
- globalThis.crypto ?? // Node.js 18+
|
||||
- (await import("node:crypto")).webcrypto; // Node.js 16 non-REPL
|
||||
+ globalThis.crypto ?? // Node.js 18+
|
||||
+ (async() => {
|
||||
+ const crypto = await import("node:crypto");
|
||||
+ return crypto.webcrypto;
|
||||
+ })();
|
||||
/**
|
||||
* Creates an array of length `size` of random bytes
|
||||
* @param size
|
||||
+4
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "CherryStudio",
|
||||
"version": "1.1.10",
|
||||
"version": "1.1.12",
|
||||
"private": true,
|
||||
"description": "A powerful AI assistant for producer.",
|
||||
"main": "./out/main/index.js",
|
||||
@@ -67,7 +67,6 @@
|
||||
"@google/generative-ai": "^0.21.0",
|
||||
"@langchain/community": "^0.3.36",
|
||||
"@mistralai/mistralai": "^1.5.2",
|
||||
"@modelcontextprotocol/sdk": "patch:@modelcontextprotocol/sdk@npm%3A1.6.1#~/.yarn/patches/@modelcontextprotocol-sdk-npm-1.6.1-b46313efe7.patch",
|
||||
"@notionhq/client": "^2.2.15",
|
||||
"@tryfabric/martian": "^1.2.4",
|
||||
"@types/react-infinite-scroll-component": "^5.0.0",
|
||||
@@ -109,6 +108,7 @@
|
||||
"@google/genai": "^0.4.0",
|
||||
"@hello-pangea/dnd": "^16.6.0",
|
||||
"@kangfenmao/keyv-storage": "^0.1.0",
|
||||
"@modelcontextprotocol/sdk": "^1.8.0",
|
||||
"@notionhq/client": "^2.2.15",
|
||||
"@reduxjs/toolkit": "^2.2.5",
|
||||
"@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch",
|
||||
@@ -189,7 +189,8 @@
|
||||
"@langchain/openai@npm:^0.3.16": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch",
|
||||
"@langchain/openai@npm:>=0.1.0 <0.4.0": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch",
|
||||
"openai@npm:^4.77.0": "patch:openai@npm%3A4.77.3#~/.yarn/patches/openai-npm-4.77.3-59c6d42e7a.patch",
|
||||
"canvas": "3.1.0"
|
||||
"canvas": "3.1.0",
|
||||
"pkce-challenge@npm:^4.1.0": "patch:pkce-challenge@npm%3A4.1.0#~/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch"
|
||||
},
|
||||
"packageManager": "yarn@4.6.0",
|
||||
"lint-staged": {
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
const { ProxyAgent } = require('undici')
|
||||
const { SocksProxyAgent } = require('socks-proxy-agent')
|
||||
const https = require('https')
|
||||
const fs = require('fs')
|
||||
const { pipeline } = require('stream/promises')
|
||||
|
||||
/**
|
||||
* Downloads a file from a URL with redirect handling
|
||||
@@ -11,42 +8,28 @@ const { pipeline } = require('stream/promises')
|
||||
* @returns {Promise<void>} Promise that resolves when download is complete
|
||||
*/
|
||||
async function downloadWithRedirects(url, destinationPath) {
|
||||
const proxyUrl = process.env.HTTPS_PROXY || process.env.HTTP_PROXY
|
||||
if (proxyUrl.startsWith('socks')) {
|
||||
const proxyAgent = new SocksProxyAgent(proxyUrl)
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = (url) => {
|
||||
https
|
||||
.get(url, { agent: proxyAgent }, (response) => {
|
||||
if (response.statusCode == 301 || response.statusCode == 302) {
|
||||
request(response.headers.location)
|
||||
return
|
||||
}
|
||||
if (response.statusCode !== 200) {
|
||||
reject(new Error(`Download failed: ${response.statusCode} ${response.statusMessage}`))
|
||||
return
|
||||
}
|
||||
const file = fs.createWriteStream(destinationPath)
|
||||
response.pipe(file)
|
||||
file.on('finish', () => resolve())
|
||||
})
|
||||
.on('error', (err) => {
|
||||
reject(err)
|
||||
})
|
||||
}
|
||||
request(url)
|
||||
})
|
||||
} else {
|
||||
const proxyAgent = new ProxyAgent(proxyUrl)
|
||||
const response = await fetch(url, {
|
||||
dispatcher: proxyAgent
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error(`Download failed: ${response.status} ${response.statusText}`)
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = (url) => {
|
||||
https
|
||||
.get(url, (response) => {
|
||||
if (response.statusCode == 301 || response.statusCode == 302) {
|
||||
request(response.headers.location)
|
||||
return
|
||||
}
|
||||
if (response.statusCode !== 200) {
|
||||
reject(new Error(`Download failed: ${response.statusCode} ${response.statusMessage}`))
|
||||
return
|
||||
}
|
||||
const file = fs.createWriteStream(destinationPath)
|
||||
response.pipe(file)
|
||||
file.on('finish', () => resolve())
|
||||
})
|
||||
.on('error', (err) => {
|
||||
reject(err)
|
||||
})
|
||||
}
|
||||
const file = fs.createWriteStream(destinationPath)
|
||||
await pipeline(response.body, file)
|
||||
}
|
||||
request(url)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = { downloadWithRedirects }
|
||||
|
||||
+6
-27
@@ -2,7 +2,7 @@ import fs from 'node:fs'
|
||||
|
||||
import { isMac, isWin } from '@main/constant'
|
||||
import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process'
|
||||
import { LocalFileSource, MCPServer, Shortcut, ThemeMode } from '@types'
|
||||
import { LocalFileSource, Shortcut, ThemeMode } from '@types'
|
||||
import { BrowserWindow, ipcMain, session, shell } from 'electron'
|
||||
import log from 'electron-log'
|
||||
|
||||
@@ -16,7 +16,7 @@ import { FileServiceManager } from './services/file/FileServiceManager'
|
||||
import FileService from './services/FileService'
|
||||
import FileStorage from './services/FileStorage'
|
||||
import KnowledgeService from './services/KnowledgeService'
|
||||
import MCPService from './services/MCPService'
|
||||
import mcpService from './services/MCPService'
|
||||
import * as NutstoreService from './services/NutstoreService'
|
||||
import ObsidianVaultService from './services/ObsidianVaultService'
|
||||
import { ProxyConfig, proxyManager } from './services/ProxyManager'
|
||||
@@ -31,7 +31,6 @@ import { compress, decompress } from './utils/zip'
|
||||
const fileManager = new FileStorage()
|
||||
const backupManager = new BackupManager()
|
||||
const exportService = new ExportService(fileManager)
|
||||
const mcpService = new MCPService()
|
||||
const obsidianVaultService = new ObsidianVaultService()
|
||||
|
||||
export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
@@ -280,36 +279,16 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
)
|
||||
|
||||
// Register MCP handlers
|
||||
ipcMain.on('mcp:servers-from-renderer', (_, servers) => mcpService.setServers(servers))
|
||||
ipcMain.handle('mcp:list-servers', async () => mcpService.listAvailableServices())
|
||||
ipcMain.handle('mcp:add-server', async (_, server: MCPServer) => mcpService.addServer(server))
|
||||
ipcMain.handle('mcp:update-server', async (_, server: MCPServer) => mcpService.updateServer(server))
|
||||
ipcMain.handle('mcp:delete-server', async (_, serverName: string) => mcpService.deleteServer(serverName))
|
||||
ipcMain.handle('mcp:set-server-active', async (_, { name, isActive }) =>
|
||||
mcpService.setServerActive({ name, isActive })
|
||||
)
|
||||
|
||||
// According to preload, this should take no parameters, but our implementation accepts
|
||||
// an optional serverName for better flexibility
|
||||
ipcMain.handle('mcp:list-tools', async (_, serverName?: string) => mcpService.listTools(serverName))
|
||||
ipcMain.handle('mcp:call-tool', async (_, params: { client: string; name: string; args: any }) =>
|
||||
mcpService.callTool(params)
|
||||
)
|
||||
|
||||
ipcMain.handle('mcp:cleanup', async () => mcpService.cleanup())
|
||||
ipcMain.handle('mcp:remove-server', mcpService.removeServer)
|
||||
ipcMain.handle('mcp:list-tools', mcpService.listTools)
|
||||
ipcMain.handle('mcp:call-tool', mcpService.callTool)
|
||||
ipcMain.handle('mcp:get-install-info', mcpService.getInstallInfo)
|
||||
|
||||
ipcMain.handle('app:is-binary-exist', (_, name: string) => isBinaryExists(name))
|
||||
ipcMain.handle('app:get-binary-path', (_, name: string) => getBinaryPath(name))
|
||||
ipcMain.handle('app:install-uv-binary', () => runInstallScript('install-uv.js'))
|
||||
ipcMain.handle('app:install-bun-binary', () => runInstallScript('install-bun.js'))
|
||||
|
||||
// Listen for changes in MCP servers and notify renderer
|
||||
mcpService.on('servers-updated', (servers) => {
|
||||
mainWindow?.webContents.send('mcp:servers-updated', servers)
|
||||
})
|
||||
|
||||
app.on('before-quit', () => mcpService.cleanup())
|
||||
|
||||
//copilot
|
||||
ipcMain.handle('copilot:get-auth-message', CopilotService.getAuthMessage)
|
||||
ipcMain.handle('copilot:get-copilot-token', CopilotService.getCopilotToken)
|
||||
|
||||
@@ -17,4 +17,15 @@ export default abstract class BaseReranker {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
|
||||
public formatErrorMessage(url: string, error: any, requestBody: any) {
|
||||
const errorDetails = {
|
||||
url: url,
|
||||
message: error.message,
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
requestBody: requestBody
|
||||
}
|
||||
return JSON.stringify(errorDetails, null, 2)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,8 +47,10 @@ export default class JinaReranker extends BaseReranker {
|
||||
.filter((doc): doc is ExtractChunkData => doc !== undefined)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
} catch (error: any) {
|
||||
console.error('Jina Reranker API 错误:', error.status)
|
||||
throw new Error(`${error} - BaseUrl: ${baseURL}`)
|
||||
const errorDetails = this.formatErrorMessage(url, error, requestBody)
|
||||
|
||||
console.error('Jina Reranker API Error:', errorDetails)
|
||||
throw new Error(`重排序请求失败: ${error.message}\n请求详情: ${errorDetails}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,8 +49,10 @@ export default class SiliconFlowReranker extends BaseReranker {
|
||||
.filter((doc): doc is ExtractChunkData => doc !== undefined)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
} catch (error: any) {
|
||||
console.error('SiliconFlow Reranker API 错误:', error.status)
|
||||
throw new Error(`${error} - BaseUrl: ${baseURL}`)
|
||||
const errorDetails = this.formatErrorMessage(url, error, requestBody)
|
||||
|
||||
console.error('SiliconFlow Reranker API 错误:', errorDetails)
|
||||
throw new Error(`重排序请求失败: ${error.message}\n请求详情: ${errorDetails}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,8 +53,10 @@ export default class VoyageReranker extends BaseReranker {
|
||||
.filter((doc): doc is ExtractChunkData => doc !== undefined)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
} catch (error: any) {
|
||||
console.error('Voyage Reranker API 错误:', error.message || error)
|
||||
throw new Error(`${error} - BaseUrl: ${baseURL}`)
|
||||
const errorDetails = this.formatErrorMessage(url, error, requestBody)
|
||||
|
||||
console.error('Voyage Reranker API Error:', errorDetails)
|
||||
throw new Error(`重排序请求失败: ${error.message}\n请求详情: ${errorDetails}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 353 KiB |
@@ -481,6 +481,9 @@ class KnowledgeService {
|
||||
_: Electron.IpcMainInvokeEvent,
|
||||
{ search, base, results }: { search: string; base: KnowledgeBaseParams; results: ExtractChunkData[] }
|
||||
): Promise<ExtractChunkData[]> => {
|
||||
if (results.length === 0) {
|
||||
return results
|
||||
}
|
||||
return await new Reranker(base).rerank(search, results)
|
||||
}
|
||||
|
||||
|
||||
+98
-544
@@ -1,615 +1,169 @@
|
||||
import { isLinux, isMac, isWin } from '@main/constant'
|
||||
import { getBinaryPath } from '@main/utils/process'
|
||||
import type { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
||||
import type { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
|
||||
import type { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
||||
import { MCPServer, MCPTool } from '@types'
|
||||
import log from 'electron-log'
|
||||
import { EventEmitter } from 'events'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
|
||||
import { CacheService } from './CacheService'
|
||||
import { windowService } from './WindowService'
|
||||
import { getBinaryName, getBinaryPath } from '@main/utils/process'
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
||||
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
||||
import { MCPServer } from '@types'
|
||||
import { app } from 'electron'
|
||||
import Logger from 'electron-log'
|
||||
|
||||
/**
|
||||
* Service for managing Model Context Protocol servers and tools
|
||||
*/
|
||||
export default class MCPService extends EventEmitter {
|
||||
private servers: MCPServer[] = []
|
||||
private activeServers: Map<string, any> = new Map()
|
||||
private clients: { [key: string]: any } = {}
|
||||
private Client: typeof Client | undefined
|
||||
private stdioTransport: typeof StdioClientTransport | undefined
|
||||
private sseTransport: typeof SSEClientTransport | undefined
|
||||
private initialized = false
|
||||
private initPromise: Promise<void> | null = null
|
||||
class McpService {
|
||||
private client: Client | null = null
|
||||
private clients: Map<string, Client> = new Map()
|
||||
|
||||
// Simplified server loading state management
|
||||
private readyState = {
|
||||
serversLoaded: false,
|
||||
promise: null as Promise<void> | null,
|
||||
resolve: null as ((value: void) => void) | null
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.createServerLoadingPromise()
|
||||
this.init().catch((err) => this.logError('Failed to initialize MCP service', err))
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a promise that resolves when servers are loaded
|
||||
*/
|
||||
private createServerLoadingPromise(): void {
|
||||
this.readyState.promise = new Promise<void>((resolve) => {
|
||||
this.readyState.resolve = resolve
|
||||
private getServerKey(server: MCPServer): string {
|
||||
return JSON.stringify({
|
||||
baseUrl: server.baseUrl,
|
||||
command: server.command,
|
||||
args: server.args,
|
||||
env: server.env,
|
||||
id: server.id
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Set servers received from Redux and trigger initialization if needed
|
||||
*/
|
||||
public setServers(servers: MCPServer[]): void {
|
||||
this.servers = servers
|
||||
log.info(`[MCP] Received ${servers.length} servers from Redux`)
|
||||
|
||||
// Mark servers as loaded and resolve the waiting promise
|
||||
if (!this.readyState.serversLoaded && this.readyState.resolve) {
|
||||
this.readyState.serversLoaded = true
|
||||
this.readyState.resolve()
|
||||
this.readyState.resolve = null
|
||||
}
|
||||
|
||||
// Initialize if not already initialized
|
||||
if (!this.initialized) {
|
||||
this.init().catch((err) => this.logError('Failed to initialize MCP service', err))
|
||||
}
|
||||
constructor() {
|
||||
this.initClient = this.initClient.bind(this)
|
||||
this.listTools = this.listTools.bind(this)
|
||||
this.callTool = this.callTool.bind(this)
|
||||
this.closeClient = this.closeClient.bind(this)
|
||||
this.removeServer = this.removeServer.bind(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the MCP service if not already initialized
|
||||
*/
|
||||
public async init(): Promise<void> {
|
||||
// If already initialized, return immediately
|
||||
if (this.initialized) return
|
||||
async initClient(server: MCPServer) {
|
||||
const serverKey = this.getServerKey(server)
|
||||
|
||||
// If initialization is in progress, return that promise
|
||||
if (this.initPromise) return this.initPromise
|
||||
|
||||
this.initPromise = (async () => {
|
||||
try {
|
||||
log.info('[MCP] Starting initialization')
|
||||
|
||||
// Wait for servers to be loaded from Redux
|
||||
await this.waitForServers()
|
||||
|
||||
// Load SDK components in parallel for better performance
|
||||
const [Client, StdioTransport, SSETransport] = await Promise.all([
|
||||
this.importClient(),
|
||||
this.importStdioClientTransport(),
|
||||
this.importSSEClientTransport()
|
||||
])
|
||||
|
||||
this.Client = Client
|
||||
this.stdioTransport = StdioTransport
|
||||
this.sseTransport = SSETransport
|
||||
|
||||
// Mark as initialized before loading servers
|
||||
this.initialized = true
|
||||
|
||||
// Load active servers
|
||||
await this.loadActiveServers()
|
||||
log.info('[MCP] Initialization successfully')
|
||||
|
||||
return
|
||||
} catch (err) {
|
||||
this.initialized = false // Reset flag on error
|
||||
log.error('[MCP] Failed to initialize:', err)
|
||||
throw err
|
||||
} finally {
|
||||
this.initPromise = null
|
||||
}
|
||||
})()
|
||||
|
||||
return this.initPromise
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for servers to be loaded from Redux
|
||||
*/
|
||||
private async waitForServers(): Promise<void> {
|
||||
if (!this.readyState.serversLoaded && this.readyState.promise) {
|
||||
log.info('[MCP] Waiting for servers data from Redux...')
|
||||
await this.readyState.promise
|
||||
log.info('[MCP] Servers received, continuing initialization')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to create consistent error logging functions
|
||||
*/
|
||||
private logError(message: string, err?: any): void {
|
||||
log.error(`[MCP] ${message}`, err)
|
||||
}
|
||||
|
||||
/**
|
||||
* Import the MCP client SDK
|
||||
*/
|
||||
private async importClient() {
|
||||
try {
|
||||
const { Client } = await import('@modelcontextprotocol/sdk/client/index.js')
|
||||
return Client
|
||||
} catch (err) {
|
||||
this.logError('Failed to import Client:', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Import the stdio transport
|
||||
*/
|
||||
private async importStdioClientTransport() {
|
||||
try {
|
||||
const { StdioClientTransport } = await import('@modelcontextprotocol/sdk/client/stdio.js')
|
||||
return StdioClientTransport
|
||||
} catch (err) {
|
||||
log.error('[MCP] Failed to import StdioTransport:', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Import the SSE transport
|
||||
*/
|
||||
private async importSSEClientTransport() {
|
||||
try {
|
||||
const { SSEClientTransport } = await import('@modelcontextprotocol/sdk/client/sse.js')
|
||||
return SSEClientTransport
|
||||
} catch (err) {
|
||||
log.error('[MCP] Failed to import SSETransport:', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List all available MCP servers
|
||||
*/
|
||||
public async listAvailableServices(): Promise<MCPServer[]> {
|
||||
await this.ensureInitialized()
|
||||
return this.servers
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the service is initialized before operations
|
||||
*/
|
||||
private async ensureInitialized() {
|
||||
if (!this.initialized) {
|
||||
log.debug('[MCP] Ensuring initialization')
|
||||
await this.init()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new MCP server
|
||||
*/
|
||||
public async addServer(server: MCPServer): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
// Check for duplicate name
|
||||
if (this.servers.some((s) => s.name === server.name)) {
|
||||
throw new Error(`Server with name ${server.name} already exists`)
|
||||
}
|
||||
|
||||
// Activate if needed
|
||||
if (server.isActive) {
|
||||
await this.activate(server)
|
||||
}
|
||||
|
||||
// Add to servers list
|
||||
this.servers = [...this.servers, server]
|
||||
this.notifyReduxServersChanged(this.servers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing MCP server
|
||||
*/
|
||||
public async updateServer(server: MCPServer): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
const index = this.servers.findIndex((s) => s.name === server.name)
|
||||
if (index === -1) {
|
||||
throw new Error(`Server ${server.name} not found`)
|
||||
}
|
||||
|
||||
// Check activation status change
|
||||
const wasActive = this.servers[index].isActive
|
||||
if (wasActive && !server.isActive) {
|
||||
await this.deactivate(server.name)
|
||||
} else if (!wasActive && server.isActive) {
|
||||
await this.activate(server)
|
||||
} else {
|
||||
await this.restartServer(server)
|
||||
}
|
||||
|
||||
// Update servers list
|
||||
const updatedServers = [...this.servers]
|
||||
updatedServers[index] = server
|
||||
this.servers = updatedServers
|
||||
|
||||
// Notify Redux
|
||||
this.notifyReduxServersChanged(updatedServers)
|
||||
}
|
||||
|
||||
public async restartServer(_server: MCPServer): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
const server = this.servers.find((s) => s.name === _server.name)
|
||||
|
||||
if (server) {
|
||||
if (server.isActive) {
|
||||
await this.deactivate(server.name)
|
||||
}
|
||||
await this.activate(server)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Delete an MCP server
|
||||
*/
|
||||
public async deleteServer(serverName: string): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
// Deactivate if running
|
||||
if (this.clients[serverName]) {
|
||||
await this.deactivate(serverName)
|
||||
}
|
||||
|
||||
// Update servers list
|
||||
const filteredServers = this.servers.filter((s) => s.name !== serverName)
|
||||
this.servers = filteredServers
|
||||
this.notifyReduxServersChanged(filteredServers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a server's active state
|
||||
*/
|
||||
public async setServerActive(params: { name: string; isActive: boolean }): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
const { name, isActive } = params
|
||||
const server = this.servers.find((s) => s.name === name)
|
||||
|
||||
if (!server) {
|
||||
throw new Error(`Server ${name} not found`)
|
||||
}
|
||||
|
||||
// Activate or deactivate as needed
|
||||
if (isActive) {
|
||||
await this.activate(server)
|
||||
} else {
|
||||
await this.deactivate(name)
|
||||
}
|
||||
|
||||
// Update server status
|
||||
server.isActive = isActive
|
||||
this.notifyReduxServersChanged([...this.servers])
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify Redux in the renderer process about server changes
|
||||
*/
|
||||
private notifyReduxServersChanged(servers: MCPServer[]): void {
|
||||
const mainWindow = windowService.getMainWindow()
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('mcp:servers-changed', servers)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate an MCP server
|
||||
*/
|
||||
public async activate(server: MCPServer): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
const { name, baseUrl, command, env } = server
|
||||
const args = [...(server.args || [])]
|
||||
|
||||
// Skip if already running
|
||||
if (this.clients[name]) {
|
||||
log.info(`[MCP] Server ${name} is already running`)
|
||||
// Check if we already have a client for this server configuration
|
||||
const existingClient = this.clients.get(serverKey)
|
||||
if (existingClient) {
|
||||
this.client = existingClient
|
||||
return
|
||||
}
|
||||
|
||||
// If there's an existing client for a different server, close it
|
||||
if (this.client) {
|
||||
await this.closeClient()
|
||||
}
|
||||
|
||||
// Create new client instance for each connection
|
||||
this.client = new Client({ name: 'Cherry Studio', version: app.getVersion() }, { capabilities: {} })
|
||||
|
||||
const args = [...(server.args || [])]
|
||||
|
||||
let transport: StdioClientTransport | SSEClientTransport
|
||||
|
||||
try {
|
||||
// Create appropriate transport based on configuration
|
||||
if (baseUrl) {
|
||||
transport = new this.sseTransport!(new URL(baseUrl))
|
||||
} else if (command) {
|
||||
let cmd: string = command
|
||||
if (command === 'npx') {
|
||||
if (server.baseUrl) {
|
||||
transport = new SSEClientTransport(new URL(server.baseUrl))
|
||||
} else if (server.command) {
|
||||
let cmd = server.command
|
||||
|
||||
if (server.command === 'npx') {
|
||||
cmd = await getBinaryPath('bun')
|
||||
|
||||
if (cmd === 'bun') {
|
||||
cmd = 'npx'
|
||||
}
|
||||
|
||||
log.info(`[MCP] Using command: ${cmd}`)
|
||||
Logger.info(`[MCP] Using command: ${cmd}`)
|
||||
|
||||
// add -x to args if args exist
|
||||
if (args && args.length > 0) {
|
||||
if (!args.includes('-y')) {
|
||||
args.unshift('-y')
|
||||
!args.includes('-y') && args.unshift('-y')
|
||||
}
|
||||
if (cmd.includes('bun') && !args.includes('x')) {
|
||||
args.unshift('x')
|
||||
}
|
||||
}
|
||||
} else if (command === 'uvx') {
|
||||
}
|
||||
|
||||
if (server.command === 'uvx') {
|
||||
cmd = await getBinaryPath('uvx')
|
||||
}
|
||||
|
||||
log.info(`[MCP] Starting server with command: ${cmd} ${args ? args.join(' ') : ''}`)
|
||||
Logger.info(`[MCP] Starting server with command: ${cmd} ${args ? args.join(' ') : ''}`)
|
||||
|
||||
transport = new this.stdioTransport!({
|
||||
transport = new StdioClientTransport({
|
||||
command: cmd,
|
||||
args,
|
||||
stderr: 'pipe',
|
||||
env: {
|
||||
PATH: this.getEnhancedPath(process.env.PATH || ''),
|
||||
...env
|
||||
}
|
||||
env: server.env
|
||||
})
|
||||
} else {
|
||||
throw new Error('Either baseUrl or command must be provided')
|
||||
}
|
||||
|
||||
// Create and connect client
|
||||
const client = new this.Client!({ name, version: '1.0.0' }, { capabilities: {} })
|
||||
await this.client.connect(transport)
|
||||
|
||||
await client.connect(transport)
|
||||
// Store the new client in the cache
|
||||
this.clients.set(serverKey, this.client)
|
||||
|
||||
// Store client and server info
|
||||
this.clients[name] = client
|
||||
this.activeServers.set(name, { client, server })
|
||||
|
||||
log.info(`[MCP] Activated server: ${server.name}`)
|
||||
this.emit('server-started', { name })
|
||||
} catch (error) {
|
||||
log.error(`[MCP] Error activating server ${name}:`, error)
|
||||
this.setServerActive({ name, isActive: false })
|
||||
Logger.info(`[MCP] Activated server: ${server.name}`)
|
||||
} catch (error: any) {
|
||||
Logger.error(`[MCP] Error activating server ${server.name}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate an MCP server
|
||||
*/
|
||||
public async deactivate(name: string): Promise<void> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
if (!this.clients[name]) {
|
||||
log.warn(`[MCP] Server ${name} is not running`)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
log.info(`[MCP] Stopping server: ${name}`)
|
||||
await this.clients[name].close()
|
||||
delete this.clients[name]
|
||||
this.activeServers.delete(name)
|
||||
this.emit('server-stopped', { name })
|
||||
} catch (error) {
|
||||
log.error(`[MCP] Error deactivating server ${name}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List available tools from active MCP servers
|
||||
*/
|
||||
public async listTools(serverName?: string): Promise<MCPTool[]> {
|
||||
await this.ensureInitialized()
|
||||
log.info(`[MCP] Listing tools from ${serverName || 'all active servers'}`)
|
||||
|
||||
try {
|
||||
// If server name provided, list tools for that server only
|
||||
if (serverName) {
|
||||
return await this.listToolsFromServer(serverName)
|
||||
}
|
||||
|
||||
// Otherwise list tools from all active servers
|
||||
let allTools: MCPTool[] = []
|
||||
|
||||
for (const clientName in this.clients) {
|
||||
log.info(`[MCP] Listing tools from ${clientName}`)
|
||||
try {
|
||||
const tools = await this.listToolsFromServer(clientName)
|
||||
allTools = allTools.concat(tools)
|
||||
} catch (error) {
|
||||
this.logError(`Error listing tools for ${clientName}`, error)
|
||||
async closeClient() {
|
||||
if (this.client) {
|
||||
// Remove the client from the cache
|
||||
for (const [key, client] of this.clients.entries()) {
|
||||
if (client === this.client) {
|
||||
this.clients.delete(key)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
log.info(`[MCP] Total tools listed: ${allTools.length}`)
|
||||
return allTools
|
||||
} catch (error) {
|
||||
this.logError('Error listing tools:', error)
|
||||
return []
|
||||
await this.client.close()
|
||||
this.client = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to list tools from a specific server
|
||||
*/
|
||||
private async listToolsFromServer(serverName: string): Promise<MCPTool[]> {
|
||||
log.info(`[MCP] start list tools from ${serverName}:`)
|
||||
if (!this.clients[serverName]) {
|
||||
throw new Error(`MCP Client ${serverName} not found`)
|
||||
}
|
||||
const cacheKey = `mcp:list_tool:${serverName}`
|
||||
async removeServer(_: Electron.IpcMainInvokeEvent, server: MCPServer) {
|
||||
await this.closeClient()
|
||||
this.clients.delete(this.getServerKey(server))
|
||||
}
|
||||
|
||||
if (CacheService.has(cacheKey)) {
|
||||
log.info(`[MCP] Tools from ${serverName} loaded from cache`)
|
||||
// Check if cache is still valid
|
||||
const cachedTools = CacheService.get<MCPTool[]>(cacheKey)
|
||||
if (cachedTools && cachedTools.length > 0) {
|
||||
return cachedTools
|
||||
}
|
||||
CacheService.remove(cacheKey)
|
||||
}
|
||||
|
||||
const { tools } = await this.clients[serverName].listTools()
|
||||
|
||||
const transformedTools = tools.map((tool: any) => ({
|
||||
async listTools(_: Electron.IpcMainInvokeEvent, server: MCPServer) {
|
||||
await this.initClient(server)
|
||||
const { tools } = await this.client!.listTools()
|
||||
return tools.map((tool) => ({
|
||||
...tool,
|
||||
serverName,
|
||||
id: 'f' + uuidv4().replace(/-/g, '')
|
||||
serverId: server.id,
|
||||
serverName: server.name
|
||||
}))
|
||||
|
||||
// Cache the tools for 5 minutes
|
||||
if (transformedTools.length > 0) {
|
||||
CacheService.set(cacheKey, transformedTools, 5 * 60 * 1000)
|
||||
}
|
||||
|
||||
log.info(`[MCP] Tools from ${serverName}:`, transformedTools)
|
||||
return transformedTools
|
||||
}
|
||||
|
||||
/**
|
||||
* Call a tool on an MCP server
|
||||
*/
|
||||
public async callTool(params: { client: string; name: string; args: any }): Promise<any> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
const { client, name, args } = params
|
||||
|
||||
if (!this.clients[client]) {
|
||||
throw new Error(`MCP Client ${client} not found`)
|
||||
}
|
||||
|
||||
log.info('[MCP] Calling:', client, name, args)
|
||||
public async callTool(
|
||||
_: Electron.IpcMainInvokeEvent,
|
||||
{ server, name, args }: { server: MCPServer; name: string; args: any }
|
||||
): Promise<any> {
|
||||
await this.initClient(server)
|
||||
|
||||
try {
|
||||
return await this.clients[client].callTool({
|
||||
name,
|
||||
arguments: args
|
||||
})
|
||||
Logger.info('[MCP] Calling:', server.name, name, args)
|
||||
const result = await this.client!.callTool({ name, arguments: args })
|
||||
return result
|
||||
} catch (error) {
|
||||
log.error(`[MCP] Error calling tool ${name} on ${client}:`, error)
|
||||
Logger.error(`[MCP] Error calling tool ${name} on ${server.name}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up all MCP resources
|
||||
*/
|
||||
public async cleanup(): Promise<void> {
|
||||
const clientNames = Object.keys(this.clients)
|
||||
|
||||
if (clientNames.length === 0) {
|
||||
log.info('[MCP] No active servers to clean up')
|
||||
return
|
||||
}
|
||||
|
||||
log.info(`[MCP] Cleaning up ${clientNames.length} active servers`)
|
||||
|
||||
// Deactivate all clients
|
||||
await Promise.allSettled(
|
||||
clientNames.map((name) =>
|
||||
this.deactivate(name).catch((err) => {
|
||||
log.error(`[MCP] Error during cleanup of ${name}:`, err)
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
this.clients = {}
|
||||
this.activeServers.clear()
|
||||
log.info('[MCP] All servers cleaned up')
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all active servers
|
||||
*/
|
||||
private async loadActiveServers(): Promise<void> {
|
||||
const activeServers = this.servers.filter((server) => server.isActive)
|
||||
|
||||
if (activeServers.length === 0) {
|
||||
log.info('[MCP] No active servers to load')
|
||||
return
|
||||
}
|
||||
|
||||
log.info(`[MCP] Start loading ${activeServers.length} active servers`)
|
||||
|
||||
// Activate servers in parallel for better performance
|
||||
await Promise.allSettled(
|
||||
activeServers.map(async (server) => {
|
||||
try {
|
||||
await this.activate(server)
|
||||
} catch (error) {
|
||||
this.logError(`Failed to activate server ${server.name}`, error)
|
||||
this.emit('server-error', { name: server.name, error })
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
log.info(`[MCP] End loading ${Object.keys(this.clients).length} active servers`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enhanced PATH including common tool locations
|
||||
*/
|
||||
private getEnhancedPath(originalPath: string): string {
|
||||
// 将原始 PATH 按分隔符分割成数组
|
||||
const pathSeparator = process.platform === 'win32' ? ';' : ':'
|
||||
const existingPaths = new Set(originalPath.split(pathSeparator).filter(Boolean))
|
||||
const homeDir = process.env.HOME || process.env.USERPROFILE || ''
|
||||
|
||||
// 定义要添加的新路径
|
||||
const newPaths: string[] = []
|
||||
|
||||
if (isMac) {
|
||||
newPaths.push(
|
||||
'/bin',
|
||||
'/usr/bin',
|
||||
'/usr/local/bin',
|
||||
'/usr/local/sbin',
|
||||
'/opt/homebrew/bin',
|
||||
'/opt/homebrew/sbin',
|
||||
'/usr/local/opt/node/bin',
|
||||
`${homeDir}/.nvm/current/bin`,
|
||||
`${homeDir}/.npm-global/bin`,
|
||||
`${homeDir}/.yarn/bin`,
|
||||
`${homeDir}/.cargo/bin`,
|
||||
'/opt/local/bin'
|
||||
)
|
||||
}
|
||||
|
||||
if (isLinux) {
|
||||
newPaths.push(
|
||||
'/bin',
|
||||
'/usr/bin',
|
||||
'/usr/local/bin',
|
||||
`${homeDir}/.nvm/current/bin`,
|
||||
`${homeDir}/.npm-global/bin`,
|
||||
`${homeDir}/.yarn/bin`,
|
||||
`${homeDir}/.cargo/bin`,
|
||||
'/snap/bin'
|
||||
)
|
||||
}
|
||||
|
||||
if (isWin) {
|
||||
newPaths.push(`${process.env.APPDATA}\\npm`, `${homeDir}\\AppData\\Local\\Yarn\\bin`, `${homeDir}\\.cargo\\bin`)
|
||||
}
|
||||
|
||||
// 只添加不存在的路径
|
||||
newPaths.forEach((path) => {
|
||||
if (path && !existingPaths.has(path)) {
|
||||
existingPaths.add(path)
|
||||
}
|
||||
})
|
||||
|
||||
// 转换回字符串
|
||||
return Array.from(existingPaths).join(pathSeparator)
|
||||
public async getInstallInfo() {
|
||||
const dir = path.join(os.homedir(), '.cherrystudio', 'bin')
|
||||
const uvName = await getBinaryName('uv')
|
||||
const bunName = await getBinaryName('bun')
|
||||
const uvPath = path.join(dir, uvName)
|
||||
const bunPath = path.join(dir, bunName)
|
||||
return { dir, uvPath, bunPath }
|
||||
}
|
||||
}
|
||||
|
||||
export default new McpService()
|
||||
|
||||
@@ -42,3 +42,7 @@ export function dumpPersistState() {
|
||||
}
|
||||
return JSON.stringify(persistState)
|
||||
}
|
||||
|
||||
export const runAsyncFunction = async (fn: () => void) => {
|
||||
await fn()
|
||||
}
|
||||
|
||||
@@ -35,12 +35,18 @@ export function runInstallScript(scriptPath: string): Promise<void> {
|
||||
})
|
||||
}
|
||||
|
||||
export async function getBinaryName(name: string): Promise<string> {
|
||||
if (process.platform === 'win32') {
|
||||
return `${name}.exe`
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
export async function getBinaryPath(name: string): Promise<string> {
|
||||
let cmd = process.platform === 'win32' ? `${name}.exe` : name
|
||||
const binaryName = await getBinaryName(name)
|
||||
const binariesDir = path.join(os.homedir(), '.cherrystudio', 'bin')
|
||||
const binariesDirExists = await fs.existsSync(binariesDir)
|
||||
cmd = binariesDirExists ? path.join(binariesDir, cmd) : name
|
||||
return cmd
|
||||
return binariesDirExists ? path.join(binariesDir, binaryName) : binaryName
|
||||
}
|
||||
|
||||
export async function isBinaryExists(name: string): Promise<boolean> {
|
||||
|
||||
Vendored
+4
-11
@@ -155,17 +155,10 @@ declare global {
|
||||
openExternal: (url: string, options?: OpenExternalOptions) => Promise<void>
|
||||
}
|
||||
mcp: {
|
||||
// servers
|
||||
listServers: () => Promise<MCPServer[]>
|
||||
addServer: (server: MCPServer) => Promise<void>
|
||||
updateServer: (server: MCPServer) => Promise<void>
|
||||
deleteServer: (serverName: string) => Promise<void>
|
||||
setServerActive: (name: string, isActive: boolean) => Promise<void>
|
||||
// tools
|
||||
listTools: () => Promise<MCPTool[]>
|
||||
callTool: ({ client, name, args }: { client: string; name: string; args: any }) => Promise<any>
|
||||
// status
|
||||
cleanup: () => Promise<void>
|
||||
removeServer: (server: MCPServer) => Promise<void>
|
||||
listTools: (server: MCPServer) => Promise<MCPTool[]>
|
||||
callTool: ({ server, name, args }: { server: MCPServer; name: string; args: any }) => Promise<any>
|
||||
getInstallInfo: () => Promise<{ dir: string; uvPath: string; bunPath: string }>
|
||||
}
|
||||
copilot: {
|
||||
getAuthMessage: (
|
||||
|
||||
@@ -124,15 +124,11 @@ const api = {
|
||||
ipcRenderer.invoke('aes:decrypt', encryptedData, iv, secretKey)
|
||||
},
|
||||
mcp: {
|
||||
listServers: () => ipcRenderer.invoke('mcp:list-servers'),
|
||||
addServer: (server: MCPServer) => ipcRenderer.invoke('mcp:add-server', server),
|
||||
updateServer: (server: MCPServer) => ipcRenderer.invoke('mcp:update-server', server),
|
||||
deleteServer: (serverName: string) => ipcRenderer.invoke('mcp:delete-server', serverName),
|
||||
setServerActive: (name: string, isActive: boolean) =>
|
||||
ipcRenderer.invoke('mcp:set-server-active', { name, isActive }),
|
||||
listTools: (serverName?: string) => ipcRenderer.invoke('mcp:list-tools', serverName),
|
||||
callTool: (params: { client: string; name: string; args: any }) => ipcRenderer.invoke('mcp:call-tool', params),
|
||||
cleanup: () => ipcRenderer.invoke('mcp:cleanup')
|
||||
removeServer: (server: MCPServer) => ipcRenderer.invoke('mcp:remove-server', server),
|
||||
listTools: (server: MCPServer) => ipcRenderer.invoke('mcp:list-tools', server),
|
||||
callTool: ({ server, name, args }: { server: MCPServer; name: string; args: any }) =>
|
||||
ipcRenderer.invoke('mcp:call-tool', { server, name, args }),
|
||||
getInstallInfo: () => ipcRenderer.invoke('mcp:get-install-info')
|
||||
},
|
||||
shell: {
|
||||
openExternal: shell.openExternal
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 12 KiB |
@@ -19,7 +19,7 @@
|
||||
--color-gray-2: #414853;
|
||||
--color-gray-3: #32363f;
|
||||
|
||||
--color-text-1: rgba(255, 255, 245, 0.86);
|
||||
--color-text-1: rgba(255, 255, 245, 0.9);
|
||||
--color-text-2: rgba(235, 235, 245, 0.6);
|
||||
--color-text-3: rgba(235, 235, 245, 0.38);
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
OnDragStartResponder,
|
||||
ResponderProvided
|
||||
} from '@hello-pangea/dnd'
|
||||
import VirtualList from 'rc-virtual-list'
|
||||
import { droppableReorder } from '@renderer/utils'
|
||||
import VirtualList from 'rc-virtual-list'
|
||||
import { FC } from 'react'
|
||||
|
||||
interface Props<T> {
|
||||
|
||||
@@ -23,7 +23,7 @@ const Container = styled.div`
|
||||
`
|
||||
|
||||
const Icon = styled(ToolOutlined)`
|
||||
color: #d97757;
|
||||
color: var(--color-primary);
|
||||
font-size: 15px;
|
||||
margin-right: 6px;
|
||||
`
|
||||
|
||||
@@ -4,15 +4,25 @@ import styled from 'styled-components'
|
||||
|
||||
interface IndicatorLightProps {
|
||||
color: string
|
||||
size?: number
|
||||
shadow?: boolean
|
||||
style?: React.CSSProperties
|
||||
animation?: boolean
|
||||
}
|
||||
|
||||
const Light = styled.div<{ color: string }>`
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
const Light = styled.div<{
|
||||
color: string
|
||||
size: number
|
||||
shadow?: boolean
|
||||
style?: React.CSSProperties
|
||||
animation?: boolean
|
||||
}>`
|
||||
width: ${({ size }) => size}px;
|
||||
height: ${({ size }) => size}px;
|
||||
border-radius: 50%;
|
||||
background-color: ${({ color }) => color};
|
||||
box-shadow: 0 0 6px ${({ color }) => color};
|
||||
animation: pulse 2s infinite;
|
||||
box-shadow: ${({ shadow, color }) => (shadow ? `0 0 6px ${color}` : 'none')};
|
||||
animation: ${({ animation }) => (animation ? 'pulse 2s infinite' : 'none')};
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
@@ -27,9 +37,9 @@ const Light = styled.div<{ color: string }>`
|
||||
}
|
||||
`
|
||||
|
||||
const IndicatorLight: React.FC<IndicatorLightProps> = ({ color }) => {
|
||||
const IndicatorLight: React.FC<IndicatorLightProps> = ({ color, size = 8, shadow = true, style, animation = true }) => {
|
||||
const actualColor = color === 'green' ? '#22c55e' : color
|
||||
return <Light color={actualColor} />
|
||||
return <Light color={actualColor} size={size} shadow={shadow} style={style} animation={animation} />
|
||||
}
|
||||
|
||||
export default IndicatorLight
|
||||
|
||||
@@ -8,17 +8,20 @@ interface ListItemProps {
|
||||
subtitle?: string
|
||||
titleStyle?: React.CSSProperties
|
||||
onClick?: () => void
|
||||
rightContent?: ReactNode
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
const ListItem = ({ active, icon, title, subtitle, titleStyle, onClick }: ListItemProps) => {
|
||||
const ListItem = ({ active, icon, title, subtitle, titleStyle, onClick, rightContent, style }: ListItemProps) => {
|
||||
return (
|
||||
<ListItemContainer className={active ? 'active' : ''} onClick={onClick}>
|
||||
<ListItemContainer className={active ? 'active' : ''} onClick={onClick} style={style}>
|
||||
<ListItemContent>
|
||||
{icon && <IconWrapper>{icon}</IconWrapper>}
|
||||
<TextContainer>
|
||||
<TitleText style={titleStyle}>{title}</TitleText>
|
||||
{subtitle && <SubtitleText>{subtitle}</SubtitleText>}
|
||||
</TextContainer>
|
||||
{rightContent && <RightContentWrapper>{rightContent}</RightContentWrapper>}
|
||||
</ListItemContent>
|
||||
</ListItemContainer>
|
||||
)
|
||||
@@ -84,4 +87,8 @@ const SubtitleText = styled.div`
|
||||
color: var(--color-text-3);
|
||||
`
|
||||
|
||||
const RightContentWrapper = styled.div`
|
||||
margin-left: auto;
|
||||
`
|
||||
|
||||
export default ListItem
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { isMac, isWindows } from '@renderer/config/constant'
|
||||
import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor'
|
||||
import type { FC, PropsWithChildren } from 'react'
|
||||
import type { HTMLAttributes } from 'react'
|
||||
@@ -63,4 +63,6 @@ const NavbarRightContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
padding-right: ${isWindows ? '140px' : 12};
|
||||
justify-content: flex-end;
|
||||
`
|
||||
|
||||
@@ -82,7 +82,7 @@ export const SyntaxHighlighterProvider: React.FC<PropsWithChildren> = ({ childre
|
||||
[highlighter, highlighterTheme]
|
||||
)
|
||||
|
||||
return <SyntaxHighlighterContext.Provider value={{ codeToHtml }}>{children}</SyntaxHighlighterContext.Provider>
|
||||
return <SyntaxHighlighterContext value={{ codeToHtml }}>{children}</SyntaxHighlighterContext>
|
||||
}
|
||||
|
||||
export const useSyntaxHighlighter = () => {
|
||||
|
||||
@@ -57,11 +57,7 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children, defaultT
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme: _theme, settingTheme: theme, toggleTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
)
|
||||
return <ThemeContext value={{ theme: _theme, settingTheme: theme, toggleTheme }}>{children}</ThemeContext>
|
||||
}
|
||||
|
||||
export const useTheme = () => use(ThemeContext)
|
||||
|
||||
@@ -11,7 +11,6 @@ import { useEffect } from 'react'
|
||||
|
||||
import { useDefaultModel } from './useAssistant'
|
||||
import useFullScreenNotice from './useFullScreenNotice'
|
||||
import { useInitMCPServers } from './useMCPServers'
|
||||
import { useRuntime } from './useRuntime'
|
||||
import { useSettings } from './useSettings'
|
||||
import useUpdateHandler from './useUpdateHandler'
|
||||
@@ -26,7 +25,6 @@ export function useAppInit() {
|
||||
|
||||
useUpdateHandler()
|
||||
useFullScreenNotice()
|
||||
useInitMCPServers()
|
||||
|
||||
useEffect(() => {
|
||||
avatar?.value && dispatch(setAvatar(avatar.value))
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import store, { useAppSelector } from '@renderer/store'
|
||||
import { setMCPServers } from '@renderer/store/mcp'
|
||||
import store, { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { addMCPServer, deleteMCPServer, setMCPServers, updateMCPServer } from '@renderer/store/mcp'
|
||||
import { MCPServer } from '@renderer/types'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
const ipcRenderer = window.electron.ipcRenderer
|
||||
|
||||
@@ -13,82 +12,16 @@ ipcRenderer.on('mcp:servers-changed', (_event, servers) => {
|
||||
export const useMCPServers = () => {
|
||||
const mcpServers = useAppSelector((state) => state.mcp.servers)
|
||||
const activedMcpServers = useAppSelector((state) => state.mcp.servers?.filter((server) => server.isActive))
|
||||
|
||||
const addMCPServer = async (server: MCPServer) => {
|
||||
try {
|
||||
await window.api.mcp.addServer(server)
|
||||
// Main process will send back updated servers via mcp:servers-changed
|
||||
} catch (error) {
|
||||
console.error('Failed to add MCP server:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const updateMCPServer = async (server: MCPServer) => {
|
||||
try {
|
||||
await window.api.mcp.updateServer(server)
|
||||
// Main process will send back updated servers via mcp:servers-changed
|
||||
} catch (error) {
|
||||
console.error('Failed to update MCP server:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const deleteMCPServer = async (name: string) => {
|
||||
try {
|
||||
await window.api.mcp.deleteServer(name)
|
||||
// Main process will send back updated servers via mcp:servers-changed
|
||||
} catch (error) {
|
||||
console.error('Failed to delete MCP server:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const setMCPServerActive = async (name: string, isActive: boolean) => {
|
||||
try {
|
||||
await window.api.mcp.setServerActive(name, isActive)
|
||||
// Main process will send back updated servers via mcp:servers-changed
|
||||
} catch (error) {
|
||||
console.error('Failed to set MCP server active status:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const getActiveMCPServers = () => {
|
||||
return mcpServers.filter((server) => server.isActive)
|
||||
}
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
return {
|
||||
mcpServers,
|
||||
activedMcpServers,
|
||||
addMCPServer,
|
||||
updateMCPServer,
|
||||
deleteMCPServer,
|
||||
setMCPServerActive,
|
||||
getActiveMCPServers
|
||||
addMCPServer: (server: MCPServer) => dispatch(addMCPServer(server)),
|
||||
updateMCPServer: (server: MCPServer) => dispatch(updateMCPServer(server)),
|
||||
deleteMCPServer: (id: string) => dispatch(deleteMCPServer(id)),
|
||||
setMCPServerActive: (server: MCPServer, isActive: boolean) => dispatch(updateMCPServer({ ...server, isActive })),
|
||||
getActiveMCPServers: () => mcpServers.filter((server) => server.isActive),
|
||||
updateMcpServers: (servers: MCPServer[]) => dispatch(setMCPServers(servers))
|
||||
}
|
||||
}
|
||||
|
||||
export const useInitMCPServers = () => {
|
||||
const mcpServers = useAppSelector((state) => state.mcp.servers)
|
||||
// const dispatch = useAppDispatch()
|
||||
|
||||
// Send servers to main process when they change in Redux
|
||||
useEffect(() => {
|
||||
ipcRenderer.send('mcp:servers-from-renderer', mcpServers)
|
||||
}, [mcpServers])
|
||||
|
||||
// Initial load of MCP servers from main process
|
||||
// useEffect(() => {
|
||||
// const loadServers = async () => {
|
||||
// try {
|
||||
// const servers = await window.api.mcp.listServers()
|
||||
// dispatch(setMCPServers(servers))
|
||||
// } catch (error) {
|
||||
// console.error('Failed to load MCP servers:', error)
|
||||
// }
|
||||
// }
|
||||
|
||||
// loadServers()
|
||||
// }, [dispatch])
|
||||
}
|
||||
|
||||
@@ -906,7 +906,8 @@
|
||||
"pathSelector.return": "Return",
|
||||
"pathSelector.currentPath": "Current Path",
|
||||
"new_folder.button.confirm": "Confirm",
|
||||
"new_folder.button.cancel": "Cancel"
|
||||
"new_folder.button.cancel": "Cancel",
|
||||
"new_folder.button": "New Folder"
|
||||
}
|
||||
},
|
||||
"display.assistant.title": "Assistant Settings",
|
||||
@@ -965,10 +966,7 @@
|
||||
"argsTooltip": "Each argument on a new line",
|
||||
"baseUrlTooltip": "Remote server base URL",
|
||||
"command": "Command",
|
||||
"commandRequired": "Please enter a command",
|
||||
"config_description": "Configure Model Context Protocol servers",
|
||||
"confirmDelete": "Delete Server",
|
||||
"confirmDeleteMessage": "Are you sure you want to delete the server?",
|
||||
"deleteError": "Failed to delete server",
|
||||
"deleteSuccess": "Server deleted successfully",
|
||||
"dependenciesInstall": "Install Dependencies",
|
||||
@@ -979,7 +977,8 @@
|
||||
"editServer": "Edit Server",
|
||||
"env": "Environment Variables",
|
||||
"envTooltip": "Format: KEY=value, one per line",
|
||||
"findMore": "Find More MCP Servers",
|
||||
"findMore": "Find More MCP",
|
||||
"searchNpx": "Search MCP",
|
||||
"install": "Install",
|
||||
"installError": "Failed to install dependencies",
|
||||
"installSuccess": "Dependencies installed successfully",
|
||||
@@ -989,8 +988,8 @@
|
||||
"jsonSaveSuccess": "JSON configuration has been saved.",
|
||||
"missingDependencies": "is Missing, please install it to continue.",
|
||||
"name": "Name",
|
||||
"nameRequired": "Please enter a server name",
|
||||
"noServers": "No servers configured",
|
||||
"newServer": "MCP Server",
|
||||
"npx_list": {
|
||||
"actions": "Actions",
|
||||
"desc": "Search and add npm packages as MCP servers",
|
||||
@@ -1006,14 +1005,19 @@
|
||||
"usage": "Usage",
|
||||
"version": "Version"
|
||||
},
|
||||
"errors": {
|
||||
"32000": "MCP server failed to start, please check the parameters according to the tutorial"
|
||||
},
|
||||
"serverPlural": "servers",
|
||||
"serverSingular": "server",
|
||||
"title": "MCP Servers",
|
||||
"toggleError": "Toggle failed",
|
||||
"startError": "Start failed",
|
||||
"type": "Type",
|
||||
"updateError": "Failed to update server",
|
||||
"updateSuccess": "Server updated successfully",
|
||||
"url": "URL"
|
||||
"url": "URL",
|
||||
"editMcpJson": "Edit MCP Configuration",
|
||||
"installHelp": "Get Installation Help"
|
||||
},
|
||||
"messages.divider": "Show divider between messages",
|
||||
"messages.grid_columns": "Message grid display columns",
|
||||
|
||||
@@ -906,7 +906,8 @@
|
||||
"pathSelector.return": "戻る",
|
||||
"pathSelector.currentPath": "現在のパス",
|
||||
"new_folder.button.confirm": "確認",
|
||||
"new_folder.button.cancel": "キャンセル"
|
||||
"new_folder.button.cancel": "キャンセル",
|
||||
"new_folder.button": "新しいフォルダー"
|
||||
}
|
||||
},
|
||||
"display.assistant.title": "アシスタント設定",
|
||||
@@ -964,10 +965,7 @@
|
||||
"argsTooltip": "1行に1つの引数を入力してください",
|
||||
"baseUrlTooltip": "リモートURLアドレス",
|
||||
"command": "コマンド",
|
||||
"commandRequired": "コマンドを入力してください",
|
||||
"config_description": "モデルコンテキストプロトコルサーバーの設定",
|
||||
"confirmDelete": "サーバーを削除",
|
||||
"confirmDeleteMessage": "本当にこのサーバーを削除しますか?",
|
||||
"deleteError": "サーバーの削除に失敗しました",
|
||||
"deleteSuccess": "サーバーが正常に削除されました",
|
||||
"dependenciesInstall": "依存関係をインストール",
|
||||
@@ -978,7 +976,8 @@
|
||||
"editServer": "サーバーを編集",
|
||||
"env": "環境変数",
|
||||
"envTooltip": "形式: KEY=value, 1行に1つ",
|
||||
"findMore": "MCP サーバーを見つける",
|
||||
"findMore": "MCP を見つける",
|
||||
"searchNpx": "MCP を検索",
|
||||
"install": "インストール",
|
||||
"installError": "依存関係のインストールに失敗しました",
|
||||
"installSuccess": "依存関係のインストールに成功しました",
|
||||
@@ -988,8 +987,8 @@
|
||||
"jsonSaveSuccess": "JSON設定が保存されました。",
|
||||
"missingDependencies": "が不足しています。続行するにはインストールしてください。",
|
||||
"name": "名前",
|
||||
"nameRequired": "サーバー名を入力してください",
|
||||
"noServers": "サーバーが設定されていません",
|
||||
"newServer": "MCP サーバー",
|
||||
"npx_list": {
|
||||
"actions": "アクション",
|
||||
"desc": "npm パッケージを検索して MCP サーバーとして追加",
|
||||
@@ -1008,11 +1007,16 @@
|
||||
"serverPlural": "サーバー",
|
||||
"serverSingular": "サーバー",
|
||||
"title": "MCP サーバー",
|
||||
"toggleError": "切り替えに失敗しました",
|
||||
"startError": "起動に失敗しました",
|
||||
"type": "タイプ",
|
||||
"updateError": "サーバーの更新に失敗しました",
|
||||
"updateSuccess": "サーバーが正常に更新されました",
|
||||
"url": "URL"
|
||||
"url": "URL",
|
||||
"errors": {
|
||||
"32000": "MCP サーバーが起動しませんでした。パラメーターを確認してください"
|
||||
},
|
||||
"editMcpJson": "MCP 設定を編集",
|
||||
"installHelp": "インストールヘルプを取得"
|
||||
},
|
||||
"messages.divider": "メッセージ間に区切り線を表示",
|
||||
"messages.grid_columns": "メッセージグリッドの表示列数",
|
||||
|
||||
@@ -906,7 +906,8 @@
|
||||
"pathSelector.return": "Назад",
|
||||
"pathSelector.currentPath": "Текущий путь",
|
||||
"new_folder.button.confirm": "Подтвердить",
|
||||
"new_folder.button.cancel": "Отмена"
|
||||
"new_folder.button.cancel": "Отмена",
|
||||
"new_folder.button": "Новая папка"
|
||||
}
|
||||
},
|
||||
"display.assistant.title": "Настройки ассистентов",
|
||||
@@ -964,10 +965,7 @@
|
||||
"argsTooltip": "Каждый аргумент с новой строки",
|
||||
"baseUrlTooltip": "Адрес удаленного URL",
|
||||
"command": "Команда",
|
||||
"commandRequired": "Пожалуйста, введите команду",
|
||||
"config_description": "Настройка серверов протокола контекста модели",
|
||||
"confirmDelete": "Удалить сервер",
|
||||
"confirmDeleteMessage": "Вы уверены, что хотите удалить этот сервер?",
|
||||
"deleteError": "Не удалось удалить сервер",
|
||||
"deleteSuccess": "Сервер успешно удален",
|
||||
"dependenciesInstall": "Установить зависимости",
|
||||
@@ -978,7 +976,8 @@
|
||||
"editServer": "Редактировать сервер",
|
||||
"env": "Переменные окружения",
|
||||
"envTooltip": "Формат: KEY=value, по одной на строку",
|
||||
"findMore": "Найти больше MCP серверов",
|
||||
"findMore": "Найти больше MCP",
|
||||
"searchNpx": "Найти MCP",
|
||||
"install": "Установить",
|
||||
"installError": "Не удалось установить зависимости",
|
||||
"installSuccess": "Зависимости успешно установлены",
|
||||
@@ -988,8 +987,8 @@
|
||||
"jsonSaveSuccess": "JSON конфигурация сохранена",
|
||||
"missingDependencies": "отсутствует, пожалуйста, установите для продолжения.",
|
||||
"name": "Имя",
|
||||
"nameRequired": "Пожалуйста, введите имя сервера",
|
||||
"noServers": "Серверы не настроены",
|
||||
"newServer": "MCP сервер",
|
||||
"npx_list": {
|
||||
"actions": "Действия",
|
||||
"desc": "Поиск и добавление npm пакетов в качестве MCP серверов",
|
||||
@@ -1005,14 +1004,19 @@
|
||||
"usage": "Использование",
|
||||
"version": "Версия"
|
||||
},
|
||||
"errors": {
|
||||
"32000": "MCP сервер не запущен, пожалуйста, проверьте параметры"
|
||||
},
|
||||
"serverPlural": "серверы",
|
||||
"serverSingular": "сервер",
|
||||
"title": "Серверы MCP",
|
||||
"toggleError": "Переключение не удалось",
|
||||
"startError": "Запуск не удалось",
|
||||
"type": "Тип",
|
||||
"updateError": "Ошибка обновления сервера",
|
||||
"updateSuccess": "Сервер успешно обновлен",
|
||||
"url": "URL"
|
||||
"url": "URL",
|
||||
"editMcpJson": "Редактировать MCP 配置",
|
||||
"installHelp": "Получить помощь по установке"
|
||||
},
|
||||
"messages.divider": "Показывать разделитель между сообщениями",
|
||||
"messages.grid_columns": "Количество столбцов сетки сообщений",
|
||||
|
||||
@@ -902,7 +902,8 @@
|
||||
"pathSelector.return": "返回",
|
||||
"pathSelector.currentPath": "当前路径",
|
||||
"new_folder.button.confirm": "确定",
|
||||
"new_folder.button.cancel": "取消"
|
||||
"new_folder.button.cancel": "取消",
|
||||
"new_folder.button": "新建文件夹"
|
||||
}
|
||||
},
|
||||
"display.assistant.title": "助手设置",
|
||||
@@ -968,10 +969,7 @@
|
||||
"argsTooltip": "每个参数占一行",
|
||||
"baseUrlTooltip": "远程 URL 地址",
|
||||
"command": "命令",
|
||||
"commandRequired": "请输入命令",
|
||||
"config_description": "配置模型上下文协议服务器",
|
||||
"confirmDelete": "删除服务器",
|
||||
"confirmDeleteMessage": "您确定要删除该服务器吗?",
|
||||
"deleteError": "删除服务器失败",
|
||||
"deleteSuccess": "服务器删除成功",
|
||||
"dependenciesInstall": "安装依赖项",
|
||||
@@ -982,7 +980,8 @@
|
||||
"editServer": "编辑服务器",
|
||||
"env": "环境变量",
|
||||
"envTooltip": "格式:KEY=value,每行一个",
|
||||
"findMore": "更多 MCP 服务器",
|
||||
"findMore": "更多 MCP",
|
||||
"searchNpx": "搜索 MCP",
|
||||
"install": "安装",
|
||||
"installError": "安装依赖项失败",
|
||||
"installSuccess": "依赖项安装成功",
|
||||
@@ -992,8 +991,8 @@
|
||||
"jsonSaveSuccess": "JSON配置已保存",
|
||||
"missingDependencies": "缺失,请安装它以继续",
|
||||
"name": "名称",
|
||||
"nameRequired": "请输入服务器名称",
|
||||
"noServers": "未配置服务器",
|
||||
"newServer": "MCP 服务器",
|
||||
"npx_list": {
|
||||
"actions": "操作",
|
||||
"desc": "搜索并添加 npm 包作为 MCP 服务",
|
||||
@@ -1009,14 +1008,19 @@
|
||||
"usage": "用法",
|
||||
"version": "版本"
|
||||
},
|
||||
"errors": {
|
||||
"32000": "MCP 服务器启动失败,请根据教程检查参数是否填写完整"
|
||||
},
|
||||
"serverPlural": "服务器",
|
||||
"serverSingular": "服务器",
|
||||
"title": "MCP 服务器",
|
||||
"toggleError": "切换失败",
|
||||
"startError": "启动失败",
|
||||
"type": "类型",
|
||||
"updateError": "更新服务器失败",
|
||||
"updateSuccess": "服务器更新成功",
|
||||
"url": "URL"
|
||||
"url": "URL",
|
||||
"editMcpJson": "编辑 MCP 配置",
|
||||
"installHelp": "获取安装帮助"
|
||||
},
|
||||
"messages.divider": "消息分割线",
|
||||
"messages.grid_columns": "消息网格展示列数",
|
||||
|
||||
@@ -906,7 +906,8 @@
|
||||
"pathSelector.return": "返回",
|
||||
"pathSelector.currentPath": "當前路徑",
|
||||
"new_folder.button.confirm": "確定",
|
||||
"new_folder.button.cancel": "取消"
|
||||
"new_folder.button.cancel": "取消",
|
||||
"new_folder.button": "新建文件夾"
|
||||
}
|
||||
},
|
||||
"display.assistant.title": "助手設定",
|
||||
@@ -964,10 +965,7 @@
|
||||
"argsTooltip": "每個參數佔一行",
|
||||
"baseUrlTooltip": "遠端 URL 地址",
|
||||
"command": "指令",
|
||||
"commandRequired": "請輸入指令",
|
||||
"config_description": "設定模型上下文協議伺服器",
|
||||
"confirmDelete": "刪除伺服器",
|
||||
"confirmDeleteMessage": "您確定要刪除該伺服器嗎?",
|
||||
"deleteError": "刪除伺服器失敗",
|
||||
"deleteSuccess": "伺服器刪除成功",
|
||||
"dependenciesInstall": "安裝相依套件",
|
||||
@@ -978,7 +976,8 @@
|
||||
"editServer": "編輯伺服器",
|
||||
"env": "環境變數",
|
||||
"envTooltip": "格式:KEY=value,每行一個",
|
||||
"findMore": "更多 MCP 伺服器",
|
||||
"findMore": "更多 MCP",
|
||||
"searchNpx": "搜索 MCP",
|
||||
"install": "安裝",
|
||||
"installError": "安裝相依套件失敗",
|
||||
"installSuccess": "相依套件安裝成功",
|
||||
@@ -988,8 +987,8 @@
|
||||
"jsonSaveSuccess": "JSON配置已儲存",
|
||||
"missingDependencies": "缺失,請安裝它以繼續",
|
||||
"name": "名稱",
|
||||
"nameRequired": "請輸入伺服器名稱",
|
||||
"noServers": "未設定伺服器",
|
||||
"newServer": "MCP 伺服器",
|
||||
"npx_list": {
|
||||
"actions": "操作",
|
||||
"desc": "搜索並添加 npm 包作為 MCP 服務",
|
||||
@@ -1005,14 +1004,19 @@
|
||||
"usage": "用法",
|
||||
"version": "版本"
|
||||
},
|
||||
"errors": {
|
||||
"32000": "MCP 伺服器啟動失敗,請根據教程檢查參數是否填寫完整"
|
||||
},
|
||||
"serverPlural": "伺服器",
|
||||
"serverSingular": "伺服器",
|
||||
"title": "MCP 伺服器",
|
||||
"toggleError": "切換失敗",
|
||||
"startError": "啟動失敗",
|
||||
"type": "類型",
|
||||
"updateError": "更新伺服器失敗",
|
||||
"updateSuccess": "伺服器更新成功",
|
||||
"url": "URL"
|
||||
"url": "URL",
|
||||
"editMcpJson": "編輯 MCP 配置",
|
||||
"installHelp": "獲取安裝幫助"
|
||||
},
|
||||
"messages.divider": "訊息間顯示分隔線",
|
||||
"messages.grid_columns": "訊息網格展示列數",
|
||||
|
||||
@@ -886,10 +886,7 @@
|
||||
"argsTooltip": "Κάθε παράμετρος σε μια γραμμή",
|
||||
"baseUrlTooltip": "Σύνδεσμος Απομακρυσμένης διεύθυνσης URL",
|
||||
"command": "Εντολή",
|
||||
"commandRequired": "Παρακαλώ εισάγετε την εντολή",
|
||||
"config_description": "Ρυθμίζει το πλαίσιο συντονισμού πρωτοκόλλων διακομιστή",
|
||||
"confirmDelete": "Διαγραφή διακομιστή",
|
||||
"confirmDeleteMessage": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτόν τον διακομιστή;",
|
||||
"deleteError": "Αποτυχία διαγραφής διακομιστή",
|
||||
"deleteSuccess": "Ο διακομιστής διαγράφηκε επιτυχώς",
|
||||
"dependenciesInstall": "Εγκατάσταση εξαρτήσεων",
|
||||
@@ -910,7 +907,6 @@
|
||||
"jsonSaveSuccess": "Η διαμορφωτική ρύθμιση JSON αποθηκεύτηκε επιτυχώς",
|
||||
"missingDependencies": "Απο缺失, παρακαλώ εγκαταστήστε το για να συνεχίσετε",
|
||||
"name": "Όνομα",
|
||||
"nameRequired": "Παρακαλώ εισάγετε το όνομα του διακομιστή",
|
||||
"noServers": "Δεν έχουν ρυθμιστεί διακομιστές",
|
||||
"npx_list": {
|
||||
"actions": "Ενέργειες",
|
||||
|
||||
@@ -886,10 +886,7 @@
|
||||
"argsTooltip": "Cada argumento en una línea",
|
||||
"baseUrlTooltip": "Dirección URL remota",
|
||||
"command": "Comando",
|
||||
"commandRequired": "Por favor ingrese el comando",
|
||||
"config_description": "Configurar modelo de contexto del protocolo del servidor",
|
||||
"confirmDelete": "Eliminar servidor",
|
||||
"confirmDeleteMessage": "¿Está seguro de que desea eliminar este servidor?",
|
||||
"deleteError": "Fallo al eliminar servidor",
|
||||
"deleteSuccess": "Servidor eliminado exitosamente",
|
||||
"dependenciesInstall": "Instalar dependencias",
|
||||
@@ -910,7 +907,6 @@
|
||||
"jsonSaveSuccess": "Configuración JSON guardada exitosamente",
|
||||
"missingDependencies": "Faltan, instalelas para continuar",
|
||||
"name": "Nombre",
|
||||
"nameRequired": "Por favor ingrese el nombre del servidor",
|
||||
"noServers": "No se han configurado servidores",
|
||||
"npx_list": {
|
||||
"actions": "Acciones",
|
||||
|
||||
@@ -886,10 +886,7 @@
|
||||
"argsTooltip": "Chaque argument sur une ligne",
|
||||
"baseUrlTooltip": "Adresse URL distante",
|
||||
"command": "Commande",
|
||||
"commandRequired": "Veuillez entrer une commande",
|
||||
"config_description": "Configurer le modèle du protocole de contexte du serveur",
|
||||
"confirmDelete": "Supprimer le serveur",
|
||||
"confirmDeleteMessage": "Êtes-vous sûr de vouloir supprimer ce serveur ?",
|
||||
"deleteError": "Échec de la suppression du serveur",
|
||||
"deleteSuccess": "Serveur supprimé avec succès",
|
||||
"dependenciesInstall": "Installer les dépendances",
|
||||
@@ -910,7 +907,6 @@
|
||||
"jsonSaveSuccess": "Configuration JSON sauvegardée",
|
||||
"missingDependencies": "Manquantes, veuillez les installer pour continuer",
|
||||
"name": "Nom",
|
||||
"nameRequired": "Veuillez entrer le nom du serveur",
|
||||
"noServers": "Aucun serveur configuré",
|
||||
"npx_list": {
|
||||
"actions": "Actions",
|
||||
|
||||
@@ -886,10 +886,7 @@
|
||||
"argsTooltip": "Cada argumento em uma linha",
|
||||
"baseUrlTooltip": "Endereço de URL remoto",
|
||||
"command": "Comando",
|
||||
"commandRequired": "Digite o comando",
|
||||
"config_description": "Configurar modelo de protocolo de contexto do servidor",
|
||||
"confirmDelete": "Excluir servidor",
|
||||
"confirmDeleteMessage": "Tem certeza de que deseja excluir este servidor?",
|
||||
"deleteError": "Falha ao excluir servidor",
|
||||
"deleteSuccess": "Servidor excluído com sucesso",
|
||||
"dependenciesInstall": "Instalar dependências",
|
||||
@@ -910,7 +907,6 @@
|
||||
"jsonSaveSuccess": "Configuração JSON salva com sucesso",
|
||||
"missingDependencies": "Ausente, instale para continuar",
|
||||
"name": "Nome",
|
||||
"nameRequired": "Digite o nome do servidor",
|
||||
"noServers": "Nenhum servidor configurado",
|
||||
"npx_list": {
|
||||
"actions": "Ações",
|
||||
|
||||
@@ -618,9 +618,9 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
|
||||
const toggelEnableMCP = (mcp: MCPServer) => {
|
||||
setEnabledMCPs((prev) => {
|
||||
const exists = prev.some((item) => item.name === mcp.name)
|
||||
const exists = prev.some((item) => item.id === mcp.id)
|
||||
if (exists) {
|
||||
return prev.filter((item) => item.name !== mcp.name)
|
||||
return prev.filter((item) => item.id !== mcp.id)
|
||||
} else {
|
||||
return [...prev, mcp]
|
||||
}
|
||||
|
||||
@@ -27,19 +27,14 @@ const MCPToolsButton: FC<Props> = ({ enabledMCPs, toggelEnableMCP, ToolbarButton
|
||||
// Check if all active servers are enabled
|
||||
const activeServers = mcpServers.filter((s) => s.isActive)
|
||||
|
||||
const anyEnable = activeServers.some((server) =>
|
||||
enabledMCPs.some((enabledServer) => enabledServer.name === server.name)
|
||||
)
|
||||
const anyEnable = activeServers.some((server) => enabledMCPs.some((enabledServer) => enabledServer.id === server.id))
|
||||
|
||||
const enableAll = () =>
|
||||
mcpServers.forEach((s) => {
|
||||
toggelEnableMCP(s)
|
||||
})
|
||||
const enableAll = () => mcpServers.forEach(toggelEnableMCP)
|
||||
|
||||
const disableAll = () =>
|
||||
mcpServers.forEach((s) => {
|
||||
enabledMCPs.forEach((enabledServer) => {
|
||||
if (enabledServer.name === s.name) {
|
||||
if (enabledServer.id === s.id) {
|
||||
toggelEnableMCP(s)
|
||||
}
|
||||
})
|
||||
@@ -64,32 +59,34 @@ const MCPToolsButton: FC<Props> = ({ enabledMCPs, toggelEnableMCP, ToolbarButton
|
||||
</div>
|
||||
</div>
|
||||
</DropdownHeader>
|
||||
{mcpServers.length > 0 ? (
|
||||
mcpServers
|
||||
.filter((s) => s.isActive)
|
||||
.map((server) => (
|
||||
<McpServerItems key={server.name} className="ant-dropdown-menu-item">
|
||||
<div className="server-info">
|
||||
<div className="server-name">{server.name}</div>
|
||||
{server.description && (
|
||||
<Tooltip title={server.description} placement="bottom">
|
||||
<div className="server-description">{truncateText(server.description)}</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
{server.baseUrl && <div className="server-url">{server.baseUrl}</div>}
|
||||
</div>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={enabledMCPs.some((s) => s.name === server.name)}
|
||||
onChange={() => toggelEnableMCP(server)}
|
||||
/>
|
||||
</McpServerItems>
|
||||
))
|
||||
) : (
|
||||
<div className="ant-dropdown-menu-item-group">
|
||||
<div className="ant-dropdown-menu-item no-results">{t('settings.mcp.noServers')}</div>
|
||||
</div>
|
||||
)}
|
||||
<DropdownBody>
|
||||
{mcpServers.length > 0 ? (
|
||||
mcpServers
|
||||
.filter((s) => s.isActive)
|
||||
.map((server) => (
|
||||
<McpServerItems key={server.id} className="ant-dropdown-menu-item">
|
||||
<div className="server-info">
|
||||
<div className="server-name">{server.name}</div>
|
||||
{server.description && (
|
||||
<Tooltip title={server.description} placement="bottom">
|
||||
<div className="server-description">{truncateText(server.description)}</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
{server.baseUrl && <div className="server-url">{server.baseUrl}</div>}
|
||||
</div>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={enabledMCPs.some((s) => s.id === server.id)}
|
||||
onChange={() => toggelEnableMCP(server)}
|
||||
/>
|
||||
</McpServerItems>
|
||||
))
|
||||
) : (
|
||||
<div className="ant-dropdown-menu-item-group">
|
||||
<div className="ant-dropdown-menu-item no-results">{t('settings.mcp.noServers')}</div>
|
||||
</div>
|
||||
)}
|
||||
</DropdownBody>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -106,7 +103,7 @@ const MCPToolsButton: FC<Props> = ({ enabledMCPs, toggelEnableMCP, ToolbarButton
|
||||
overlayClassName="mention-models-dropdown">
|
||||
<Tooltip placement="top" title={t('settings.mcp.title')} arrow>
|
||||
<ToolbarButton type="text" ref={dropdownRef}>
|
||||
<CodeOutlined style={{ color: enabledMCPs.length > 0 ? '#d97757' : 'var(--color-icon)' }} />
|
||||
<CodeOutlined style={{ color: enabledMCPs.length > 0 ? 'var(--color-primary)' : 'var(--color-icon)' }} />
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
</Dropdown>
|
||||
@@ -127,6 +124,10 @@ const McpServerItems = styled.div`
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: var(--color-text-1);
|
||||
max-width: 400px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.server-description {
|
||||
@@ -177,4 +178,8 @@ const DropdownHeader = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const DropdownBody = styled.div`
|
||||
padding-bottom: 10px;
|
||||
`
|
||||
|
||||
export default MCPToolsButton
|
||||
|
||||
@@ -25,7 +25,9 @@ const MessageGroupModelList: FC<MessageGroupModelListProps> = ({ messages, setSe
|
||||
|
||||
return (
|
||||
<ModelsWrapper>
|
||||
<DisplayModeToggle displayMode={foldDisplayMode} onClick={() => dispatch(setFoldDisplayMode(isCompact ? 'expanded' : 'compact'))}>
|
||||
<DisplayModeToggle
|
||||
displayMode={foldDisplayMode}
|
||||
onClick={() => dispatch(setFoldDisplayMode(isCompact ? 'expanded' : 'compact'))}>
|
||||
<Tooltip
|
||||
title={
|
||||
foldDisplayMode === 'compact'
|
||||
|
||||
@@ -100,8 +100,8 @@ const MessageTools: FC<Props> = ({ message }) => {
|
||||
</MessageTitleLabel>
|
||||
),
|
||||
children: isDone && result && (
|
||||
<ToolResponseContainer style={{ fontFamily, fontSize }}>
|
||||
<pre>{JSON.stringify(result, null, 2)}</pre>
|
||||
<ToolResponseContainer style={{ fontFamily, fontSize: '12px' }}>
|
||||
<CodeBlock>{JSON.stringify(result, null, 2)}</CodeBlock>
|
||||
</ToolResponseContainer>
|
||||
)
|
||||
})
|
||||
@@ -129,9 +129,8 @@ const MessageTools: FC<Props> = ({ message }) => {
|
||||
onCancel={() => setExpandedResponse(null)}
|
||||
footer={null}
|
||||
width="80%"
|
||||
styles={{
|
||||
body: { maxHeight: '80vh', overflow: 'auto' }
|
||||
}}>
|
||||
centered
|
||||
styles={{ body: { maxHeight: '80vh', overflow: 'auto' } }}>
|
||||
{expandedResponse && (
|
||||
<ExpandedResponseContainer style={{ fontFamily, fontSize }}>
|
||||
<ActionButton
|
||||
@@ -145,7 +144,7 @@ const MessageTools: FC<Props> = ({ message }) => {
|
||||
aria-label={t('common.copy')}>
|
||||
<i className="iconfont icon-copy"></i>
|
||||
</ActionButton>
|
||||
<pre>{expandedResponse.content}</pre>
|
||||
<CodeBlock>{expandedResponse.content}</CodeBlock>
|
||||
</ExpandedResponseContainer>
|
||||
)}
|
||||
</Modal>
|
||||
@@ -157,7 +156,6 @@ const CollapseContainer = styled(Collapse)`
|
||||
margin-bottom: 15px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.ant-collapse-header {
|
||||
background-color: var(--color-bg-2);
|
||||
@@ -257,13 +255,14 @@ const ToolResponseContainer = styled.div`
|
||||
max-height: 300px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
position: relative;
|
||||
`
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
color: var(--color-text);
|
||||
}
|
||||
const CodeBlock = styled.pre`
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
color: var(--color-text);
|
||||
font-family: ubuntu;
|
||||
`
|
||||
|
||||
const ExpandedResponseContainer = styled.div`
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import MinAppsPopover from '@renderer/components/Popups/MinAppsPopover'
|
||||
import SearchPopup from '@renderer/components/Popups/SearchPopup'
|
||||
import { isMac, isWindows } from '@renderer/config/constant'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { modelGenerating } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
@@ -71,9 +71,7 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
|
||||
</Tooltip>
|
||||
</NavbarLeft>
|
||||
)}
|
||||
<NavbarRight
|
||||
style={{ justifyContent: 'space-between', paddingRight: isWindows ? 140 : 12, flex: 1 }}
|
||||
className="home-navbar-right">
|
||||
<NavbarRight style={{ justifyContent: 'space-between', flex: 1 }} className="home-navbar-right">
|
||||
<HStack alignItems="center">
|
||||
{!showAssistants && (
|
||||
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={0.8}>
|
||||
|
||||
@@ -202,7 +202,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
||||
allowClear: true
|
||||
}
|
||||
})
|
||||
prompt && updateTopic({ ...topic, prompt: prompt.trim() })
|
||||
prompt !== null && updateTopic({ ...topic, prompt: prompt.trim() })
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -6,13 +6,12 @@ import {
|
||||
SearchOutlined,
|
||||
SettingOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { Navbar, NavbarCenter, NavbarRight as NavbarRightFromComponents } from '@renderer/components/app/Navbar'
|
||||
import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navbar'
|
||||
import DragableList from '@renderer/components/DragableList'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import ListItem from '@renderer/components/ListItem'
|
||||
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { isWindows } from '@renderer/config/constant'
|
||||
import { useKnowledgeBases } from '@renderer/hooks/useKnowledge'
|
||||
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
||||
import { NavbarIcon } from '@renderer/pages/home/Navbar'
|
||||
@@ -252,9 +251,4 @@ const NarrowIcon = styled(NavbarIcon)`
|
||||
}
|
||||
`
|
||||
|
||||
const NavbarRight = styled(NavbarRightFromComponents)`
|
||||
min-width: auto;
|
||||
padding-right: ${isWindows ? '140px' : 15};
|
||||
`
|
||||
|
||||
export default KnowledgePage
|
||||
|
||||
@@ -1,241 +0,0 @@
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { useAppSelector } from '@renderer/store'
|
||||
import { MCPServer } from '@renderer/types'
|
||||
import { Form, Input, Modal, Radio, Switch } from 'antd'
|
||||
import TextArea from 'antd/es/input/TextArea'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface ShowParams {
|
||||
server?: MCPServer
|
||||
create?: boolean
|
||||
}
|
||||
|
||||
interface Props extends ShowParams {
|
||||
resolve: (data: any) => void
|
||||
}
|
||||
|
||||
interface MCPFormValues {
|
||||
name: string
|
||||
description?: string
|
||||
serverType: 'sse' | 'stdio'
|
||||
baseUrl?: string
|
||||
command?: string
|
||||
args?: string
|
||||
env?: string
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
const PopupContainer: React.FC<Props> = ({ server, create, resolve }) => {
|
||||
const [open, setOpen] = useState(true)
|
||||
const { t } = useTranslation()
|
||||
const [serverType, setServerType] = useState<'sse' | 'stdio'>('stdio')
|
||||
const mcpServers = useAppSelector((state) => state.mcp.servers)
|
||||
const [form] = Form.useForm<MCPFormValues>()
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (server) {
|
||||
// Determine server type based on server properties
|
||||
const serverType = server.baseUrl ? 'sse' : 'stdio'
|
||||
setServerType(serverType)
|
||||
|
||||
form.setFieldsValue({
|
||||
name: server.name,
|
||||
description: server.description,
|
||||
serverType: serverType,
|
||||
baseUrl: server.baseUrl || '',
|
||||
command: server.command || '',
|
||||
args: server.args ? server.args.join('\n') : '',
|
||||
env: server.env
|
||||
? Object.entries(server.env)
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join('\n')
|
||||
: '',
|
||||
isActive: server.isActive
|
||||
})
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
// Watch the serverType field to update the form layout dynamically
|
||||
useEffect(() => {
|
||||
const type = form.getFieldValue('serverType')
|
||||
type && setServerType(type)
|
||||
}, [form])
|
||||
|
||||
const onOK = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const values = await form.validateFields()
|
||||
const mcpServer: MCPServer = {
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
isActive: values.isActive
|
||||
}
|
||||
|
||||
if (values.serverType === 'sse') {
|
||||
mcpServer.baseUrl = values.baseUrl
|
||||
} else {
|
||||
mcpServer.command = values.command
|
||||
mcpServer.args = values.args ? values.args.split('\n').filter((arg) => arg.trim() !== '') : []
|
||||
|
||||
const env: Record<string, string> = {}
|
||||
if (values.env) {
|
||||
values.env.split('\n').forEach((line) => {
|
||||
if (line.trim()) {
|
||||
const [key, ...chunks] = line.split('=')
|
||||
const value = chunks.join('=')
|
||||
if (key && value) {
|
||||
env[key.trim()] = value.trim()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
mcpServer.env = Object.keys(env).length > 0 ? env : undefined
|
||||
}
|
||||
|
||||
if (server && !create) {
|
||||
try {
|
||||
await window.api.mcp.updateServer(mcpServer)
|
||||
window.message.success(t('settings.mcp.updateSuccess'))
|
||||
setLoading(false)
|
||||
setOpen(false)
|
||||
form.resetFields()
|
||||
} catch (error: any) {
|
||||
window.message.error(`${t('settings.mcp.updateError')}: ${error.message}`)
|
||||
setLoading(false)
|
||||
}
|
||||
} else {
|
||||
// Check for duplicate name
|
||||
if (mcpServers.some((server: MCPServer) => server.name === mcpServer.name)) {
|
||||
window.message.error(t('settings.mcp.duplicateName'))
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await window.api.mcp.addServer(mcpServer)
|
||||
window.message.success(t('settings.mcp.addSuccess'))
|
||||
setLoading(false)
|
||||
setOpen(false)
|
||||
form.resetFields()
|
||||
} catch (error: any) {
|
||||
window.message.error(`${t('settings.mcp.addError')}: ${error.message}`)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
resolve({})
|
||||
}
|
||||
|
||||
AddMcpServerPopup.hide = onCancel
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={server ? t('settings.mcp.editServer') : t('settings.mcp.addServer')}
|
||||
open={open}
|
||||
onOk={onOK}
|
||||
onCancel={onCancel}
|
||||
afterClose={onClose}
|
||||
confirmLoading={loading}
|
||||
maskClosable={false}
|
||||
width={600}
|
||||
transitionName="ant-move-down"
|
||||
centered
|
||||
styles={{
|
||||
body: {
|
||||
maxHeight: '70vh',
|
||||
overflowY: 'auto'
|
||||
}
|
||||
}}>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t('settings.mcp.name')}
|
||||
rules={[{ required: true, message: t('settings.mcp.nameRequired') }]}>
|
||||
<Input disabled={!!server} placeholder={t('common.name')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="description" label={t('settings.mcp.description')}>
|
||||
<TextArea rows={2} placeholder={t('common.description')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="serverType" label={t('settings.mcp.type')} rules={[{ required: true }]} initialValue="stdio">
|
||||
<Radio.Group
|
||||
onChange={(e) => setServerType(e.target.value)}
|
||||
options={[
|
||||
{ label: 'SSE (Server-Sent Events)', value: 'sse' },
|
||||
{ label: 'STDIO (Standard Input/Output)', value: 'stdio' }
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{serverType === 'sse' && (
|
||||
<Form.Item
|
||||
name="baseUrl"
|
||||
label={t('settings.mcp.url')}
|
||||
rules={[{ required: serverType === 'sse', message: t('settings.mcp.baseUrlRequired') }]}
|
||||
tooltip={t('settings.mcp.baseUrlTooltip')}>
|
||||
<Input placeholder="http://localhost:3000/sse" />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{serverType === 'stdio' && (
|
||||
<>
|
||||
<Form.Item
|
||||
name="command"
|
||||
label={t('settings.mcp.command')}
|
||||
rules={[{ required: serverType === 'stdio', message: t('settings.mcp.commandRequired') }]}>
|
||||
<Input placeholder="uvx or npx" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="args" label={t('settings.mcp.args')} tooltip={t('settings.mcp.argsTooltip')}>
|
||||
<TextArea rows={3} placeholder={`arg1\narg2`} style={{ fontFamily: 'monospace' }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="env" label={t('settings.mcp.env')} tooltip={t('settings.mcp.envTooltip')}>
|
||||
<TextArea rows={3} placeholder={`KEY1=value1\nKEY2=value2`} style={{ fontFamily: 'monospace' }} />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Form.Item name="isActive" label={t('settings.mcp.active')} valuePropName="checked" initialValue={true}>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const TopViewKey = 'AddMcpServerPopup'
|
||||
|
||||
export default class AddMcpServerPopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
TopView.hide(TopViewKey)
|
||||
}
|
||||
static show(props: ShowParams = {}) {
|
||||
return new Promise<any>((resolve) => {
|
||||
TopView.show(
|
||||
<PopupContainer
|
||||
{...props}
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
TopView.hide(TopViewKey)
|
||||
}}
|
||||
/>,
|
||||
TopViewKey
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -21,15 +21,13 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
const dispatch = useAppDispatch()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const ipcRenderer = window.electron.ipcRenderer
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const mcpServersObj: Record<string, any> = {}
|
||||
|
||||
mcpServers.forEach((server) => {
|
||||
const { name, ...serverData } = server
|
||||
mcpServersObj[name] = serverData
|
||||
const { id, ...serverData } = server
|
||||
mcpServersObj[id] = serverData
|
||||
})
|
||||
|
||||
const standardFormat = {
|
||||
@@ -47,6 +45,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
|
||||
const onOk = async () => {
|
||||
setJsonSaving(true)
|
||||
|
||||
try {
|
||||
if (!jsonConfig.trim()) {
|
||||
dispatch(setMCPServers([]))
|
||||
@@ -55,6 +54,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
setJsonSaving(false)
|
||||
return
|
||||
}
|
||||
|
||||
const parsedConfig = JSON.parse(jsonConfig)
|
||||
|
||||
if (!parsedConfig.mcpServers || typeof parsedConfig.mcpServers !== 'object') {
|
||||
@@ -62,9 +62,10 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
}
|
||||
|
||||
const serversArray: MCPServer[] = []
|
||||
for (const [name, serverConfig] of Object.entries(parsedConfig.mcpServers)) {
|
||||
|
||||
for (const [id, serverConfig] of Object.entries(parsedConfig.mcpServers)) {
|
||||
const server: MCPServer = {
|
||||
name,
|
||||
id,
|
||||
isActive: false,
|
||||
...(serverConfig as any)
|
||||
}
|
||||
@@ -72,7 +73,6 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
}
|
||||
|
||||
dispatch(setMCPServers(serversArray))
|
||||
ipcRenderer.send('mcp:servers-from-renderer', mcpServers)
|
||||
|
||||
window.message.success(t('settings.mcp.jsonSaveSuccess'))
|
||||
setJsonError('')
|
||||
|
||||
@@ -1,23 +1,37 @@
|
||||
import { CheckCircleOutlined, QuestionCircleOutlined, WarningOutlined } from '@ant-design/icons'
|
||||
import { Center, VStack } from '@renderer/components/Layout'
|
||||
import { EventEmitter } from '@renderer/services/EventService'
|
||||
import { Alert, Button } from 'antd'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { SettingRow, SettingSubtitle } from '..'
|
||||
import { SettingDescription, SettingRow, SettingSubtitle } from '..'
|
||||
|
||||
const InstallNpxUv: FC = () => {
|
||||
interface Props {
|
||||
mini?: boolean
|
||||
}
|
||||
|
||||
const InstallNpxUv: FC<Props> = ({ mini = false }) => {
|
||||
const [isUvInstalled, setIsUvInstalled] = useState(true)
|
||||
const [isBunInstalled, setIsBunInstalled] = useState(true)
|
||||
const [isInstallingUv, setIsInstallingUv] = useState(false)
|
||||
const [isInstallingBun, setIsInstallingBun] = useState(false)
|
||||
const [uvPath, setUvPath] = useState<string | null>(null)
|
||||
const [bunPath, setBunPath] = useState<string | null>(null)
|
||||
const [binariesDir, setBinariesDir] = useState<string | null>(null)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const checkBinaries = async () => {
|
||||
const uvExists = await window.api.isBinaryExist('uv')
|
||||
const bunExists = await window.api.isBinaryExist('bun')
|
||||
const { uvPath, bunPath, dir } = await window.api.mcp.getInstallInfo()
|
||||
|
||||
setIsUvInstalled(uvExists)
|
||||
setIsBunInstalled(bunExists)
|
||||
setUvPath(uvPath)
|
||||
setBunPath(bunPath)
|
||||
setBinariesDir(dir)
|
||||
}
|
||||
|
||||
const installUV = async () => {
|
||||
@@ -27,7 +41,7 @@ const InstallNpxUv: FC = () => {
|
||||
setIsUvInstalled(true)
|
||||
setIsInstallingUv(false)
|
||||
} catch (error: any) {
|
||||
window.message.error(`${t('settings.mcp.installError')}: ${error.message}`)
|
||||
window.message.error({ content: `${t('settings.mcp.installError')}: ${error.message}`, key: 'mcp-install-error' })
|
||||
setIsInstallingUv(false)
|
||||
checkBinaries()
|
||||
}
|
||||
@@ -40,7 +54,10 @@ const InstallNpxUv: FC = () => {
|
||||
setIsBunInstalled(true)
|
||||
setIsInstallingBun(false)
|
||||
} catch (error: any) {
|
||||
window.message.error(`${t('settings.mcp.installError')}: ${error.message}`)
|
||||
window.message.error({
|
||||
content: `${t('settings.mcp.installError')}: ${error.message}`,
|
||||
key: 'mcp-install-error'
|
||||
})
|
||||
setIsInstallingBun(false)
|
||||
checkBinaries()
|
||||
}
|
||||
@@ -50,56 +67,99 @@ const InstallNpxUv: FC = () => {
|
||||
checkBinaries()
|
||||
}, [])
|
||||
|
||||
if (isUvInstalled && isBunInstalled) {
|
||||
return null
|
||||
if (mini) {
|
||||
const installed = isUvInstalled && isBunInstalled
|
||||
return (
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
variant="filled"
|
||||
shape="circle"
|
||||
icon={installed ? <CheckCircleOutlined /> : <WarningOutlined />}
|
||||
className="nodrag"
|
||||
color={installed ? 'green' : 'danger'}
|
||||
onClick={() => EventEmitter.emit('mcp:mcp-install')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const openBinariesDir = () => {
|
||||
if (binariesDir) {
|
||||
window.api.openPath(binariesDir)
|
||||
}
|
||||
}
|
||||
|
||||
const onHelp = () => {
|
||||
window.open('https://docs.cherry-ai.com/advanced-basic/mcp', '_blank')
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{!isUvInstalled && (
|
||||
<Alert
|
||||
type="warning"
|
||||
banner
|
||||
style={{ padding: 8 }}
|
||||
description={
|
||||
<SettingRow>
|
||||
<SettingSubtitle style={{ margin: 0 }}>
|
||||
<Alert
|
||||
type={isUvInstalled ? 'success' : 'warning'}
|
||||
banner
|
||||
description={
|
||||
<VStack>
|
||||
<SettingRow style={{ width: '100%' }}>
|
||||
<SettingSubtitle style={{ margin: 0, fontWeight: 'normal' }}>
|
||||
{isUvInstalled ? 'UV Installed' : `UV ${t('settings.mcp.missingDependencies')}`}
|
||||
</SettingSubtitle>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={installUV}
|
||||
loading={isInstallingUv}
|
||||
disabled={isInstallingUv}
|
||||
size="small">
|
||||
{isInstallingUv ? t('settings.mcp.dependenciesInstalling') : t('settings.mcp.install')}
|
||||
</Button>
|
||||
{!isUvInstalled && (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={installUV}
|
||||
loading={isInstallingUv}
|
||||
disabled={isInstallingUv}
|
||||
size="small">
|
||||
{isInstallingUv ? t('settings.mcp.dependenciesInstalling') : t('settings.mcp.install')}
|
||||
</Button>
|
||||
)}
|
||||
</SettingRow>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{!isBunInstalled && (
|
||||
<Alert
|
||||
type="warning"
|
||||
banner
|
||||
style={{ padding: 8 }}
|
||||
description={
|
||||
<SettingRow>
|
||||
<SettingSubtitle style={{ margin: 0 }}>
|
||||
<SettingRow style={{ width: '100%' }}>
|
||||
<SettingDescription
|
||||
onClick={openBinariesDir}
|
||||
style={{ margin: 0, fontWeight: 'normal', cursor: 'pointer' }}>
|
||||
{uvPath}
|
||||
</SettingDescription>
|
||||
</SettingRow>
|
||||
</VStack>
|
||||
}
|
||||
/>
|
||||
<Alert
|
||||
type={isBunInstalled ? 'success' : 'warning'}
|
||||
banner
|
||||
description={
|
||||
<VStack>
|
||||
<SettingRow style={{ width: '100%' }}>
|
||||
<SettingSubtitle style={{ margin: 0, fontWeight: 'normal' }}>
|
||||
{isBunInstalled ? 'Bun Installed' : `Bun ${t('settings.mcp.missingDependencies')}`}
|
||||
</SettingSubtitle>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={installBun}
|
||||
loading={isInstallingBun}
|
||||
disabled={isInstallingBun}
|
||||
size="small">
|
||||
{isInstallingBun ? t('settings.mcp.dependenciesInstalling') : t('settings.mcp.install')}
|
||||
</Button>
|
||||
{!isBunInstalled && (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={installBun}
|
||||
loading={isInstallingBun}
|
||||
disabled={isInstallingBun}
|
||||
size="small">
|
||||
{isInstallingBun ? t('settings.mcp.dependenciesInstalling') : t('settings.mcp.install')}
|
||||
</Button>
|
||||
)}
|
||||
</SettingRow>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<SettingRow style={{ width: '100%' }}>
|
||||
<SettingDescription
|
||||
onClick={openBinariesDir}
|
||||
style={{ margin: 0, fontWeight: 'normal', cursor: 'pointer' }}>
|
||||
{bunPath}
|
||||
</SettingDescription>
|
||||
</SettingRow>
|
||||
</VStack>
|
||||
}
|
||||
/>
|
||||
<Center>
|
||||
<Button type="link" onClick={onHelp} icon={<QuestionCircleOutlined />}>
|
||||
{t('settings.mcp.installHelp')}
|
||||
</Button>
|
||||
</Center>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -108,7 +168,7 @@ const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 20px;
|
||||
gap: 10px;
|
||||
gap: 12px;
|
||||
`
|
||||
|
||||
export default InstallNpxUv
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
||||
import { MCPServer } from '@renderer/types'
|
||||
import { Button, Flex, Form, Input, Radio, Switch } from 'antd'
|
||||
import TextArea from 'antd/es/input/TextArea'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '..'
|
||||
|
||||
interface Props {
|
||||
server: MCPServer
|
||||
}
|
||||
|
||||
interface MCPFormValues {
|
||||
name: string
|
||||
description?: string
|
||||
serverType: 'sse' | 'stdio'
|
||||
baseUrl?: string
|
||||
command?: string
|
||||
args?: string
|
||||
env?: string
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
const McpSettings: React.FC<Props> = ({ server }) => {
|
||||
const { t } = useTranslation()
|
||||
const [serverType, setServerType] = useState<'sse' | 'stdio'>('stdio')
|
||||
const [form] = Form.useForm<MCPFormValues>()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [isFormChanged, setIsFormChanged] = useState(false)
|
||||
const [loadingServer, setLoadingServer] = useState<string | null>(null)
|
||||
const { updateMCPServer } = useMCPServers()
|
||||
|
||||
useEffect(() => {
|
||||
if (server) {
|
||||
form.setFieldsValue({
|
||||
name: server.name,
|
||||
description: server.description,
|
||||
serverType: server.baseUrl ? 'sse' : 'stdio',
|
||||
baseUrl: server.baseUrl || '',
|
||||
command: server.command || '',
|
||||
args: server.args ? server.args.join('\n') : '',
|
||||
env: server.env
|
||||
? Object.entries(server.env)
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join('\n')
|
||||
: '',
|
||||
isActive: server.isActive
|
||||
})
|
||||
}
|
||||
}, [form, server])
|
||||
|
||||
useEffect(() => {
|
||||
const serverType = server.baseUrl ? 'sse' : 'stdio'
|
||||
setServerType(serverType)
|
||||
|
||||
form.setFieldsValue({
|
||||
name: server.name,
|
||||
description: server.description,
|
||||
serverType: serverType,
|
||||
baseUrl: server.baseUrl || '',
|
||||
command: server.command || '',
|
||||
args: server.args ? server.args.join('\n') : '',
|
||||
env: server.env
|
||||
? Object.entries(server.env)
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join('\n')
|
||||
: ''
|
||||
})
|
||||
}, [form, server])
|
||||
|
||||
// Watch the serverType field to update the form layout dynamically
|
||||
useEffect(() => {
|
||||
const type = form.getFieldValue('serverType')
|
||||
type && setServerType(type)
|
||||
}, [form])
|
||||
|
||||
const onSave = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const values = await form.validateFields()
|
||||
|
||||
const mcpServer: MCPServer = {
|
||||
id: server.id,
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
isActive: values.isActive
|
||||
}
|
||||
|
||||
if (values.serverType === 'sse') {
|
||||
mcpServer.baseUrl = values.baseUrl
|
||||
} else {
|
||||
mcpServer.command = values.command
|
||||
mcpServer.args = values.args ? values.args.split('\n').filter((arg) => arg.trim() !== '') : []
|
||||
|
||||
const env: Record<string, string> = {}
|
||||
if (values.env) {
|
||||
values.env.split('\n').forEach((line) => {
|
||||
if (line.trim()) {
|
||||
const [key, ...chunks] = line.split('=')
|
||||
const value = chunks.join('=')
|
||||
if (key && value) {
|
||||
env[key.trim()] = value.trim()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
mcpServer.env = Object.keys(env).length > 0 ? env : undefined
|
||||
}
|
||||
|
||||
try {
|
||||
await window.api.mcp.listTools(mcpServer)
|
||||
updateMCPServer({ ...mcpServer, isActive: true })
|
||||
window.message.success({ content: t('settings.mcp.updateSuccess'), key: 'mcp-update-success' })
|
||||
setLoading(false)
|
||||
setIsFormChanged(false)
|
||||
} catch (error: any) {
|
||||
updateMCPServer({ ...mcpServer, isActive: false })
|
||||
window.modal.error({
|
||||
title: t('settings.mcp.updateError'),
|
||||
content: error.message,
|
||||
centered: true
|
||||
})
|
||||
setLoading(false)
|
||||
}
|
||||
} catch (error: any) {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const onFormValuesChange = () => {
|
||||
setIsFormChanged(true)
|
||||
}
|
||||
|
||||
const formatError = (error: any) => {
|
||||
if (error.message.includes('32000')) {
|
||||
return t('settings.mcp.errors.32000')
|
||||
}
|
||||
|
||||
return error.message
|
||||
}
|
||||
|
||||
const onToggleActive = async (active: boolean) => {
|
||||
await form.validateFields()
|
||||
setLoadingServer(server.id)
|
||||
|
||||
try {
|
||||
if (active) {
|
||||
await window.api.mcp.listTools(server)
|
||||
}
|
||||
updateMCPServer({ ...server, isActive: active })
|
||||
} catch (error: any) {
|
||||
window.modal.error({
|
||||
title: t('settings.mcp.startError'),
|
||||
content: formatError(error),
|
||||
centered: true
|
||||
})
|
||||
console.error('[MCP] Error toggling server active', error)
|
||||
} finally {
|
||||
setLoadingServer(null)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingContainer>
|
||||
<SettingGroup style={{ marginBottom: 0 }}>
|
||||
<SettingTitle>
|
||||
<ServerName>{server?.name}</ServerName>
|
||||
<Flex align="center" gap={16}>
|
||||
<Switch
|
||||
value={server.isActive}
|
||||
key={server.id}
|
||||
loading={loadingServer === server.id}
|
||||
onChange={onToggleActive}
|
||||
/>
|
||||
<Button type="primary" size="small" onClick={onSave} loading={loading} disabled={!isFormChanged}>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</SettingTitle>
|
||||
<SettingDivider />
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onValuesChange={onFormValuesChange}
|
||||
style={{
|
||||
height: 'calc(100vh - var(--navbar-height) - 115px)',
|
||||
overflowY: 'auto',
|
||||
width: 'calc(100% + 10px)',
|
||||
paddingRight: '10px'
|
||||
}}>
|
||||
<Form.Item name="name" label={t('settings.mcp.name')} rules={[{ required: true, message: '' }]}>
|
||||
<Input placeholder={t('common.name')} />
|
||||
</Form.Item>
|
||||
<Form.Item name="description" label={t('settings.mcp.description')}>
|
||||
<TextArea rows={2} placeholder={t('common.description')} />
|
||||
</Form.Item>
|
||||
<Form.Item name="serverType" label={t('settings.mcp.type')} rules={[{ required: true }]} initialValue="stdio">
|
||||
<Radio.Group
|
||||
onChange={(e) => setServerType(e.target.value)}
|
||||
options={[
|
||||
{ label: 'SSE', value: 'sse' },
|
||||
{ label: 'STDIO', value: 'stdio' }
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
{serverType === 'sse' && (
|
||||
<Form.Item
|
||||
name="baseUrl"
|
||||
label={t('settings.mcp.url')}
|
||||
rules={[{ required: serverType === 'sse', message: '' }]}
|
||||
tooltip={t('settings.mcp.baseUrlTooltip')}>
|
||||
<Input placeholder="http://localhost:3000/sse" />
|
||||
</Form.Item>
|
||||
)}
|
||||
{serverType === 'stdio' && (
|
||||
<>
|
||||
<Form.Item
|
||||
name="command"
|
||||
label={t('settings.mcp.command')}
|
||||
rules={[{ required: serverType === 'stdio', message: '' }]}>
|
||||
<Input placeholder="uvx or npx" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="args"
|
||||
label={t('settings.mcp.args')}
|
||||
tooltip={t('settings.mcp.argsTooltip')}
|
||||
rules={[{ required: serverType === 'stdio', message: '' }]}>
|
||||
<TextArea rows={3} placeholder={`arg1\narg2`} style={{ fontFamily: 'monospace' }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="env" label={t('settings.mcp.env')} tooltip={t('settings.mcp.envTooltip')}>
|
||||
<TextArea rows={3} placeholder={`KEY1=value1\nKEY2=value2`} style={{ fontFamily: 'monospace' }} />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</SettingGroup>
|
||||
</SettingContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const ServerName = styled.span`
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
`
|
||||
|
||||
export default McpSettings
|
||||
@@ -0,0 +1,49 @@
|
||||
import { EditOutlined, ExportOutlined, SearchOutlined } from '@ant-design/icons'
|
||||
import { NavbarRight } from '@renderer/components/app/Navbar'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { EventEmitter } from '@renderer/services/EventService'
|
||||
import { Button } from 'antd'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import EditMcpJsonPopup from './EditMcpJsonPopup'
|
||||
import InstallNpxUv from './InstallNpxUv'
|
||||
|
||||
export const McpSettingsNavbar = () => {
|
||||
const { t } = useTranslation()
|
||||
const onClick = () => window.open('https://mcp.so/', '_blank')
|
||||
|
||||
return (
|
||||
<NavbarRight>
|
||||
<HStack alignItems="center" gap={5}>
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
onClick={() => EventEmitter.emit('mcp:npx-search')}
|
||||
icon={<SearchOutlined />}
|
||||
className="nodrag"
|
||||
style={{ fontSize: 13, height: 28, borderRadius: 20 }}>
|
||||
{t('settings.mcp.searchNpx')}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
onClick={() => EditMcpJsonPopup.show()}
|
||||
icon={<EditOutlined />}
|
||||
className="nodrag"
|
||||
style={{ fontSize: 13, height: 28, borderRadius: 20 }}>
|
||||
{t('settings.mcp.editMcpJson')}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
onClick={onClick}
|
||||
icon={<ExportOutlined />}
|
||||
className="nodrag"
|
||||
style={{ fontSize: 13, height: 28, borderRadius: 20 }}>
|
||||
{t('settings.mcp.findMore')}
|
||||
</Button>
|
||||
<InstallNpxUv mini />
|
||||
</HStack>
|
||||
</NavbarRight>
|
||||
)
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
import { SearchOutlined } from '@ant-design/icons'
|
||||
import { PlusOutlined, SearchOutlined } from '@ant-design/icons'
|
||||
import { nanoid } from '@reduxjs/toolkit'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
||||
import type { MCPServer } from '@renderer/types'
|
||||
import { Button, Input, Space, Spin, Table, Typography } from 'antd'
|
||||
import { Button, Card, Flex, Input, Space, Spin, Tag, Typography } from 'antd'
|
||||
import { npxFinder } from 'npx-scope-finder'
|
||||
import { type FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled, { css } from 'styled-components'
|
||||
|
||||
import { SettingDivider, SettingGroup, SettingTitle } from '..'
|
||||
import AddMcpServerPopup from './AddMcpServerPopup'
|
||||
|
||||
interface SearchResult {
|
||||
name: string
|
||||
@@ -18,20 +21,31 @@ interface SearchResult {
|
||||
fullName: string
|
||||
}
|
||||
|
||||
const npmScopes = ['@mcpmarket', '@modelcontextprotocol', '@gongrzhe']
|
||||
|
||||
let _searchResults: SearchResult[] = []
|
||||
|
||||
const NpxSearch: FC = () => {
|
||||
const { theme } = useTheme()
|
||||
const { t } = useTranslation()
|
||||
const { Paragraph, Text, Link } = Typography
|
||||
const { Text, Link } = Typography
|
||||
|
||||
// Add new state variables for npm scope search
|
||||
const [npmScope, setNpmScope] = useState('@modelcontextprotocol')
|
||||
const [searchLoading, setSearchLoading] = useState(false)
|
||||
const [searchResults, setSearchResults] = useState<SearchResult[]>([])
|
||||
const [searchResults, setSearchResults] = useState<SearchResult[]>(_searchResults)
|
||||
const { addMCPServer } = useMCPServers()
|
||||
|
||||
_searchResults = searchResults
|
||||
|
||||
// Add new function to handle npm scope search
|
||||
const handleNpmSearch = async () => {
|
||||
if (!npmScope.trim()) {
|
||||
window.message.warning(t('settings.mcp.npx_list.scope_required'))
|
||||
window.message.warning({ content: t('settings.mcp.npx_list.scope_required'), key: 'mcp-npx-scope-required' })
|
||||
return
|
||||
}
|
||||
|
||||
if (searchLoading) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -45,7 +59,7 @@ const NpxSearch: FC = () => {
|
||||
const formattedResults = packages.map((pkg) => {
|
||||
return {
|
||||
key: pkg.name,
|
||||
name: pkg.name || '',
|
||||
name: pkg.name?.split('/')[1] || '',
|
||||
description: pkg.description || 'No description available',
|
||||
version: pkg.version || 'Latest',
|
||||
usage: `npx ${pkg.name}`,
|
||||
@@ -57,13 +71,16 @@ const NpxSearch: FC = () => {
|
||||
setSearchResults(formattedResults)
|
||||
|
||||
if (formattedResults.length === 0) {
|
||||
window.message.info(t('settings.mcp.npx_list.no_packages'))
|
||||
window.message.info({ content: t('settings.mcp.npx_list.no_packages'), key: 'mcp-npx-no-packages' })
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
window.message.error(`${t('settings.mcp.npx_list.search_error')}: ${error.message}`)
|
||||
window.message.error({
|
||||
content: `${t('settings.mcp.npx_list.search_error')}: ${error.message}`,
|
||||
key: 'mcp-npx-search-error'
|
||||
})
|
||||
} else {
|
||||
window.message.error(t('settings.mcp.npx_list.search_error'))
|
||||
window.message.error({ content: t('settings.mcp.npx_list.search_error'), key: 'mcp-npx-search-error' })
|
||||
}
|
||||
} finally {
|
||||
setSearchLoading(false)
|
||||
@@ -71,97 +88,115 @@ const NpxSearch: FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingTitle>{t('settings.mcp.npx_list.title')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<Paragraph type="secondary" style={{ margin: '0 0 10px 0' }}>
|
||||
{t('settings.mcp.npx_list.desc')}
|
||||
</Paragraph>
|
||||
<SettingGroup theme={theme} css={SettingGroupCss}>
|
||||
<div>
|
||||
<SettingTitle>
|
||||
{t('settings.mcp.npx_list.title')} <Text type="secondary">{t('settings.mcp.npx_list.desc')}</Text>
|
||||
</SettingTitle>
|
||||
<SettingDivider />
|
||||
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Space.Compact style={{ width: '100%', marginBottom: 10 }}>
|
||||
<Input
|
||||
placeholder={t('settings.mcp.npx_list.scope_placeholder')}
|
||||
value={npmScope}
|
||||
onChange={(e) => setNpmScope(e.target.value)}
|
||||
onPressEnter={handleNpmSearch}
|
||||
/>
|
||||
<Button icon={<SearchOutlined />} onClick={handleNpmSearch} disabled={searchLoading}>
|
||||
{t('settings.mcp.npx_list.search')}
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Space.Compact style={{ width: '100%', marginBottom: 10 }}>
|
||||
<Input
|
||||
placeholder={t('settings.mcp.npx_list.scope_placeholder')}
|
||||
value={npmScope}
|
||||
onChange={(e) => setNpmScope(e.target.value)}
|
||||
onPressEnter={handleNpmSearch}
|
||||
/>
|
||||
<Button icon={<SearchOutlined />} onClick={handleNpmSearch} disabled={searchLoading}>
|
||||
{t('settings.mcp.npx_list.search')}
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
<HStack alignItems="center" mt="-5px" mb="5px">
|
||||
{npmScopes.map((scope) => (
|
||||
<Tag
|
||||
key={scope}
|
||||
onClick={() => {
|
||||
if (!searchLoading) {
|
||||
setNpmScope(scope)
|
||||
setTimeout(handleNpmSearch, 100)
|
||||
}
|
||||
}}
|
||||
style={{ cursor: searchLoading ? 'not-allowed' : 'pointer' }}>
|
||||
{scope}
|
||||
</Tag>
|
||||
))}
|
||||
</HStack>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<ResultList>
|
||||
{searchLoading ? (
|
||||
<div style={{ textAlign: 'center', padding: '20px' }}>
|
||||
<Spin />
|
||||
</div>
|
||||
) : searchResults.length > 0 ? (
|
||||
<Table
|
||||
dataSource={searchResults}
|
||||
columns={[
|
||||
{
|
||||
title: t('settings.mcp.npx_list.package_name'),
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: '200px'
|
||||
},
|
||||
{
|
||||
title: t('settings.mcp.npx_list.description'),
|
||||
key: 'description',
|
||||
ellipsis: true,
|
||||
render: (_, record: SearchResult) => (
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
<Text ellipsis={{ tooltip: true }}>{record.description}</Text>
|
||||
<Text ellipsis={{ tooltip: true }} type="secondary">
|
||||
{t('settings.mcp.npx_list.usage')}: {record.usage}
|
||||
</Text>
|
||||
<Paragraph ellipsis={{ tooltip: true }}>
|
||||
<Link href={record.npmLink} target="_blank" rel="noopener noreferrer">
|
||||
{record.npmLink}
|
||||
</Link>
|
||||
</Paragraph>
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('settings.mcp.npx_list.version'),
|
||||
dataIndex: 'version',
|
||||
key: 'version',
|
||||
width: '100px'
|
||||
},
|
||||
{
|
||||
title: t('settings.mcp.npx_list.actions'),
|
||||
key: 'actions',
|
||||
width: '120px',
|
||||
render: (_, record: SearchResult) => (
|
||||
searchResults.map((record) => (
|
||||
<Card
|
||||
size="small"
|
||||
key={record.npmLink}
|
||||
title={
|
||||
<Typography.Title level={5} style={{ margin: 0 }}>
|
||||
{record.name}
|
||||
</Typography.Title>
|
||||
}
|
||||
extra={
|
||||
<Flex>
|
||||
<Tag bordered={false} color="processing">
|
||||
v{record.version}
|
||||
</Tag>
|
||||
<Button
|
||||
type="primary"
|
||||
type="text"
|
||||
icon={<PlusOutlined />}
|
||||
size="small"
|
||||
onClick={() => {
|
||||
// 创建一个临时的 MCP 服务器对象
|
||||
const tempServer: MCPServer = {
|
||||
id: nanoid(),
|
||||
name: record.name,
|
||||
description: `${record.description}\n\n${t('settings.mcp.npx_list.usage')}: ${record.usage}\n${t('settings.mcp.npx_list.npm')}: ${record.npmLink}`,
|
||||
command: 'npx',
|
||||
args: ['-y', record.fullName],
|
||||
isActive: true
|
||||
isActive: false
|
||||
}
|
||||
|
||||
// 使用 showEditModal 函数设置表单值并显示弹窗
|
||||
AddMcpServerPopup.show({ server: tempServer, create: true })
|
||||
}}>
|
||||
{t('settings.mcp.addServer')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
]}
|
||||
pagination={false}
|
||||
bordered
|
||||
/>
|
||||
addMCPServer(tempServer)
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
}>
|
||||
<Space direction="vertical" size="small">
|
||||
<Text>{record.description}</Text>
|
||||
<Text type="secondary">
|
||||
{t('settings.mcp.npx_list.usage')}: {record.usage}
|
||||
</Text>
|
||||
<Link href={record.npmLink} target="_blank" rel="noopener noreferrer">
|
||||
{record.npmLink}
|
||||
</Link>
|
||||
</Space>
|
||||
</Card>
|
||||
))
|
||||
) : null}
|
||||
</Space>
|
||||
</ResultList>
|
||||
</SettingGroup>
|
||||
)
|
||||
}
|
||||
|
||||
const SettingGroupCss = css`
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-bottom: 0;
|
||||
`
|
||||
|
||||
const ResultList = styled.div`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
width: calc(100% + 10px);
|
||||
padding-right: 4px;
|
||||
overflow-y: scroll;
|
||||
`
|
||||
|
||||
export default NpxSearch
|
||||
|
||||
@@ -1,188 +1,179 @@
|
||||
import {
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
LinkOutlined,
|
||||
PlusOutlined,
|
||||
QuestionCircleOutlined,
|
||||
SearchOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { CodeOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons'
|
||||
import { nanoid } from '@reduxjs/toolkit'
|
||||
import DragableList from '@renderer/components/DragableList'
|
||||
import IndicatorLight from '@renderer/components/IndicatorLight'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import ListItem from '@renderer/components/ListItem'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useAppSelector } from '@renderer/store'
|
||||
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
||||
import { EventEmitter } from '@renderer/services/EventService'
|
||||
import { MCPServer } from '@renderer/types'
|
||||
import { Button, Space, Switch, Table, Tag, Tooltip, Typography } from 'antd'
|
||||
import { FC, useState } from 'react'
|
||||
import { Dropdown, MenuProps } from 'antd'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { FC, useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '..'
|
||||
import AddMcpServerPopup from './AddMcpServerPopup'
|
||||
import EditMcpJsonPopup from './EditMcpJsonPopup'
|
||||
import { SettingContainer } from '..'
|
||||
import InstallNpxUv from './InstallNpxUv'
|
||||
import McpSettings from './McpSettings'
|
||||
import NpxSearch from './NpxSearch'
|
||||
|
||||
const MCPSettings: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { mcpServers, addMCPServer, updateMcpServers, deleteMCPServer } = useMCPServers()
|
||||
const [selectedMcpServer, setSelectedMcpServer] = useState<MCPServer | null>(mcpServers[0])
|
||||
const [route, setRoute] = useState<'npx-search' | 'mcp-install' | null>(null)
|
||||
const { theme } = useTheme()
|
||||
const { Paragraph, Text } = Typography
|
||||
const mcpServers = useAppSelector((state) => state.mcp.servers)
|
||||
const [loadingServer, setLoadingServer] = useState<string | null>(null)
|
||||
|
||||
const handleDelete = (serverName: string) => {
|
||||
window.modal.confirm({
|
||||
title: t('settings.mcp.confirmDelete'),
|
||||
content: t('settings.mcp.confirmDeleteMessage'),
|
||||
okText: t('common.delete'),
|
||||
okButtonProps: { danger: true },
|
||||
cancelText: t('common.cancel'),
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await window.api.mcp.deleteServer(serverName)
|
||||
window.message.success(t('settings.mcp.deleteSuccess'))
|
||||
} catch (error: any) {
|
||||
window.message.error(`${t('settings.mcp.deleteError')}: ${error.message}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
useEffect(() => {
|
||||
const unsubs = [
|
||||
EventEmitter.on('mcp:npx-search', () => setRoute('npx-search')),
|
||||
EventEmitter.on('mcp:mcp-install', () => setRoute('mcp-install'))
|
||||
]
|
||||
return () => unsubs.forEach((unsub) => unsub())
|
||||
}, [])
|
||||
|
||||
const handleToggleActive = async (name: string, isActive: boolean) => {
|
||||
setLoadingServer(name)
|
||||
try {
|
||||
await window.api.mcp.setServerActive(name, isActive)
|
||||
} catch (error: any) {
|
||||
window.message.error(`${t('settings.mcp.toggleError')}: ${error.message}`)
|
||||
} finally {
|
||||
setLoadingServer(null)
|
||||
const onAddMcpServer = async () => {
|
||||
const newServer = {
|
||||
id: nanoid(),
|
||||
name: t('settings.mcp.newServer'),
|
||||
description: '',
|
||||
baseUrl: '',
|
||||
command: '',
|
||||
args: [],
|
||||
env: {},
|
||||
isActive: false
|
||||
}
|
||||
addMCPServer(newServer)
|
||||
window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-list' })
|
||||
setSelectedMcpServer(newServer)
|
||||
}
|
||||
|
||||
const handleOpenMCPServers = () => {
|
||||
window.open('https://glama.ai/mcp/servers', '_blank')
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t('settings.mcp.name'),
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: '300px',
|
||||
render: (text: string, record: MCPServer) => <Text strong={record.isActive}>{text}</Text>
|
||||
},
|
||||
{
|
||||
title: t('settings.mcp.type'),
|
||||
key: 'type',
|
||||
width: '100px',
|
||||
render: (_: any, record: MCPServer) => <Tag color="cyan">{record.baseUrl ? 'SSE' : 'STDIO'}</Tag>
|
||||
},
|
||||
{
|
||||
title: t('settings.mcp.description'),
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
width: 'auto',
|
||||
render: (text: string) => {
|
||||
if (!text) {
|
||||
return (
|
||||
<Text type="secondary" italic>
|
||||
{t('common.description')}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Paragraph
|
||||
className="selectable"
|
||||
ellipsis={{
|
||||
rows: 1,
|
||||
expandable: 'collapsible',
|
||||
symbol: t('common.more'),
|
||||
onExpand: () => {}, // Empty callback required for proper functionality
|
||||
tooltip: true
|
||||
}}
|
||||
style={{ marginBottom: 0 }}>
|
||||
{text}
|
||||
</Paragraph>
|
||||
)
|
||||
const onDeleteMcpServer = useCallback(
|
||||
async (server: MCPServer) => {
|
||||
try {
|
||||
await window.api.mcp.removeServer(server)
|
||||
await deleteMCPServer(server.id)
|
||||
window.message.success({ content: t('settings.mcp.deleteSuccess'), key: 'mcp-list' })
|
||||
} catch (error: any) {
|
||||
window.message.error({
|
||||
content: `${t('settings.mcp.deleteError')}: ${error.message}`,
|
||||
key: 'mcp-list'
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('settings.mcp.active'),
|
||||
dataIndex: 'isActive',
|
||||
key: 'isActive',
|
||||
width: '100px',
|
||||
render: (isActive: boolean, record: MCPServer) => (
|
||||
<Switch
|
||||
checked={isActive}
|
||||
loading={loadingServer === record.name}
|
||||
onChange={(checked) => handleToggleActive(record.name, checked)}
|
||||
/>
|
||||
)
|
||||
[deleteMCPServer, t]
|
||||
)
|
||||
|
||||
const getMenuItems = useCallback(
|
||||
(server: MCPServer) => {
|
||||
const menus: MenuProps['items'] = [
|
||||
{
|
||||
label: t('common.delete'),
|
||||
danger: true,
|
||||
key: 'delete',
|
||||
icon: <DeleteOutlined />,
|
||||
onClick: () => onDeleteMcpServer(server)
|
||||
}
|
||||
]
|
||||
return menus
|
||||
},
|
||||
{
|
||||
title: t('settings.mcp.actions'),
|
||||
key: 'actions',
|
||||
width: '100px',
|
||||
render: (_: any, record: MCPServer) => (
|
||||
<Space>
|
||||
<Tooltip title={t('common.edit')}>
|
||||
<Button
|
||||
type="primary"
|
||||
ghost
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => AddMcpServerPopup.show({ server: record })}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('common.delete')}>
|
||||
<Button danger icon={<DeleteOutlined />} onClick={() => handleDelete(record.name)} />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
[onDeleteMcpServer, t]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const _selectedMcpServer = mcpServers.find((server) => server.id === selectedMcpServer?.id)
|
||||
setSelectedMcpServer(_selectedMcpServer || mcpServers[0])
|
||||
}, [mcpServers, route, selectedMcpServer])
|
||||
|
||||
const MainContent = useCallback(() => {
|
||||
if (route === 'npx-search' || isEmpty(mcpServers)) {
|
||||
return (
|
||||
<SettingContainer theme={theme}>
|
||||
<NpxSearch />
|
||||
</SettingContainer>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
// Create a CSS class for inactive rows instead of using jsx global
|
||||
const inactiveRowStyle = {
|
||||
opacity: 0.7,
|
||||
backgroundColor: theme === 'dark' ? '#1a1a1a' : '#f5f5f5'
|
||||
}
|
||||
if (route === 'mcp-install') {
|
||||
return (
|
||||
<SettingContainer theme={theme}>
|
||||
<InstallNpxUv />
|
||||
</SettingContainer>
|
||||
)
|
||||
}
|
||||
|
||||
if (selectedMcpServer) {
|
||||
return <McpSettings server={selectedMcpServer} />
|
||||
}
|
||||
|
||||
return <NpxSearch />
|
||||
}, [mcpServers, route, selectedMcpServer, theme])
|
||||
|
||||
return (
|
||||
<SettingContainer theme={theme}>
|
||||
<InstallNpxUv />
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingTitle>
|
||||
{t('settings.mcp.title')}
|
||||
<Tooltip title={t('settings.mcp.config_description')}>
|
||||
<QuestionCircleOutlined style={{ marginLeft: 8, fontSize: 14 }} />
|
||||
</Tooltip>
|
||||
</SettingTitle>
|
||||
<SettingDivider />
|
||||
<HStack gap={15} alignItems="center">
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => AddMcpServerPopup.show()}>
|
||||
{t('settings.mcp.addServer')}
|
||||
</Button>
|
||||
<Button icon={<EditOutlined />} onClick={() => EditMcpJsonPopup.show()}>
|
||||
{t('settings.mcp.editJson')}
|
||||
</Button>
|
||||
<Button icon={<SearchOutlined />} onClick={handleOpenMCPServers}>
|
||||
{t('settings.mcp.findMore')} <LinkOutlined />
|
||||
</Button>
|
||||
</HStack>
|
||||
<Table
|
||||
dataSource={mcpServers}
|
||||
columns={columns}
|
||||
rowKey="name"
|
||||
pagination={false}
|
||||
size="small"
|
||||
locale={{ emptyText: t('settings.mcp.noServers') }}
|
||||
rowClassName={(record) => (!record.isActive ? 'inactive-row' : '')}
|
||||
onRow={(record) => ({ style: !record.isActive ? inactiveRowStyle : {} })}
|
||||
style={{ marginTop: 15 }}
|
||||
<Container>
|
||||
<McpList>
|
||||
<ListItem
|
||||
key="add"
|
||||
title={t('settings.mcp.addServer')}
|
||||
active={false}
|
||||
onClick={onAddMcpServer}
|
||||
icon={<PlusOutlined />}
|
||||
titleStyle={{ fontWeight: 500 }}
|
||||
style={{ marginBottom: 5 }}
|
||||
/>
|
||||
</SettingGroup>
|
||||
<NpxSearch />
|
||||
</SettingContainer>
|
||||
<DragableList list={mcpServers} onUpdate={updateMcpServers}>
|
||||
{(server: MCPServer) => (
|
||||
<Dropdown menu={{ items: getMenuItems(server) }} trigger={['contextMenu']} key={server.id}>
|
||||
<div>
|
||||
<ListItem
|
||||
key={server.id}
|
||||
title={server.name}
|
||||
active={selectedMcpServer?.id === server.id}
|
||||
onClick={() => {
|
||||
setSelectedMcpServer(server)
|
||||
setRoute(null)
|
||||
}}
|
||||
titleStyle={{ fontWeight: 500 }}
|
||||
icon={<CodeOutlined />}
|
||||
rightContent={
|
||||
<IndicatorLight
|
||||
size={6}
|
||||
color={server.isActive ? 'green' : 'var(--color-text-3)'}
|
||||
animation={server.isActive}
|
||||
shadow={false}
|
||||
style={{ marginRight: 4 }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Dropdown>
|
||||
)}
|
||||
</DragableList>
|
||||
</McpList>
|
||||
<MainContent />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled(HStack)`
|
||||
flex: 1;
|
||||
`
|
||||
|
||||
const McpList = styled(Scrollbar)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
width: var(--settings-width);
|
||||
padding: 12px;
|
||||
border-right: 0.5px solid var(--color-border);
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
.iconfont {
|
||||
color: var(--color-text-2);
|
||||
line-height: 16px;
|
||||
}
|
||||
`
|
||||
|
||||
export default MCPSettings
|
||||
|
||||
@@ -22,6 +22,7 @@ import DataSettings from './DataSettings/DataSettings'
|
||||
import DisplaySettings from './DisplaySettings/DisplaySettings'
|
||||
import GeneralSettings from './GeneralSettings'
|
||||
import MCPSettings from './MCPSettings'
|
||||
import { McpSettingsNavbar } from './MCPSettings/McpSettingsNavbar'
|
||||
import ProvidersList from './ProviderSettings'
|
||||
import QuickAssistantSettings from './QuickAssistantSettings'
|
||||
import ShortcutSettings from './ShortcutSettings'
|
||||
@@ -37,6 +38,7 @@ const SettingsPage: FC = () => {
|
||||
<Container>
|
||||
<Navbar>
|
||||
<NavbarCenter style={{ borderRight: 'none' }}>{t('settings.title')}</NavbarCenter>
|
||||
{pathname === '/settings/mcp' && <McpSettingsNavbar />}
|
||||
</Navbar>
|
||||
<ContentContainer id="content-container">
|
||||
<SettingMenus>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ThemeMode } from '@renderer/types'
|
||||
import { Divider } from 'antd'
|
||||
import Link from 'antd/es/typography/Link'
|
||||
import styled from 'styled-components'
|
||||
import styled, { CSSProp } from 'styled-components'
|
||||
|
||||
export const SettingContainer = styled.div<{ theme?: ThemeMode }>`
|
||||
display: flex;
|
||||
@@ -80,7 +80,7 @@ export const SettingHelpLink = styled(Link)`
|
||||
margin: 0 5px;
|
||||
`
|
||||
|
||||
export const SettingGroup = styled.div<{ theme?: ThemeMode }>`
|
||||
export const SettingGroup = styled.div<{ theme?: ThemeMode; css?: CSSProp }>`
|
||||
margin-bottom: 20px;
|
||||
border-radius: 8px;
|
||||
border: 0.5px solid var(--color-border);
|
||||
|
||||
@@ -514,6 +514,7 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
|
||||
for (const toolCall of toolCalls) {
|
||||
const mcpTool = openAIToolsToMcpTool(mcpTools, toolCall)
|
||||
|
||||
if (!mcpTool) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -99,12 +99,15 @@ export async function fetchChatCompletion({
|
||||
|
||||
const lastUserMessage = findLast(messages, (m) => m.role === 'user')
|
||||
// Get MCP tools
|
||||
let mcpTools: MCPTool[] = []
|
||||
const mcpTools: MCPTool[] = []
|
||||
const enabledMCPs = lastUserMessage?.enabledMCPs
|
||||
|
||||
if (enabledMCPs && enabledMCPs.length > 0) {
|
||||
const allMCPTools = await window.api.mcp.listTools()
|
||||
mcpTools = allMCPTools.filter((tool) => enabledMCPs.some((mcp) => mcp.name === tool.serverName))
|
||||
for (const mcpServer of enabledMCPs) {
|
||||
const tools = await window.api.mcp.listTools(mcpServer)
|
||||
console.debug('tools', tools)
|
||||
mcpTools.push(...tools)
|
||||
}
|
||||
}
|
||||
|
||||
await AI.completions({
|
||||
@@ -127,6 +130,7 @@ export async function fetchChatCompletion({
|
||||
if (mcpToolResponse) {
|
||||
message.metadata = { ...message.metadata, mcpTools: cloneDeep(mcpToolResponse) }
|
||||
}
|
||||
|
||||
if (generateImage && generateImage.images.length > 0) {
|
||||
const existingImages = message.metadata?.generateImage?.images || []
|
||||
generateImage.images = [...existingImages, ...generateImage.images]
|
||||
|
||||
@@ -54,7 +54,7 @@ export function filterEmptyMessages(messages: Message[]): Message[] {
|
||||
}
|
||||
|
||||
export function filterUsefulMessages(messages: Message[]): Message[] {
|
||||
const _messages = [...messages]
|
||||
let _messages = [...messages]
|
||||
const groupedMessages = getGroupedMessages(messages)
|
||||
|
||||
Object.entries(groupedMessages).forEach(([key, messages]) => {
|
||||
@@ -78,6 +78,18 @@ export function filterUsefulMessages(messages: Message[]): Message[] {
|
||||
_messages.pop()
|
||||
}
|
||||
|
||||
// 过滤两条及以上 user 类型消息相邻的情况,只保留最新一条 user 消息
|
||||
_messages = _messages.filter((message, index, origin) => {
|
||||
if (
|
||||
message.role === 'user'
|
||||
&& index + 1 < origin.length
|
||||
&& origin[index + 1].role === 'user'
|
||||
) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return _messages
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ import store from '@renderer/store'
|
||||
import { setNutstoreSyncState } from '@renderer/store/nutstore'
|
||||
import { WebDavConfig } from '@renderer/types'
|
||||
import { NUTSTORE_HOST } from '@shared/config/nutstore'
|
||||
import dayjs from 'dayjs'
|
||||
import Logger from 'electron-log'
|
||||
import { type CreateDirectoryOptions } from 'webdav'
|
||||
|
||||
import { getBackupData, handleData } from './BackupService'
|
||||
@@ -59,13 +61,18 @@ let syncTimeout: NodeJS.Timeout | null = null
|
||||
let isAutoBackupRunning = false
|
||||
let isManualBackupRunning = false
|
||||
|
||||
export async function backupToNutstore(options: { showMessage?: boolean } = {}) {
|
||||
export async function backupToNutstore({
|
||||
showMessage = false,
|
||||
customFileName = ''
|
||||
}: {
|
||||
showMessage?: boolean
|
||||
customFileName?: string
|
||||
} = {}) {
|
||||
const nutstoreToken = getNutstoreToken()
|
||||
if (!nutstoreToken) {
|
||||
return
|
||||
}
|
||||
|
||||
const { showMessage = false } = options
|
||||
if (isManualBackupRunning) {
|
||||
console.log('Backup already in progress')
|
||||
return
|
||||
@@ -76,6 +83,16 @@ export async function backupToNutstore(options: { showMessage?: boolean } = {})
|
||||
return
|
||||
}
|
||||
|
||||
let deviceType = 'unknown'
|
||||
try {
|
||||
deviceType = (await window.api.system.getDeviceType()) || 'unknown'
|
||||
} catch (error) {
|
||||
Logger.error('[Backup] Failed to get device type:', error)
|
||||
}
|
||||
const timestamp = dayjs().format('YYYYMMDDHHmmss')
|
||||
const backupFileName = customFileName || `cherry-studio.${timestamp}.${deviceType}.zip`
|
||||
const finalFileName = backupFileName.endsWith('.zip') ? backupFileName : `${backupFileName}.zip`
|
||||
|
||||
isManualBackupRunning = true
|
||||
|
||||
store.dispatch(setNutstoreSyncState({ syncing: true, lastSyncError: null }))
|
||||
@@ -83,7 +100,7 @@ export async function backupToNutstore(options: { showMessage?: boolean } = {})
|
||||
const backupData = await getBackupData()
|
||||
|
||||
try {
|
||||
const isSuccess = await window.api.backup.backupToWebdav(backupData, config)
|
||||
const isSuccess = await window.api.backup.backupToWebdav(backupData, { ...config, fileName: finalFileName })
|
||||
|
||||
if (isSuccess) {
|
||||
store.dispatch(
|
||||
|
||||
@@ -13,19 +13,19 @@ const mcpSlice = createSlice({
|
||||
state.servers = action.payload
|
||||
},
|
||||
addMCPServer: (state, action: PayloadAction<MCPServer>) => {
|
||||
state.servers.push(action.payload)
|
||||
state.servers.unshift(action.payload)
|
||||
},
|
||||
updateMCPServer: (state, action: PayloadAction<MCPServer>) => {
|
||||
const index = state.servers.findIndex((server) => server.name === action.payload.name)
|
||||
const index = state.servers.findIndex((server) => server.id === action.payload.id)
|
||||
if (index !== -1) {
|
||||
state.servers[index] = action.payload
|
||||
}
|
||||
},
|
||||
deleteMCPServer: (state, action: PayloadAction<string>) => {
|
||||
state.servers = state.servers.filter((server) => server.name !== action.payload)
|
||||
state.servers = state.servers.filter((server) => server.id !== action.payload)
|
||||
},
|
||||
setMCPServerActive: (state, action: PayloadAction<{ name: string; isActive: boolean }>) => {
|
||||
const index = state.servers.findIndex((server) => server.name === action.payload.name)
|
||||
setMCPServerActive: (state, action: PayloadAction<{ id: string; isActive: boolean }>) => {
|
||||
const index = state.servers.findIndex((server) => server.id === action.payload.id)
|
||||
if (index !== -1) {
|
||||
state.servers[index].isActive = action.payload.isActive
|
||||
}
|
||||
|
||||
@@ -278,6 +278,7 @@ export const sendMessage =
|
||||
const assistantMessage = getAssistantMessage({ assistant, topic })
|
||||
assistantMessage.askId = userMessage.id
|
||||
assistantMessage.status = 'sending'
|
||||
assistantMessage.foldSelected = true
|
||||
assistantMessages.push(assistantMessage)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { nanoid } from '@reduxjs/toolkit'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
||||
import { SYSTEM_MODELS } from '@renderer/config/models'
|
||||
@@ -809,6 +810,15 @@ const migrateConfig = {
|
||||
return state
|
||||
},
|
||||
'86': (state: RootState) => {
|
||||
if (state.mcp.servers) {
|
||||
state.mcp.servers = state.mcp.servers.map((server) => ({
|
||||
...server,
|
||||
id: nanoid()
|
||||
}))
|
||||
}
|
||||
return state
|
||||
},
|
||||
'87': (state: RootState) => {
|
||||
if (!state.ocr) {
|
||||
state.ocr = {
|
||||
defaultProvider: '',
|
||||
|
||||
@@ -418,6 +418,7 @@ export interface MCPServerParameter {
|
||||
}
|
||||
|
||||
export interface MCPServer {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
baseUrl?: string
|
||||
@@ -437,6 +438,7 @@ export interface MCPToolInputSchema {
|
||||
|
||||
export interface MCPTool {
|
||||
id: string
|
||||
serverId: string
|
||||
serverName: string
|
||||
name: string
|
||||
description?: string
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Tool, ToolUnion, ToolUseBlock } from '@anthropic-ai/sdk/resources'
|
||||
import { FunctionCall, FunctionDeclaration, SchemaType, Tool as geminiToool } from '@google/generative-ai'
|
||||
import store from '@renderer/store'
|
||||
import { MCPServer, MCPTool, MCPToolResponse } from '@renderer/types'
|
||||
import { ChatCompletionMessageToolCall, ChatCompletionTool } from 'openai/resources'
|
||||
|
||||
@@ -58,8 +59,9 @@ function filterPropertieAttributes(tool: MCPTool, filterNestedObj = false) {
|
||||
export function mcpToolsToOpenAITools(mcpTools: MCPTool[]): Array<ChatCompletionTool> {
|
||||
return mcpTools.map((tool) => ({
|
||||
type: 'function',
|
||||
name: tool.name,
|
||||
function: {
|
||||
name: tool.id,
|
||||
name: tool.serverId,
|
||||
description: tool.description,
|
||||
parameters: {
|
||||
type: 'object',
|
||||
@@ -73,11 +75,16 @@ export function openAIToolsToMcpTool(
|
||||
mcpTools: MCPTool[] | undefined,
|
||||
llmTool: ChatCompletionMessageToolCall
|
||||
): MCPTool | undefined {
|
||||
if (!mcpTools) return undefined
|
||||
const tool = mcpTools.find((tool) => tool.id === llmTool.function.name)
|
||||
if (!mcpTools) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const tool = mcpTools.find((mcptool) => mcptool.serverId === llmTool.function.name)
|
||||
|
||||
if (!tool) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[MCP] OpenAI Tool to MCP Tool: ${tool.serverName} ${tool.name}`,
|
||||
tool,
|
||||
@@ -94,6 +101,7 @@ export function openAIToolsToMcpTool(
|
||||
|
||||
return {
|
||||
id: tool.id,
|
||||
serverId: tool.serverId,
|
||||
serverName: tool.serverName,
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
@@ -104,11 +112,18 @@ export function openAIToolsToMcpTool(
|
||||
export async function callMCPTool(tool: MCPTool): Promise<any> {
|
||||
console.log(`[MCP] Calling Tool: ${tool.serverName} ${tool.name}`, tool)
|
||||
try {
|
||||
const server = getMcpServerByTool(tool)
|
||||
|
||||
if (!server) {
|
||||
throw new Error(`Server not found: ${tool.serverName}`)
|
||||
}
|
||||
|
||||
const resp = await window.api.mcp.callTool({
|
||||
client: tool.serverName,
|
||||
server,
|
||||
name: tool.name,
|
||||
args: tool.inputSchema
|
||||
})
|
||||
|
||||
console.log(`[MCP] Tool called: ${tool.serverName} ${tool.name}`, resp)
|
||||
return resp
|
||||
} catch (e) {
|
||||
@@ -227,3 +242,8 @@ export function filterMCPTools(
|
||||
}
|
||||
return mcpTools
|
||||
}
|
||||
|
||||
export function getMcpServerByTool(tool: MCPTool) {
|
||||
const servers = store.getState().mcp.servers
|
||||
return servers.find((s) => s.id === tool.serverId)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BulbOutlined, EnterOutlined, FileTextOutlined, MessageOutlined, TranslationOutlined } from '@ant-design/icons'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { Col } from 'antd'
|
||||
import { Dispatch, forwardRef, SetStateAction, useImperativeHandle, useMemo, useState } from 'react'
|
||||
import { Dispatch, SetStateAction, useImperativeHandle, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@@ -18,7 +18,12 @@ export interface FeatureMenusRef {
|
||||
resetSelectedIndex: () => void
|
||||
}
|
||||
|
||||
const FeatureMenus = forwardRef<FeatureMenusRef, FeatureMenusProps>(({ text, setRoute, onSendMessage }, ref) => {
|
||||
const FeatureMenus = ({
|
||||
ref,
|
||||
text,
|
||||
setRoute,
|
||||
onSendMessage
|
||||
}: FeatureMenusProps & { ref?: React.RefObject<FeatureMenusRef | null> }) => {
|
||||
const { t } = useTranslation()
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
|
||||
@@ -94,7 +99,7 @@ const FeatureMenus = forwardRef<FeatureMenusRef, FeatureMenusProps>(({ text, set
|
||||
</FeatureListWrapper>
|
||||
</FeatureList>
|
||||
)
|
||||
})
|
||||
}
|
||||
FeatureMenus.displayName = 'FeatureMenus'
|
||||
|
||||
const FeatureList = styled(Scrollbar)`
|
||||
|
||||
@@ -2,7 +2,7 @@ import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { Input as AntdInput } from 'antd'
|
||||
import { InputRef } from 'rc-input/lib/interface'
|
||||
import React, { forwardRef, useRef } from 'react'
|
||||
import React, { useRef } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface InputBarProps {
|
||||
@@ -14,30 +14,35 @@ interface InputBarProps {
|
||||
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
const InputBar = forwardRef<HTMLDivElement, InputBarProps>(
|
||||
({ text, model, placeholder, handleKeyDown, handleChange }, ref) => {
|
||||
const { generating } = useRuntime()
|
||||
const inputRef = useRef<InputRef>(null)
|
||||
if (!generating) {
|
||||
setTimeout(() => inputRef.current?.input?.focus(), 0)
|
||||
}
|
||||
return (
|
||||
<InputWrapper ref={ref}>
|
||||
<ModelAvatar model={model} size={30} />
|
||||
<Input
|
||||
value={text}
|
||||
placeholder={placeholder}
|
||||
variant="borderless"
|
||||
autoFocus
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={handleChange}
|
||||
disabled={generating}
|
||||
ref={inputRef}
|
||||
/>
|
||||
</InputWrapper>
|
||||
)
|
||||
const InputBar = ({
|
||||
ref,
|
||||
text,
|
||||
model,
|
||||
placeholder,
|
||||
handleKeyDown,
|
||||
handleChange
|
||||
}: InputBarProps & { ref?: React.RefObject<HTMLDivElement | null> }) => {
|
||||
const { generating } = useRuntime()
|
||||
const inputRef = useRef<InputRef>(null)
|
||||
if (!generating) {
|
||||
setTimeout(() => inputRef.current?.input?.focus(), 0)
|
||||
}
|
||||
)
|
||||
return (
|
||||
<InputWrapper ref={ref}>
|
||||
<ModelAvatar model={model} size={30} />
|
||||
<Input
|
||||
value={text}
|
||||
placeholder={placeholder}
|
||||
variant="borderless"
|
||||
autoFocus
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={handleChange}
|
||||
disabled={generating}
|
||||
ref={inputRef}
|
||||
/>
|
||||
</InputWrapper>
|
||||
)
|
||||
}
|
||||
InputBar.displayName = 'InputBar'
|
||||
|
||||
const InputWrapper = styled.div`
|
||||
|
||||
@@ -2391,12 +2391,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@modelcontextprotocol/sdk@npm:1.6.1":
|
||||
version: 1.6.1
|
||||
resolution: "@modelcontextprotocol/sdk@npm:1.6.1"
|
||||
"@modelcontextprotocol/sdk@npm:^1.8.0":
|
||||
version: 1.8.0
|
||||
resolution: "@modelcontextprotocol/sdk@npm:1.8.0"
|
||||
dependencies:
|
||||
content-type: "npm:^1.0.5"
|
||||
cors: "npm:^2.8.5"
|
||||
cross-spawn: "npm:^7.0.3"
|
||||
eventsource: "npm:^3.0.2"
|
||||
express: "npm:^5.0.1"
|
||||
express-rate-limit: "npm:^7.5.0"
|
||||
@@ -2404,24 +2405,7 @@ __metadata:
|
||||
raw-body: "npm:^3.0.0"
|
||||
zod: "npm:^3.23.8"
|
||||
zod-to-json-schema: "npm:^3.24.1"
|
||||
checksum: 10c0/767aca8096c06aabfa9432fab6a4e7bafb671833b1bddb2797b8089e102a9d6ac0486e7a353b28df9984eff5c5291bde76cd5ad079b576ae70666cdff10c5b2a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@modelcontextprotocol/sdk@patch:@modelcontextprotocol/sdk@npm%3A1.6.1#~/.yarn/patches/@modelcontextprotocol-sdk-npm-1.6.1-b46313efe7.patch":
|
||||
version: 1.6.1
|
||||
resolution: "@modelcontextprotocol/sdk@patch:@modelcontextprotocol/sdk@npm%3A1.6.1#~/.yarn/patches/@modelcontextprotocol-sdk-npm-1.6.1-b46313efe7.patch::version=1.6.1&hash=9be799"
|
||||
dependencies:
|
||||
content-type: "npm:^1.0.5"
|
||||
cors: "npm:^2.8.5"
|
||||
eventsource: "npm:^3.0.2"
|
||||
express: "npm:^5.0.1"
|
||||
express-rate-limit: "npm:^7.5.0"
|
||||
pkce-challenge: "npm:^4.1.0"
|
||||
raw-body: "npm:^3.0.0"
|
||||
zod: "npm:^3.23.8"
|
||||
zod-to-json-schema: "npm:^3.24.1"
|
||||
checksum: 10c0/4121a7d958bce44499feeb8dbe1405e3e778060d93666257cdda3f22ece1837b6e6b8ee81082c13213156b853e501ce02c199b1f6a074530f976e4bd3646ef12
|
||||
checksum: 10c0/aa453697a9be5e431bc473508654cc77887b35125366c9ec81815d9302872baf708332694c1d5a7ff7d06ac4c22d8446667c24caba78c505f643990b17d95820
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -3811,7 +3795,7 @@ __metadata:
|
||||
"@kangfenmao/keyv-storage": "npm:^0.1.0"
|
||||
"@langchain/community": "npm:^0.3.36"
|
||||
"@mistralai/mistralai": "npm:^1.5.2"
|
||||
"@modelcontextprotocol/sdk": "patch:@modelcontextprotocol/sdk@npm%3A1.6.1#~/.yarn/patches/@modelcontextprotocol-sdk-npm-1.6.1-b46313efe7.patch"
|
||||
"@modelcontextprotocol/sdk": "npm:^1.8.0"
|
||||
"@notionhq/client": "npm:^2.2.15"
|
||||
"@reduxjs/toolkit": "npm:^2.2.5"
|
||||
"@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch"
|
||||
@@ -12479,13 +12463,20 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pkce-challenge@npm:^4.1.0":
|
||||
"pkce-challenge@npm:4.1.0":
|
||||
version: 4.1.0
|
||||
resolution: "pkce-challenge@npm:4.1.0"
|
||||
checksum: 10c0/7cdc45977eb9af6f561a6f48ffcf19bd3e6f0c651727d00feef1c501384b1ed3c32d92ee67636f02011168959aedf099003a7c0bed668e7943444b20558c54e4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pkce-challenge@patch:pkce-challenge@npm%3A4.1.0#~/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch":
|
||||
version: 4.1.0
|
||||
resolution: "pkce-challenge@patch:pkce-challenge@npm%3A4.1.0#~/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch::version=4.1.0&hash=3298c3"
|
||||
checksum: 10c0/8d5a2ad2d6e826011a95e89081d8b2acc40a9e104dc7c7423b22d81520412c013a72157b7f6259650adf5bf796b97062476b7f4c90a7f6baa606ed124f57c0bc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pkg-up@npm:^3.1.0":
|
||||
version: 3.1.0
|
||||
resolution: "pkg-up@npm:3.1.0"
|
||||
|
||||
Reference in New Issue
Block a user