From 8adb26fdac6785690016b34ca75d5d340969ddc7 Mon Sep 17 00:00:00 2001 From: hllshiro <40970081+hllshiro@users.noreply.github.com> Date: Tue, 15 Jul 2025 15:24:49 +0800 Subject: [PATCH 1/6] (WIP) feat: selectively enable mcp toolsets --- .../presenter/mcpPresenter/toolManager.ts | 24 +++- src/main/presenter/threadPresenter/index.ts | 4 +- src/shared/presenter.d.ts | 1 + thread-mcp-tool-use-improve.md | 121 ++++++++++++++++++ 4 files changed, 147 insertions(+), 3 deletions(-) create mode 100644 thread-mcp-tool-use-improve.md diff --git a/src/main/presenter/mcpPresenter/toolManager.ts b/src/main/presenter/mcpPresenter/toolManager.ts index 35abd4879..e85f03fac 100644 --- a/src/main/presenter/mcpPresenter/toolManager.ts +++ b/src/main/presenter/mcpPresenter/toolManager.ts @@ -45,8 +45,19 @@ export class ToolManager { } // 获取所有工具定义 - public async getAllToolDefinitions(): Promise { + public async getAllToolDefinitions( + enabledTools?: string[] + ): Promise { if (this.cachedToolDefinitions !== null && this.cachedToolDefinitions.length > 0) { + if (enabledTools && enabledTools.length > 0) { + const enabledSet = new Set(enabledTools) + return this.cachedToolDefinitions.filter((toolDef) => { + const finalName = toolDef.function.name + const originalName = + this.toolNameToTargetMap?.get(finalName)?.originalName || finalName + return enabledSet.has(finalName) || enabledSet.has(originalName) + }) + } return this.cachedToolDefinitions } @@ -200,6 +211,17 @@ export class ToolManager { // 缓存结果并返回 this.cachedToolDefinitions = results console.info(`Cached ${results.length} final tool definitions and populated target map.`) + + if (enabledTools && enabledTools.length > 0) { + const enabledSet = new Set(enabledTools) + return this.cachedToolDefinitions.filter((toolDef) => { + const finalName = toolDef.function.name + const originalName = + this.toolNameToTargetMap?.get(finalName)?.originalName || finalName + return enabledSet.has(finalName) || enabledSet.has(originalName) + }) + } + return this.cachedToolDefinitions } diff --git a/src/main/presenter/threadPresenter/index.ts b/src/main/presenter/threadPresenter/index.ts index 15b2881e7..be400cb48 100644 --- a/src/main/presenter/threadPresenter/index.ts +++ b/src/main/presenter/threadPresenter/index.ts @@ -1789,7 +1789,7 @@ export class ThreadPresenter implements IThreadPresenter { finalContent: ChatMessage[] promptTokens: number }> { - const { systemPrompt, contextLength, artifacts } = conversation.settings + const { systemPrompt, contextLength, artifacts, enabledMcpTools } = conversation.settings const searchPrompt = searchResults ? generateSearchPrompt(userContent, searchResults) : '' const enrichedUserMessage = @@ -1801,7 +1801,7 @@ export class ThreadPresenter implements IThreadPresenter { const searchPromptTokens = searchPrompt ? approximateTokenSize(searchPrompt ?? '') : 0 const systemPromptTokens = systemPrompt ? approximateTokenSize(systemPrompt ?? '') : 0 const userMessageTokens = approximateTokenSize(userContent + enrichedUserMessage) - const mcpTools = await presenter.mcpPresenter.getAllToolDefinitions() + const mcpTools = await presenter.mcpPresenter.getAllToolDefinitions(enabledMcpTools) const mcpToolsTokens = mcpTools.reduce( (acc, tool) => acc + approximateTokenSize(JSON.stringify(tool)), 0 diff --git a/src/shared/presenter.d.ts b/src/shared/presenter.d.ts index d14d8c54a..7ab3a0b31 100644 --- a/src/shared/presenter.d.ts +++ b/src/shared/presenter.d.ts @@ -534,6 +534,7 @@ export type CONVERSATION_SETTINGS = { providerId: string modelId: string artifacts: 0 | 1 + enabledMcpTools?: string[] } export type CONVERSATION = { diff --git a/thread-mcp-tool-use-improve.md b/thread-mcp-tool-use-improve.md new file mode 100644 index 000000000..a1734bd2e --- /dev/null +++ b/thread-mcp-tool-use-improve.md @@ -0,0 +1,121 @@ +# MCP 工具在 Thread 中启用机制的分析与改进方案 (v2) + +## 1. 当前机制与 UI 分析 + +### 1.1. 后端机制 + +通过对 `ServerManager`、`ToolManager` 和 `ThreadPresenter` 的代码分析,当前 MCP (Multi-Codepath) 工具在会话(Thread)中的启用机制是**全局性**的: + +1. **服务启动**: `ServerManager` 根据全局配置启动默认的 MCP 服务器。 +2. **工具发现**: `ToolManager` 的 `getAllToolDefinitions` 方法会扫描所有**正在运行**的 MCP 服务器,收集它们提供的**所有**工具。 +3. **线程集成**: `ThreadPresenter` 在每次生成 AI 响应前,都会调用 `getAllToolDefinitions` 来获取**全部**可用工具,并将它们提供给大语言模型。 + +结论是,一个工具是否可用,仅取决于其所属的 MCP 服务器当前是否正在运行,缺乏会话级别的细粒度控制。 + +### 1.2. 前端 UI 分析 (`mcpToolsList.vue`) + +`mcpToolsList.vue` 是 MCP 的主控制面板,其核心功能点如下: + +- **全局总开关**: 一个顶层开关 (`mcpEnabled`) 用于启用或禁用整个 MCP 功能。 +- **服务器独立启停**: 在总开关打开后,UI 会列出所有已配置的 MCP 服务器,并为每个服务器提供一个独立的开关。用户可以在此**手动启动或停止**任何一个服务器。 +- **工具信息展示**: 对于正在运行的服务器,UI 会显示其提供的工具数量,并允许用户点击查看具体的工具列表。 + +这个 UI 已经为用户建立了“服务”和“工具”是两个不同层级的心理模型,非常适合在其基础上进行扩展。 + +--- + +## 2. 改进方案:分层工具选择模型 + +结合现有的后端机制和前端 UI,我们提出一个分层的工具选择模型,以实现会话级别的工具启用控制,同时保持用户体验的一致性。 + +### 2.1. 核心思路 + +将工具的管理分为两个清晰的层次: + +1. **全局服务管理层 (现状)**: 用户在 `mcpToolsList.vue` 中启动或停止 MCP 服务器。这决定了整个应用中“可能可用”的工具池。 +2. **会话工具配置层 (新增)**: 在每个会话各自的设置中,用户可以从“全局可用工具池”里,为当前会话挑选一个或多个工具子集来启用。 + +### 2.2. 具体实施步骤 + +#### **步骤 1: 扩展会话设置 (`CONVERSATION_SETTINGS`)** + +在共享类型定义文件中,为 `CONVERSATION_SETTINGS` 接口添加一个新字段,用于存储会话级别的工具选择。 + +- **文件**: `src/shared/presenter.d.ts` +- **修改**: + ```typescript + export interface CONVERSATION_SETTINGS { + // ... existing fields + enabledMcpTools?: string[]; // 新增字段:存储此会话启用的工具名称 + } + ``` + 该字段为可选。如果 `enabledMcpTools` 未定义或为空,系统将默认使用所有“全局可用”的工具,以实现向后兼容。 + +#### **步骤 2: 修改 `ToolManager` 以支持过滤 (与原方案一致)** + +修改 `getAllToolDefinitions` 方法,使其能够根据传入的启用列表过滤工具。 + +- **文件**: `src/main/presenter/mcpPresenter/toolManager.ts` +- **修改**: 为 `getAllToolDefinitions` 方法增加一个可选参数 `enabledTools?: string[]`。 + - 如果 `enabledTools` 数组被提供且不为空,则在聚合所有工具后,只返回那些名称存在于 `enabledTools` 列表中的工具。 + - **匹配逻辑**: 过滤时应同时检查工具的最终名称(可能包含服务器前缀)和原始名称,以确保匹配的健壮性。 + - 如果 `enabledTools` 未提供,则返回所有工具。 + +#### **步骤 3: 修改 `ThreadPresenter` 以传递会话配置 (与原方案一致)** + +更新 `ThreadPresenter`,使其在准备 Prompt 时,从当前会话的设置中读取 `enabledMcpTools` 列表并传递给 `ToolManager`。 + +- **文件**: `src/main/presenter/threadPresenter/index.ts` +- **修改**: 在 `preparePromptContent` 方法中: + ```typescript + // in preparePromptContent method of ThreadPresenter + private async preparePromptContent(...) { + // ... + const enabledMcpTools = conversation.settings.enabledMcpTools; + const mcpTools = await presenter.mcpPresenter.getAllToolDefinitions(enabledMcpTools); + // ... + } + ``` + +#### **步骤 4: 新增“会话工具配置”前端 UI** + +这是新方案的核心 UI 部分,需要**在会话设置界面中新增一个配置区域**,而不是修改 `mcpToolsList.vue`。 + +- **位置**: 在每个会话的设置面板中(例如,一个模态框或侧边栏)。 +- **功能**: + 1. **数据源**: 该 UI 从 `useMcpStore` 获取所有“全局可用”的工具列表(即所有已启动服务器提供的工具)。 + 2. **展示**: 使用多选框列表来展示这些工具,最好按服务器进行分组,以保持与 `mcpToolsList.vue` 的视觉一致性。 + 3. **状态同步**: + - UI 加载时,读取当前会话已保存的 `enabledMcpTools` 列表,并设置复选框的初始选中状态。 + - 当用户修改复选框时,立即或通过“保存”按钮更新会话设置,调用 `ThreadPresenter.updateConversationSettings` 将新的 `enabledMcpTools` 数组持久化到数据库。 + +- **UI Mockup 示例 (在会话设置面板中)**: + ``` + ----------------------------------------- + | 会话设置 | + |---------------------------------------| + | ... (模型、温度等其他设置) ... | + |---------------------------------------| + | ✓ 启用会话工具 | + | 从此会话可用的工具中进行选择。 | + | | + | ▼ 🌐 a-server (2/2) | + | [x] a-server_tool_1 | + | [x] a-server_tool_2 | + | | + | ▼ 💻 another-server (1/2) | + | [x] another-server_read_file | + | [ ] another-server_write_file | + | | + | ► ☁️ cloud-service (0/1) - 已停止 | + | | + ----------------------------------------- + ``` + - 可以在 UI 中提示哪些服务器当前未运行,其下的工具为不可选状态,引导用户去 `mcpToolsList.vue` 启动服务。 + +### 2.3. 新方案的优势 + +- **清晰的职责分离**: `mcpToolsList.vue` 负责**服务启停**(管理“工具箱”),会话设置负责**工具选用**(从“工具箱”里拿工具到“工作台”),逻辑清晰。 +- **一致的用户体验**: 沿用了用户已经熟悉的 UI 模式(服务器列表和开关),降低了学习成本。 +- **强大的灵活性**: 用户可以实现“启动所有服务,但在会话 A 中只用文件工具,在会话 B 中只用搜索工具”这样的精细化控制。 +- **完全向后兼容**: 对于旧会話或未做任何配置的新会话,系统默认使用所有可用工具,不影响现有功能。 \ No newline at end of file From edf84da0e00f811a8f5aa2fbf2a2912b2af22571 Mon Sep 17 00:00:00 2001 From: sqsyli Date: Fri, 18 Jul 2025 11:21:41 +0800 Subject: [PATCH 2/6] feat: add enabledMcpTools field to conversation for controlling MCP tools --- .../sqlitePresenter/tables/conversations.ts | 44 ++++++++++++++----- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/src/main/presenter/sqlitePresenter/tables/conversations.ts b/src/main/presenter/sqlitePresenter/tables/conversations.ts index b037326be..e1ea7a337 100644 --- a/src/main/presenter/sqlitePresenter/tables/conversations.ts +++ b/src/main/presenter/sqlitePresenter/tables/conversations.ts @@ -17,6 +17,16 @@ type ConversationRow = { artifacts: number is_new: number is_pinned: number + enabled_mcp_tools: string | null +} + +// 解析 JSON 字段 +function getJsonField(val: string | null | undefined, fallback: T): T { + try { + return val ? JSON.parse(val) : fallback + } catch { + return fallback + } } export class ConversationsTable extends BaseTable { @@ -46,7 +56,6 @@ export class ConversationsTable extends BaseTable { CREATE INDEX idx_conversations_pinned ON conversations(is_pinned); ` } - getMigrationSQL(version: number): string | null { if (version === 1) { return ` @@ -67,11 +76,17 @@ export class ConversationsTable extends BaseTable { UPDATE conversations SET artifacts = 0; ` } + if (version === 3) { + return ` + ALTER TABLE conversations ADD COLUMN enabled_mcp_tools TEXT DEFAULT '[]'; + ` + } + return null } getLatestVersion(): number { - return 2 + return 3 } async create(title: string, settings: Partial = {}): Promise { @@ -89,9 +104,10 @@ export class ConversationsTable extends BaseTable { model_id, is_new, artifacts, - is_pinned + is_pinned, + enabled_mcp_tools ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ,?) `) const conv_id = nanoid() const now = Date.now() @@ -108,7 +124,8 @@ export class ConversationsTable extends BaseTable { settings.modelId || 'gpt-4', 1, settings.artifacts || 0, - 0 // Default is_pinned to 0 + 0, // Default is_pinned to 0 + settings.enabledMcpTools ? JSON.stringify(settings.enabledMcpTools) : '[]' ) return conv_id } @@ -130,7 +147,8 @@ export class ConversationsTable extends BaseTable { model_id as modelId, is_new, artifacts, - is_pinned + is_pinned, + enabled_mcp_tools FROM conversations WHERE conv_id = ? ` @@ -155,7 +173,8 @@ export class ConversationsTable extends BaseTable { maxTokens: result.maxTokens, providerId: result.providerId, modelId: result.modelId, - artifacts: result.artifacts as 0 | 1 + artifacts: result.artifacts as 0 | 1, + enabledMcpTools: getJsonField(result.enabled_mcp_tools, []) } } } @@ -208,8 +227,11 @@ export class ConversationsTable extends BaseTable { updates.push('artifacts = ?') params.push(data.settings.artifacts) } + if (data.settings.enabledMcpTools !== undefined) { + updates.push('enabled_mcp_tools = ?') + params.push(JSON.stringify(data.settings.enabledMcpTools)) + } } - if (updates.length > 0 || data.updatedAt) { updates.push('updated_at = ?') params.push(data.updatedAt || Date.now()) @@ -252,7 +274,8 @@ export class ConversationsTable extends BaseTable { model_id as modelId, is_new, artifacts, - is_pinned + is_pinned, + enabled_mcp_tools FROM conversations ORDER BY updated_at DESC LIMIT ? OFFSET ? @@ -276,7 +299,8 @@ export class ConversationsTable extends BaseTable { maxTokens: row.maxTokens, providerId: row.providerId, modelId: row.modelId, - artifacts: row.artifacts as 0 | 1 + artifacts: row.artifacts as 0 | 1, + enabledMcpTools: getJsonField(row.enabled_mcp_tools, []) } })) } From 0f0550f259773cb51efc04a3c60d7619cbefd744 Mon Sep 17 00:00:00 2001 From: sqsyli Date: Fri, 18 Jul 2025 11:32:27 +0800 Subject: [PATCH 3/6] feat: filter MCP tools by enabledMcpTools --- .../presenter/llmProviderPresenter/index.ts | 11 ++++++----- src/main/presenter/mcpPresenter/index.ts | 5 ++--- src/main/presenter/mcpPresenter/toolManager.ts | 13 ++++--------- src/main/presenter/threadPresenter/index.ts | 18 ++++++++++-------- src/renderer/src/stores/chat.ts | 3 ++- src/shared/presenter.d.ts | 3 ++- 6 files changed, 26 insertions(+), 27 deletions(-) diff --git a/src/main/presenter/llmProviderPresenter/index.ts b/src/main/presenter/llmProviderPresenter/index.ts index a427a0e27..e2725000d 100644 --- a/src/main/presenter/llmProviderPresenter/index.ts +++ b/src/main/presenter/llmProviderPresenter/index.ts @@ -297,7 +297,8 @@ export class LLMProviderPresenter implements ILlmProviderPresenter { modelId: string, eventId: string, temperature: number = 0.6, - maxTokens: number = 4096 + maxTokens: number = 4096, + enabledMcpTools?: string[] ): AsyncGenerator { console.log(`[Agent Loop] Starting agent loop for event: ${eventId} with model: ${modelId}`) if (!this.canStartNewStream()) { @@ -371,7 +372,7 @@ export class LLMProviderPresenter implements ILlmProviderPresenter { try { console.log(`[Agent Loop] Iteration ${toolCallCount + 1} for event: ${eventId}`) - const mcpTools = await presenter.mcpPresenter.getAllToolDefinitions() + const mcpTools = await presenter.mcpPresenter.getAllToolDefinitions(enabledMcpTools) // Call the provider's core stream method, expecting LLMCoreStreamEvent const stream = provider.coreStream( conversationMessages, @@ -591,9 +592,9 @@ export class LLMProviderPresenter implements ILlmProviderPresenter { toolCallCount++ // Find the tool definition to get server info - const toolDef = (await presenter.mcpPresenter.getAllToolDefinitions()).find( - (t) => t.function.name === toolCall.name - ) + const toolDef = ( + await presenter.mcpPresenter.getAllToolDefinitions(enabledMcpTools) + ).find((t) => t.function.name === toolCall.name) if (!toolDef) { console.error(`Tool definition not found for ${toolCall.name}. Skipping execution.`) diff --git a/src/main/presenter/mcpPresenter/index.ts b/src/main/presenter/mcpPresenter/index.ts index e23fedd0a..704bcecbc 100644 --- a/src/main/presenter/mcpPresenter/index.ts +++ b/src/main/presenter/mcpPresenter/index.ts @@ -410,11 +410,10 @@ export class McpPresenter implements IMCPPresenter { // 通知渲染进程服务器已停止 eventBus.send(MCP_EVENTS.SERVER_STOPPED, SendTarget.ALL_WINDOWS, serverName) } - - async getAllToolDefinitions(): Promise { + async getAllToolDefinitions(enabledMcpTools?: string[]): Promise { const enabled = await this.configPresenter.getMcpEnabled() if (enabled) { - return this.toolManager.getAllToolDefinitions() + return await this.toolManager.getAllToolDefinitions(enabledMcpTools) } return [] } diff --git a/src/main/presenter/mcpPresenter/toolManager.ts b/src/main/presenter/mcpPresenter/toolManager.ts index e85f03fac..1f984e075 100644 --- a/src/main/presenter/mcpPresenter/toolManager.ts +++ b/src/main/presenter/mcpPresenter/toolManager.ts @@ -43,18 +43,14 @@ export class ToolManager { public async getRunningClients(): Promise { return this.serverManager.getRunningClients() } - // 获取所有工具定义 - public async getAllToolDefinitions( - enabledTools?: string[] - ): Promise { + public async getAllToolDefinitions(enabledTools?: string[]): Promise { if (this.cachedToolDefinitions !== null && this.cachedToolDefinitions.length > 0) { - if (enabledTools && enabledTools.length > 0) { + if (enabledTools) { const enabledSet = new Set(enabledTools) return this.cachedToolDefinitions.filter((toolDef) => { const finalName = toolDef.function.name - const originalName = - this.toolNameToTargetMap?.get(finalName)?.originalName || finalName + const originalName = this.toolNameToTargetMap?.get(finalName)?.originalName || finalName return enabledSet.has(finalName) || enabledSet.has(originalName) }) } @@ -216,8 +212,7 @@ export class ToolManager { const enabledSet = new Set(enabledTools) return this.cachedToolDefinitions.filter((toolDef) => { const finalName = toolDef.function.name - const originalName = - this.toolNameToTargetMap?.get(finalName)?.originalName || finalName + const originalName = this.toolNameToTargetMap?.get(finalName)?.originalName || finalName return enabledSet.has(finalName) || enabledSet.has(originalName) }) } diff --git a/src/main/presenter/threadPresenter/index.ts b/src/main/presenter/threadPresenter/index.ts index be400cb48..889559b84 100644 --- a/src/main/presenter/threadPresenter/index.ts +++ b/src/main/presenter/threadPresenter/index.ts @@ -706,7 +706,6 @@ export class ThreadPresenter implements IThreadPresenter { return conversation } - async createConversation( title: string, settings: Partial = {}, @@ -1467,16 +1466,17 @@ export class ThreadPresenter implements IThreadPresenter { providerId: currentProviderId, modelId: currentModelId, temperature: currentTemperature, - maxTokens: currentMaxTokens + maxTokens: currentMaxTokens, + enabledMcpTools: crrentEnabledMcpTools } = currentConversation.settings - const stream = this.llmProviderPresenter.startStreamCompletion( currentProviderId, // 使用最新的设置 finalContent, currentModelId, // 使用最新的设置 state.message.id, currentTemperature, // 使用最新的设置 - currentMaxTokens // 使用最新的设置 + currentMaxTokens, // 使用最新的设置 + crrentEnabledMcpTools ) for await (const event of stream) { const msg = event.data @@ -1574,7 +1574,7 @@ export class ThreadPresenter implements IThreadPresenter { this.throwIfCancelled(state.message.id) // 7. 准备提示内容 - const { providerId, modelId, temperature, maxTokens } = conversation.settings + const { providerId, modelId, temperature, maxTokens, enabledMcpTools } = conversation.settings const modelConfig = this.configPresenter.getModelConfig(modelId, providerId) const { finalContent, promptTokens } = await this.preparePromptContent( @@ -1641,7 +1641,8 @@ export class ThreadPresenter implements IThreadPresenter { modelId, state.message.id, temperature, - maxTokens + maxTokens, + enabledMcpTools ) for await (const event of stream) { const msg = event.data @@ -3049,7 +3050,7 @@ export class ThreadPresenter implements IThreadPresenter { throw new Error(errorMsg) } - const { providerId, modelId, temperature, maxTokens } = conversation.settings + const { providerId, modelId, temperature, maxTokens, enabledMcpTools } = conversation.settings const modelConfig = this.configPresenter.getModelConfig(modelId, providerId) if (!modelConfig) { @@ -3102,7 +3103,8 @@ export class ThreadPresenter implements IThreadPresenter { modelId, messageId, temperature, - maxTokens + maxTokens, + enabledMcpTools ) for await (const event of stream) { diff --git a/src/renderer/src/stores/chat.ts b/src/renderer/src/stores/chat.ts index e098d475c..f4ede3950 100644 --- a/src/renderer/src/stores/chat.ts +++ b/src/renderer/src/stores/chat.ts @@ -56,7 +56,8 @@ export const useChatStore = defineStore('chat', () => { maxTokens: 8000, providerId: '', modelId: '', - artifacts: 0 + artifacts: 0, + enabledMcpTools: [] }) // Deeplink 消息缓存 diff --git a/src/shared/presenter.d.ts b/src/shared/presenter.d.ts index 7ab3a0b31..784da13d8 100644 --- a/src/shared/presenter.d.ts +++ b/src/shared/presenter.d.ts @@ -502,7 +502,8 @@ export interface ILlmProviderPresenter { modelId: string, eventId: string, temperature?: number, - maxTokens?: number + maxTokens?: number, + enabledMcpTools?: string[] ): AsyncGenerator generateCompletion( providerId: string, From dbabbbcf20c72d75fcb1842442785748bf6a7b99 Mon Sep 17 00:00:00 2001 From: sqsyli Date: Fri, 18 Jul 2025 18:00:22 +0800 Subject: [PATCH 4/6] feat: refactor toggle logic for MCP service and tool state --- src/renderer/src/components/NewThread.vue | 3 +- src/renderer/src/components/mcpToolsList.vue | 47 +++++++++++++++++--- src/renderer/src/stores/mcp.ts | 25 ++++++++++- 3 files changed, 67 insertions(+), 8 deletions(-) diff --git a/src/renderer/src/components/NewThread.vue b/src/renderer/src/components/NewThread.vue index 45383f030..645680f2c 100644 --- a/src/renderer/src/components/NewThread.vue +++ b/src/renderer/src/components/NewThread.vue @@ -366,7 +366,8 @@ const handleSend = async (content: UserMessageContent) => { temperature: temperature.value, contextLength: contextLength.value, maxTokens: maxTokens.value, - artifacts: artifacts.value as 0 | 1 + artifacts: artifacts.value as 0 | 1, + enabledMcpTools: chatStore.chatConfig.enabledMcpTools }) console.log('threadId', threadId, activeModel.value) chatStore.sendMessage(content) diff --git a/src/renderer/src/components/mcpToolsList.vue b/src/renderer/src/components/mcpToolsList.vue index fe0fbe717..3a83b96b7 100644 --- a/src/renderer/src/components/mcpToolsList.vue +++ b/src/renderer/src/components/mcpToolsList.vue @@ -9,16 +9,17 @@ import { Button } from './ui/button' import { Switch } from './ui/switch' import { Badge } from './ui/badge' import { useLanguageStore } from '@/stores/language' +import { useChatStore } from '@/stores/chat' const { t } = useI18n() const mcpStore = useMcpStore() const langStore = useLanguageStore() +const chatStore = useChatStore() // 计算属性 const isLoading = computed(() => mcpStore.toolsLoading) const isError = computed(() => mcpStore.toolsError) const errorMessage = computed(() => mcpStore.toolsErrorMessage) -const toolCount = computed(() => mcpStore.toolCount) const hasTools = computed(() => mcpStore.hasTools) const mcpEnabled = computed(() => mcpStore.mcpEnabled) @@ -31,9 +32,39 @@ const getTools = (serverName: string) => { return mcpStore.tools.filter((tool) => tool.server.name === serverName) } +// 获取每个mcp服务的工具数量 +const getLength = (serverName: string) => { + const enabledTools = chatStore.chatConfig.enabledMcpTools ?? [] + const serverTools = mcpStore.tools.filter((tool) => tool.server.name === serverName) + return serverTools.filter((tool) => enabledTools.includes(tool.function.name)).length +} + +// 获取当前对话的工具总数 +const getCount = () => { + const enabledMcpTools = chatStore.chatConfig.enabledMcpTools || [] + const filterList = mcpStore.tools.filter((item) => enabledMcpTools.includes(item.function.name)) + return filterList.length +} + +// 处理单个服务开关状态变化 const onServerToggle = (serverName: string) => { mcpStore.toggleServer(serverName) } + +// 处理单个工具开关状态变化 +const handleToolEnabledChange = (isEnabled: boolean, functionName: string) => { + const currentTools = chatStore.chatConfig.enabledMcpTools || [] + const updatedTools = isEnabled + ? Array.from(new Set([...currentTools, functionName])) + : currentTools.filter((name) => name !== functionName) + chatStore.updateChatConfig({ enabledMcpTools: updatedTools }) +} + +// 获取单个工具开关状态 +const isEnabled = (functionName: string): boolean => { + return chatStore.chatConfig.enabledMcpTools?.includes(functionName) ?? false +} + // 获取内置服务器的本地化名称和描述 const getLocalizedServerName = (serverName: string) => { return t(`mcp.inmemory.${serverName}.name`, serverName) @@ -76,7 +107,7 @@ onMounted(async () => { v-if="hasTools && !isLoading && !isError" :class="{ 'text-muted-foreground': !mcpEnabled, 'text-white': mcpEnabled }" class="text-sm" - >{{ toolCount }}{{ getCount() }} @@ -84,7 +115,7 @@ onMounted(async () => {

{{ t('mcp.tools.disabled') }}

{{ t('mcp.tools.loading') }}

{{ t('mcp.tools.error') }}

-

{{ t('mcp.tools.available', { count: toolCount }) }}

+

{{ t('mcp.tools.available', { count: getCount() }) }}

{{ t('mcp.tools.none') }}

@@ -144,16 +175,22 @@ onMounted(async () => { variant="outline" class="flex items-center gap-1 mr-2 text-xs" > - {{ getTools(server.name).length }} + {{ getLength(server.name) }}
{{ tool.function.name }}
+
diff --git a/src/renderer/src/stores/mcp.ts b/src/renderer/src/stores/mcp.ts index a7a4dd484..c544e3b21 100644 --- a/src/renderer/src/stores/mcp.ts +++ b/src/renderer/src/stores/mcp.ts @@ -4,6 +4,7 @@ import { usePresenter } from '@/composables/usePresenter' import { MCP_EVENTS } from '@/events' import { useI18n } from 'vue-i18n' import { useThrottleFn } from '@vueuse/core' +import { useChatStore } from './chat' import type { McpClient, MCPConfig, @@ -29,6 +30,7 @@ interface MCPToolCallResult { } export const useMcpStore = defineStore('mcp', () => { + const chatStore = useChatStore() const { t } = useI18n() // 获取MCP相关的presenter const mcpPresenter = usePresenter('mcpPresenter') @@ -163,8 +165,22 @@ export const useMcpStore = defineStore('mcp', () => { try { serverStatuses.value[serverName] = await mcpPresenter.isServerRunning(serverName) if (config.value.mcpEnabled && !noRefresh) { - loadTools() - loadClients() + await _loadTools() + await _loadClients() + } + // 根据服务器的状态,关闭或者开启该服务器的所有工具 + const isRunning = serverStatuses.value[serverName] || false + const currentTools = chatStore.chatConfig.enabledMcpTools || [] + if (isRunning) { + const serverTools = tools.value + .filter((tool) => tool.server.name === serverName) + .map((tool) => tool.function.name) + const mergedTools = Array.from(new Set([...currentTools, ...serverTools])) + chatStore.updateChatConfig({ enabledMcpTools: mergedTools }) + } else { + const allServerToolNames = tools.value.map((tool) => tool.function.name) + const filteredTools = currentTools.filter((name) => allServerToolNames.includes(name)) + chatStore.updateChatConfig({ enabledMcpTools: filteredTools }) } } catch (error) { console.error(t('mcp.errors.getServerStatusFailed', { serverName }), error) @@ -498,6 +514,11 @@ export const useMcpStore = defineStore('mcp', () => { await loadTools() await loadClients() } + + // 如果是新建会话页面,则缓存已激活工具名称 + if (!chatStore.getActiveThreadId()) { + chatStore.chatConfig.enabledMcpTools = tools.value.map((item) => item.function.name) + } } // 立即初始化 From 9df0ee5c538a80ee134e797048defaf86f3b2b3e Mon Sep 17 00:00:00 2001 From: sqsyli Date: Fri, 25 Jul 2025 10:44:20 +0800 Subject: [PATCH 5/6] feat: change function name --- src/renderer/src/components/mcpToolsList.vue | 16 +-- thread-mcp-tool-use-improve.md | 121 ------------------- 2 files changed, 9 insertions(+), 128 deletions(-) delete mode 100644 thread-mcp-tool-use-improve.md diff --git a/src/renderer/src/components/mcpToolsList.vue b/src/renderer/src/components/mcpToolsList.vue index 3a83b96b7..9b8c02884 100644 --- a/src/renderer/src/components/mcpToolsList.vue +++ b/src/renderer/src/components/mcpToolsList.vue @@ -32,15 +32,15 @@ const getTools = (serverName: string) => { return mcpStore.tools.filter((tool) => tool.server.name === serverName) } -// 获取每个mcp服务的工具数量 -const getLength = (serverName: string) => { +// 获取每个mcp服务的可用工具数量 +const getEnabledToolCountByServer = (serverName: string) => { const enabledTools = chatStore.chatConfig.enabledMcpTools ?? [] const serverTools = mcpStore.tools.filter((tool) => tool.server.name === serverName) return serverTools.filter((tool) => enabledTools.includes(tool.function.name)).length } -// 获取当前对话的工具总数 -const getCount = () => { +// 获取可用工具总数 +const getTotalEnabledToolCount = () => { const enabledMcpTools = chatStore.chatConfig.enabledMcpTools || [] const filterList = mcpStore.tools.filter((item) => enabledMcpTools.includes(item.function.name)) return filterList.length @@ -107,7 +107,7 @@ onMounted(async () => { v-if="hasTools && !isLoading && !isError" :class="{ 'text-muted-foreground': !mcpEnabled, 'text-white': mcpEnabled }" class="text-sm" - >{{ getCount() }}{{ getTotalEnabledToolCount() }} @@ -115,7 +115,9 @@ onMounted(async () => {

{{ t('mcp.tools.disabled') }}

{{ t('mcp.tools.loading') }}

{{ t('mcp.tools.error') }}

-

{{ t('mcp.tools.available', { count: getCount() }) }}

+

+ {{ t('mcp.tools.available', { count: getTotalEnabledToolCount() }) }} +

{{ t('mcp.tools.none') }}

@@ -175,7 +177,7 @@ onMounted(async () => { variant="outline" class="flex items-center gap-1 mr-2 text-xs" > - {{ getLength(server.name) }} + {{ getEnabledToolCountByServer(server.name) }} diff --git a/thread-mcp-tool-use-improve.md b/thread-mcp-tool-use-improve.md deleted file mode 100644 index a1734bd2e..000000000 --- a/thread-mcp-tool-use-improve.md +++ /dev/null @@ -1,121 +0,0 @@ -# MCP 工具在 Thread 中启用机制的分析与改进方案 (v2) - -## 1. 当前机制与 UI 分析 - -### 1.1. 后端机制 - -通过对 `ServerManager`、`ToolManager` 和 `ThreadPresenter` 的代码分析,当前 MCP (Multi-Codepath) 工具在会话(Thread)中的启用机制是**全局性**的: - -1. **服务启动**: `ServerManager` 根据全局配置启动默认的 MCP 服务器。 -2. **工具发现**: `ToolManager` 的 `getAllToolDefinitions` 方法会扫描所有**正在运行**的 MCP 服务器,收集它们提供的**所有**工具。 -3. **线程集成**: `ThreadPresenter` 在每次生成 AI 响应前,都会调用 `getAllToolDefinitions` 来获取**全部**可用工具,并将它们提供给大语言模型。 - -结论是,一个工具是否可用,仅取决于其所属的 MCP 服务器当前是否正在运行,缺乏会话级别的细粒度控制。 - -### 1.2. 前端 UI 分析 (`mcpToolsList.vue`) - -`mcpToolsList.vue` 是 MCP 的主控制面板,其核心功能点如下: - -- **全局总开关**: 一个顶层开关 (`mcpEnabled`) 用于启用或禁用整个 MCP 功能。 -- **服务器独立启停**: 在总开关打开后,UI 会列出所有已配置的 MCP 服务器,并为每个服务器提供一个独立的开关。用户可以在此**手动启动或停止**任何一个服务器。 -- **工具信息展示**: 对于正在运行的服务器,UI 会显示其提供的工具数量,并允许用户点击查看具体的工具列表。 - -这个 UI 已经为用户建立了“服务”和“工具”是两个不同层级的心理模型,非常适合在其基础上进行扩展。 - ---- - -## 2. 改进方案:分层工具选择模型 - -结合现有的后端机制和前端 UI,我们提出一个分层的工具选择模型,以实现会话级别的工具启用控制,同时保持用户体验的一致性。 - -### 2.1. 核心思路 - -将工具的管理分为两个清晰的层次: - -1. **全局服务管理层 (现状)**: 用户在 `mcpToolsList.vue` 中启动或停止 MCP 服务器。这决定了整个应用中“可能可用”的工具池。 -2. **会话工具配置层 (新增)**: 在每个会话各自的设置中,用户可以从“全局可用工具池”里,为当前会话挑选一个或多个工具子集来启用。 - -### 2.2. 具体实施步骤 - -#### **步骤 1: 扩展会话设置 (`CONVERSATION_SETTINGS`)** - -在共享类型定义文件中,为 `CONVERSATION_SETTINGS` 接口添加一个新字段,用于存储会话级别的工具选择。 - -- **文件**: `src/shared/presenter.d.ts` -- **修改**: - ```typescript - export interface CONVERSATION_SETTINGS { - // ... existing fields - enabledMcpTools?: string[]; // 新增字段:存储此会话启用的工具名称 - } - ``` - 该字段为可选。如果 `enabledMcpTools` 未定义或为空,系统将默认使用所有“全局可用”的工具,以实现向后兼容。 - -#### **步骤 2: 修改 `ToolManager` 以支持过滤 (与原方案一致)** - -修改 `getAllToolDefinitions` 方法,使其能够根据传入的启用列表过滤工具。 - -- **文件**: `src/main/presenter/mcpPresenter/toolManager.ts` -- **修改**: 为 `getAllToolDefinitions` 方法增加一个可选参数 `enabledTools?: string[]`。 - - 如果 `enabledTools` 数组被提供且不为空,则在聚合所有工具后,只返回那些名称存在于 `enabledTools` 列表中的工具。 - - **匹配逻辑**: 过滤时应同时检查工具的最终名称(可能包含服务器前缀)和原始名称,以确保匹配的健壮性。 - - 如果 `enabledTools` 未提供,则返回所有工具。 - -#### **步骤 3: 修改 `ThreadPresenter` 以传递会话配置 (与原方案一致)** - -更新 `ThreadPresenter`,使其在准备 Prompt 时,从当前会话的设置中读取 `enabledMcpTools` 列表并传递给 `ToolManager`。 - -- **文件**: `src/main/presenter/threadPresenter/index.ts` -- **修改**: 在 `preparePromptContent` 方法中: - ```typescript - // in preparePromptContent method of ThreadPresenter - private async preparePromptContent(...) { - // ... - const enabledMcpTools = conversation.settings.enabledMcpTools; - const mcpTools = await presenter.mcpPresenter.getAllToolDefinitions(enabledMcpTools); - // ... - } - ``` - -#### **步骤 4: 新增“会话工具配置”前端 UI** - -这是新方案的核心 UI 部分,需要**在会话设置界面中新增一个配置区域**,而不是修改 `mcpToolsList.vue`。 - -- **位置**: 在每个会话的设置面板中(例如,一个模态框或侧边栏)。 -- **功能**: - 1. **数据源**: 该 UI 从 `useMcpStore` 获取所有“全局可用”的工具列表(即所有已启动服务器提供的工具)。 - 2. **展示**: 使用多选框列表来展示这些工具,最好按服务器进行分组,以保持与 `mcpToolsList.vue` 的视觉一致性。 - 3. **状态同步**: - - UI 加载时,读取当前会话已保存的 `enabledMcpTools` 列表,并设置复选框的初始选中状态。 - - 当用户修改复选框时,立即或通过“保存”按钮更新会话设置,调用 `ThreadPresenter.updateConversationSettings` 将新的 `enabledMcpTools` 数组持久化到数据库。 - -- **UI Mockup 示例 (在会话设置面板中)**: - ``` - ----------------------------------------- - | 会话设置 | - |---------------------------------------| - | ... (模型、温度等其他设置) ... | - |---------------------------------------| - | ✓ 启用会话工具 | - | 从此会话可用的工具中进行选择。 | - | | - | ▼ 🌐 a-server (2/2) | - | [x] a-server_tool_1 | - | [x] a-server_tool_2 | - | | - | ▼ 💻 another-server (1/2) | - | [x] another-server_read_file | - | [ ] another-server_write_file | - | | - | ► ☁️ cloud-service (0/1) - 已停止 | - | | - ----------------------------------------- - ``` - - 可以在 UI 中提示哪些服务器当前未运行,其下的工具为不可选状态,引导用户去 `mcpToolsList.vue` 启动服务。 - -### 2.3. 新方案的优势 - -- **清晰的职责分离**: `mcpToolsList.vue` 负责**服务启停**(管理“工具箱”),会话设置负责**工具选用**(从“工具箱”里拿工具到“工作台”),逻辑清晰。 -- **一致的用户体验**: 沿用了用户已经熟悉的 UI 模式(服务器列表和开关),降低了学习成本。 -- **强大的灵活性**: 用户可以实现“启动所有服务,但在会话 A 中只用文件工具,在会话 B 中只用搜索工具”这样的精细化控制。 -- **完全向后兼容**: 对于旧会話或未做任何配置的新会话,系统默认使用所有可用工具,不影响现有功能。 \ No newline at end of file From 34ddb747c290932877d944193a14886ceddba71a Mon Sep 17 00:00:00 2001 From: sqsyli Date: Fri, 25 Jul 2025 10:45:39 +0800 Subject: [PATCH 6/6] feat: add empty data display --- src/renderer/src/components/mcpToolsList.vue | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/renderer/src/components/mcpToolsList.vue b/src/renderer/src/components/mcpToolsList.vue index 9b8c02884..120bb02f9 100644 --- a/src/renderer/src/components/mcpToolsList.vue +++ b/src/renderer/src/components/mcpToolsList.vue @@ -194,6 +194,12 @@ onMounted(async () => { " /> +
+ {{ t('mcp.tools.empty') }} +