diff --git a/src/main/presenter/builtInToolsPresenter/index.ts b/src/main/presenter/builtInToolsPresenter/index.ts index 298b35af2fe95ff57d6627355d69c4321fdd7a97..c829fc02964595a917e7d5d2198892326db30e23 100644 --- a/src/main/presenter/builtInToolsPresenter/index.ts +++ b/src/main/presenter/builtInToolsPresenter/index.ts @@ -13,6 +13,7 @@ import { writeFileTool, executeWriteFileTool } from './writeFileTool' import { listFilesTool, executeListFilesTool } from './listFilesTool' import { executeCommandTool, executeCommandToolHandler } from './executeCommandTool' import { useA2AServerTool, executeUseA2AServerToolHandler } from './useA2AServerTool' +import { useMcpTool, executeUseMcpTool } from './useMcpTool' export const BUILT_IN_TOOL_SERVER_NAME = 'polymind-builtin' export const BUILT_IN_TOOL_SERVER_DESCRIPTION = 'PolyMind built-in tools' @@ -21,7 +22,8 @@ export const builtInTools: Record = { [readFileTool.name]: readFileTool, [writeFileTool.name]: writeFileTool, [listFilesTool.name]: listFilesTool, - [executeCommandTool.name]: executeCommandTool + [executeCommandTool.name]: executeCommandTool, + [useMcpTool.name]: useMcpTool } type BuiltInExecutor = (args: any, toolCallId: string) => Promise @@ -29,7 +31,8 @@ const builtInToolExecutors: Record = { [readFileTool.name]: executeReadFileTool, [writeFileTool.name]: executeWriteFileTool, [listFilesTool.name]: executeListFilesTool, - [executeCommandTool.name]: executeCommandToolHandler + [executeCommandTool.name]: executeCommandToolHandler, + [useMcpTool.name]: executeUseMcpTool } const a2aBuiltInTools: Record = { @@ -192,14 +195,7 @@ export class BuiltInToolsPresenter implements IBuiltInToolsPresenter { } } - async getBuiltInToolDefinitions( - enabled: boolean = true, - currentAgent?: Agent - ): Promise { - if (!enabled) { - return [] - } - + async getBuiltInToolDefinitions(currentAgent?: Agent): Promise { try { const tools = await this.getBuiltInTools(currentAgent) return tools.map((tool) => this.mapToolToDefinition(tool)) @@ -213,12 +209,12 @@ export class BuiltInToolsPresenter implements IBuiltInToolsPresenter { * 将 MCPToolDefinition 转换为 XML 格式 * @returns XML 格式的工具定义字符串 */ - async convertToolsToXml(enabled: boolean = true, currentAgent?: Agent): Promise { - const tools = await this.getBuiltInToolDefinitions(enabled, currentAgent) + convertToolsToXml(tools: MCPToolDefinition[]): string { const xmlTools = tools .map((tool) => { const { name, description, parameters } = tool.function const { properties, required = [] } = parameters + const serverName = tool.server?.name ? tool.server.name : '' const paramsXml = Object.entries(properties) .map(([paramName, paramDef]) => { @@ -232,7 +228,7 @@ export class BuiltInToolsPresenter implements IBuiltInToolsPresenter { }) .join('\n ') - return ` + return ` ${paramsXml} ` }) diff --git a/src/main/presenter/builtInToolsPresenter/useMcpTool.ts b/src/main/presenter/builtInToolsPresenter/useMcpTool.ts new file mode 100644 index 0000000000000000000000000000000000000000..b39ecc809c7724939ed1aff6d226c85d7c025ffb --- /dev/null +++ b/src/main/presenter/builtInToolsPresenter/useMcpTool.ts @@ -0,0 +1,189 @@ +import { BuiltInToolDefinition, BuiltInToolResponse, buildRawData } from './base' +import { MCPToolCall, MCPToolDefinition, MCPToolResponse } from '@shared/presenter' + +export const useMcpTool: BuiltInToolDefinition = { + name: 'use_mcp_tool', + description: + 'Request to use a tool provided by a connected MCP server. Each MCP server can provide multiple tools with different capabilities. Tools have defined input schemas that specify required and optional parameters.', + parameters: { + type: 'object', + properties: { + server_name: { + type: 'string', + description: 'The name of the MCP server providing the tool.' + }, + tool_name: { + type: 'string', + description: 'The name of the tool to execute.' + }, + arguments: { + type: 'object', + description: + "A JSON object containing the tool's input parameters, following the tool's input schema." + } + }, + required: ['server_name', 'tool_name', 'arguments'] + } +} + +function validateArgs(args: any): { + serverName: string + toolName: string + toolArguments: Record +} { + const { server_name, tool_name, arguments: toolArguments } = args ?? {} + + if (typeof server_name !== 'string' || server_name.trim().length === 0) { + throw new Error('server_name is required and must be a non-empty string') + } + if (typeof tool_name !== 'string' || tool_name.trim().length === 0) { + throw new Error('tool_name is required and must be a non-empty string') + } + if ( + toolArguments === null || + toolArguments === undefined || + typeof toolArguments !== 'object' || + Array.isArray(toolArguments) + ) { + throw new Error('arguments is required and must be a JSON object') + } + + return { + serverName: server_name.trim(), + toolName: tool_name.trim(), + toolArguments + } +} + +async function resolveToolName( + mcpToolDefinitions: MCPToolDefinition[], + serverName: string, + requestedToolName: string +): Promise<{ resolvedToolName: string; definition?: MCPToolDefinition }> { + if (!Array.isArray(mcpToolDefinitions)) { + return { resolvedToolName: requestedToolName } + } + + const directMatch = mcpToolDefinitions.find( + (def) => def.server?.name === serverName && def.function.name === requestedToolName + ) + if (directMatch) { + return { resolvedToolName: directMatch.function.name, definition: directMatch } + } + + const prefixedName = `${serverName}_${requestedToolName}` + const prefixedMatch = mcpToolDefinitions.find( + (def) => def.server?.name === serverName && def.function.name === prefixedName + ) + if (prefixedMatch) { + return { resolvedToolName: prefixedMatch.function.name, definition: prefixedMatch } + } + + const suffixMatch = mcpToolDefinitions.find( + (def) => + def.server?.name === serverName && + typeof def.function.name === 'string' && + def.function.name.endsWith(`_${requestedToolName}`) + ) + if (suffixMatch) { + return { resolvedToolName: suffixMatch.function.name, definition: suffixMatch } + } + + return { resolvedToolName: requestedToolName } +} + +export async function executeUseMcpTool( + args: any, + toolCallId: string +): Promise { + try { + const { serverName, toolName, toolArguments } = validateArgs(args) + + // Import presenter lazily to avoid circular dependencies at module load time + const { presenter } = await import('@/presenter') + const mcpPresenter = presenter?.mcpPresenter + + if (!mcpPresenter || typeof mcpPresenter.callTool !== 'function') { + throw new Error('MCP presenter is not available') + } + + const toolDefinitions = await mcpPresenter.getAllToolDefinitions() + const { resolvedToolName, definition } = await resolveToolName( + toolDefinitions, + serverName, + toolName + ) + + const toolCall: MCPToolCall = { + id: toolCallId, + type: 'function', + function: { + name: resolvedToolName, + arguments: JSON.stringify(toolArguments) + }, + server: definition?.server ?? { + name: serverName, + icons: '', + description: '' + } + } + + const result = await mcpPresenter.callTool(toolCall) + const rawData: MCPToolResponse = { + ...result.rawData, + toolCallId, + _meta: { + ...(result.rawData?._meta ?? {}), + serverName, + toolName: resolvedToolName, + arguments: toolArguments + } + } + + if (rawData.requiresPermission) { + const permission = rawData.permissionRequest + const content = `Permission required to execute MCP tool.\nServer: ${serverName}\nTool: ${resolvedToolName}\nPermission: ${permission?.permissionType ?? 'unknown'}` + const metadata = { + serverName, + toolName: resolvedToolName, + permissionRequest: permission + } + return { + toolCallId, + content, + success: false, + metadata, + rawData + } + } + + const success = !rawData.isError + const contentPrefix = success ? 'MCP tool executed successfully.' : 'MCP tool execution failed.' + const content = `${contentPrefix}\nServer: ${serverName}\nTool: ${resolvedToolName}\n\n${result.content}` + + const metadata = { + serverName, + toolName: resolvedToolName, + arguments: toolArguments + } + + return { + toolCallId, + content, + success, + metadata, + rawData + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + const failureMessage = `Failed to execute MCP tool: ${errorMessage}` + const metadata = { error: errorMessage } + return { + toolCallId, + content: failureMessage, + success: false, + metadata, + rawData: buildRawData(toolCallId, failureMessage, true, metadata) + } + } +} diff --git a/src/main/presenter/configPresenter/index.ts b/src/main/presenter/configPresenter/index.ts index d6f84d2d1f1348d194c8f24a1cb57834440e6858..8f29e2a08e3142755bdf1895148f83b09149fb6f 100644 --- a/src/main/presenter/configPresenter/index.ts +++ b/src/main/presenter/configPresenter/index.ts @@ -1271,8 +1271,8 @@ export class ConfigPresenter implements IConfigPresenter { private async getBuildInSystemPrompt(agent?: Agent): Promise { // 获取内置的系统提示词 - const useBuiltInToolsEnabled = this.getUseBuiltInToolsEnabled() - return await SYSTEM_PROMPT('', '', this.getLanguage(), '', useBuiltInToolsEnabled, agent) + const enabledMcpTools = await presenter.mcpPresenter.getAllToolDefinitions() + return await SYSTEM_PROMPT('', '', this.getLanguage(), '', agent, enabledMcpTools) } async getSystemPrompts(): Promise { diff --git a/src/main/presenter/llmProviderPresenter/index.ts b/src/main/presenter/llmProviderPresenter/index.ts index 8851e477723b15b3fb462c469dea3631c5367c48..ab27e84eda6c5102e7928b705a10492eb5583340 100644 --- a/src/main/presenter/llmProviderPresenter/index.ts +++ b/src/main/presenter/llmProviderPresenter/index.ts @@ -12,7 +12,6 @@ import { ModelScopeMcpSyncOptions, ModelScopeMcpSyncResult, IConfigPresenter, - MCPToolDefinition, Agent } from '@shared/presenter' import { ProviderChange, ProviderBatchUpdate } from '@shared/provider-operations' @@ -801,17 +800,9 @@ export class LLMProviderPresenter implements ILlmProviderPresenter { try { console.log(`[Agent Loop] Iteration ${toolCallCount + 1} for event: ${eventId}`) - let availableTools: MCPToolDefinition[] = [] - const useBuiltInToolsEnabled = this.configPresenter.getUseBuiltInToolsEnabled() - const [mcpTools, builtInTools] = await Promise.all([ - presenter.mcpPresenter.getAllToolDefinitions(enabledMcpTools), - presenter.builtInToolsPresenter.getBuiltInToolDefinitions( - useBuiltInToolsEnabled, - currentAgent - ) - ]) - availableTools = [...mcpTools, ...builtInTools] + const availableTools = + await presenter.builtInToolsPresenter.getBuiltInToolDefinitions(currentAgent) const canExecute = this.canExecuteImmediately(providerId) if (!canExecute) { const config = this.getProviderRateLimitConfig(providerId) diff --git a/src/main/presenter/promptPresenter/sections/tool-use.ts b/src/main/presenter/promptPresenter/sections/tool-use.ts index 72466c6146479fcfaa867effb1178c9883bd2af3..35d8feeaed43d6fb8f82fc4a05f13f618d71955f 100644 --- a/src/main/presenter/promptPresenter/sections/tool-use.ts +++ b/src/main/presenter/promptPresenter/sections/tool-use.ts @@ -4,13 +4,22 @@ function getSystemPromptByA2ATool(toolsXML: string): string { } return '' } -export function getSharedToolUseSection(toolsXML: string): string { +export function getSharedToolUseSection(toolsXML: string, mcpToolsXML: string): string { + const connectedServers = mcpToolsXML + ? ` +Connected MCP Servers: +\`\`\`xml +${mcpToolsXML} +\`\`\` +` + : '(No MCP servers currently connected)' + return ` ==== # Tool Use You have the ability to invoke external tools to assist in resolving user problems. ${getSystemPromptByA2ATool(toolsXML)} ## Tools -The list of available tools is defined in the tag: +The list of available tools is defined in the tag and tag: \`\`\`xml @@ -18,10 +27,20 @@ The list of available tools is defined in the tag: ${toolsXML} -\`\`\` + +You can prioritize using the tools listed above to solve problems. +The tools defined in tag below are some MCP server tools, you should choose the appropriate tool based on the description of each tool to solve specific problems. +You can use the MCP server's tools via the \`use_mcp_tool\` tool. + +\`\`\`xml + + +${connectedServers} + + ## Tool Use Formatting -When invoking tools, your output should **only** contain the tag and its content, without any other text, explanations or comments. +When invoking tools, your output should **only** contain the one tag and its content, without any other text, explanations or comments. Tool uses are formatted using XML-style tags. Here's the structure: diff --git a/src/main/presenter/promptPresenter/system.ts b/src/main/presenter/promptPresenter/system.ts index f390c11a74344aa2f08979267e118a2eb43fb4ed..e86400e7db7f1f8d91480c6b4e5349024f0aeb61 100644 --- a/src/main/presenter/promptPresenter/system.ts +++ b/src/main/presenter/promptPresenter/system.ts @@ -3,20 +3,19 @@ import { formatLanguage } from '../../../shared/language' import { getSystemInfoSection, getObjectiveSection, - getSharedToolUseSection, addCustomInstructions, + getSharedToolUseSection, markdownFormattingSection } from './sections' -import { Agent } from '@shared/presenter' - +import { Agent, MCPToolDefinition } from '@shared/presenter' async function generatePrompt( cwd?: string, globalCustomInstructions?: string, language?: string, IgnoreInstructions?: string, - useBuiltInToolsEnabled?: boolean, - agent?: Agent + agent?: Agent, + enabledMcpTools?: MCPToolDefinition[] ): Promise { const promptSections: string[] = [] if (agent) { @@ -34,13 +33,14 @@ async function generatePrompt( } promptSections.push(markdownFormattingSection()) - if (useBuiltInToolsEnabled) { - const toolsXML = await presenter.builtInToolsPresenter.convertToolsToXml( - useBuiltInToolsEnabled, - agent - ) - promptSections.push(`${getSharedToolUseSection(toolsXML)}`) + const builtInTool = await presenter.builtInToolsPresenter.getBuiltInToolDefinitions(agent) + const builtInToolXML = presenter.builtInToolsPresenter.convertToolsToXml(builtInTool) + + let mcpToolsXML = '' + if (enabledMcpTools && enabledMcpTools.length > 0) { + mcpToolsXML = presenter.builtInToolsPresenter.convertToolsToXml(enabledMcpTools) } + promptSections.push(`${getSharedToolUseSection(builtInToolXML, mcpToolsXML)}`) promptSections.push(getSystemInfoSection(), getObjectiveSection()) @@ -67,15 +67,15 @@ export const SYSTEM_PROMPT = async ( globalCustomInstructions?: string, language?: string, IgnoreInstructions?: string, - useBuiltInToolsEnabled?: boolean, - agent?: Agent + agent?: Agent, + enabledMcpTools?: MCPToolDefinition[] ): Promise => { return generatePrompt( cwd, globalCustomInstructions, language, IgnoreInstructions, - useBuiltInToolsEnabled, - agent + agent, + enabledMcpTools ) } diff --git a/src/shared/types/presenters/legacy.presenters.d.ts b/src/shared/types/presenters/legacy.presenters.d.ts index 96f02ad15ce91d56b7fe8380b755f3085ecd5666..cf282d0529b85dc804652741b4d0e1d2ba797209 100644 --- a/src/shared/types/presenters/legacy.presenters.d.ts +++ b/src/shared/types/presenters/legacy.presenters.d.ts @@ -1247,8 +1247,8 @@ export interface ProgressResponse { // export interface IBuiltInToolsPresenter { - convertToolsToXml(useBuiltInToolsEnabled: boolean, currentAgent?: Agent): Promise - getBuiltInToolDefinitions(enabled?: boolean, currentAgent?: Agent | null): any + convertToolsToXml(tools: MCPToolDefinition[]): string + getBuiltInToolDefinitions(currentAgent?: Agent | null): any /** * 获取所有内置工具的定义