diff --git a/api/server/controllers/agents/v1.js b/api/server/controllers/agents/v1.js index 157b4e8493..abf9c7bae0 100644 --- a/api/server/controllers/agents/v1.js +++ b/api/server/controllers/agents/v1.js @@ -215,6 +215,7 @@ const filterAuthorizedTools = async ({ availableTools, existingTools, configServers, + resolvedServerNames, }) => { const filteredTools = []; let mcpServerConfigs; @@ -283,6 +284,7 @@ const filterAuthorizedTools = async ({ continue; } + resolvedServerNames?.add(serverName); filteredTools.push(tool); } @@ -433,6 +435,9 @@ const createAgentHandler = async (req, res) => { hasMCPTools ? resolveConfigServers(req) : Promise.resolve(undefined), ]); const mcpPermissionContext = createMCPPermissionContext(req); + /** Resolved during authorization, so persistence indexes the real server rather + * than a suffix guess - see the note on `filterAuthorizedTools`. */ + const resolvedServerNames = new Set(); agentData.tools = await filterAuthorizedTools({ tools, userId, @@ -441,7 +446,11 @@ const createAgentHandler = async (req, res) => { mcpPermissionContext, availableTools, configServers, + resolvedServerNames, }); + if (hasMCPTools) { + agentData.mcpServerNames = Array.from(resolvedServerNames); + } const agent = await db.createAgent(agentData); @@ -735,6 +744,7 @@ const updateAgentHandler = async (req, res) => { getCachedTools().then((t) => t ?? {}), resolveConfigServers(req), ]); + const resolvedServerNames = new Set(); const approvedNew = await filterAuthorizedTools({ tools: newMCPTools, userId: req.user.id, @@ -743,11 +753,19 @@ const updateAgentHandler = async (req, res) => { mcpPermissionContext, availableTools, configServers, + resolvedServerNames, }); const rejectedSet = new Set(newMCPTools.filter((t) => !approvedNew.includes(t))); if (rejectedSet.size > 0) { updateData.tools = updateData.tools.filter((t) => !rejectedSet.has(t)); } + /** Union with what the agent already had: the new tools were resolved during + * authorization, and re-deriving the rest from their keys would reintroduce + * the suffix guess this avoids. */ + for (const existingName of existingAgent.mcpServerNames ?? []) { + resolvedServerNames.add(existingName); + } + updateData.mcpServerNames = Array.from(resolvedServerNames); } } } @@ -903,6 +921,9 @@ const duplicateAgentHandler = async (req, res) => { resolveConfigServers(req), ]); const mcpPermissionContext = createMCPPermissionContext(req); + /** The duplicate carries the source agent's `mcpServerNames`; replace it with what + * this user is actually authorized for, or the copy would grant the source's servers. */ + const resolvedServerNames = new Set(); newAgentData.tools = await filterAuthorizedTools({ tools: newAgentData.tools, userId, @@ -912,7 +933,9 @@ const duplicateAgentHandler = async (req, res) => { availableTools, existingTools: newAgentData.tools, configServers, + resolvedServerNames, }); + newAgentData.mcpServerNames = Array.from(resolvedServerNames); } if (newAgentData.tool_resources) { diff --git a/client/src/components/Chat/Messages/Content/ToolCall.tsx b/client/src/components/Chat/Messages/Content/ToolCall.tsx index 0acd42277f..bc3efc4605 100644 --- a/client/src/components/Chat/Messages/Content/ToolCall.tsx +++ b/client/src/components/Chat/Messages/Content/ToolCall.tsx @@ -12,7 +12,7 @@ import { import type { TAttachment } from 'librechat-data-provider'; import { useLocalize, useProgress, useExpandCollapse } from '~/hooks'; import { ToolIcon, getToolIconType, isError } from './ToolOutput'; -import { useMCPIconMap } from '~/hooks/MCP'; +import { useMCPIconMap, useMCPServerNames } from '~/hooks/MCP'; import { AttachmentGroup } from './Parts'; import ToolCallInfo from './ToolCallInfo'; import ProgressText from './ProgressText'; @@ -67,12 +67,13 @@ export default function ToolCall({ } }, [auth]); + const mcpServerNames = useMCPServerNames(); const { function_name, domain, isMCPToolCall, mcpServerName } = useMemo(() => { if (typeof name !== 'string') { return { function_name: '', domain: null, isMCPToolCall: false, mcpServerName: '' }; } if (name.includes(Constants.mcp_delimiter)) { - const [func, server = ''] = splitToolCallName(name); + const [func, server = ''] = splitToolCallName(name, mcpServerNames); const displayName = func === 'oauth' ? server : func; return { function_name: displayName || '', @@ -104,7 +105,7 @@ export default function ToolCall({ isMCPToolCall: false, mcpServerName: '', }; - }, [name, parsedAuthUrl]); + }, [name, parsedAuthUrl, mcpServerNames]); const toolIconType = useMemo(() => getToolIconType(name), [name]); const mcpIconMap = useMCPIconMap(); diff --git a/client/src/hooks/MCP/index.ts b/client/src/hooks/MCP/index.ts index b53003f3ce..b32bca419b 100644 --- a/client/src/hooks/MCP/index.ts +++ b/client/src/hooks/MCP/index.ts @@ -3,5 +3,5 @@ export * from './useVisibleTools'; export * from './useMCPServerManager'; export * from './useMCPConnectionStatus'; -export { useMCPIconMap } from './useMCPIconMap'; +export { useMCPIconMap, useMCPServerNames } from './useMCPIconMap'; export { useRemoveMCPTool } from './useRemoveMCPTool'; diff --git a/client/src/hooks/MCP/useMCPIconMap.ts b/client/src/hooks/MCP/useMCPIconMap.ts index 43f109b68c..017462d995 100644 --- a/client/src/hooks/MCP/useMCPIconMap.ts +++ b/client/src/hooks/MCP/useMCPIconMap.ts @@ -17,3 +17,9 @@ export function useMCPIconMap(): Map { return map; }, [servers]); } + +/** Configured MCP server names, used to resolve the tool-key boundary exactly. */ +export function useMCPServerNames(): string[] { + const { data: servers } = useMCPServersQuery(); + return useMemo(() => (servers ? Object.keys(servers) : []), [servers]); +} diff --git a/packages/data-schemas/src/methods/agent.ts b/packages/data-schemas/src/methods/agent.ts index f04340c3cd..1437930d0e 100644 --- a/packages/data-schemas/src/methods/agent.ts +++ b/packages/data-schemas/src/methods/agent.ts @@ -368,7 +368,11 @@ export function createAgentMethods( }, ], category: (agentData.category as string) || 'general', - mcpServerNames: extractMCPServerNames(agentData.tools as string[] | undefined), + /** Callers that authorized the tools pass resolved names; deriving from the key + * alone cannot tell a config server's suffix from a real DB server name. */ + mcpServerNames: + (agentData.mcpServerNames as string[] | undefined) ?? + extractMCPServerNames(agentData.tools as string[] | undefined), }; return (await Agent.create(initialAgentData)).toObject() as IAgent; @@ -523,9 +527,14 @@ export function createAgentMethods( // Sync mcpServerNames when tools are updated if ((directUpdates as Record).tools !== undefined) { - const mcpServerNames = extractMCPServerNames( - (directUpdates as Record).tools as string[], - ); + /** Callers that authorized the tools pass resolved names; deriving from the key + * alone cannot tell a config server's suffix from a real DB server name. */ + const supplied = (directUpdates as Record).mcpServerNames as + | string[] + | undefined; + const mcpServerNames = + supplied ?? + extractMCPServerNames((directUpdates as Record).tools as string[]); (directUpdates as Record).mcpServerNames = mcpServerNames; updateData.mcpServerNames = mcpServerNames; }