fix: thread configured server names through display parsing

parseToolName and getMCPServerName resolved context-free, so a configured
server whose name contains the delimiter showed the wrong server in grouped
tool summaries and subagent tool labels, and stacked icons missed its entry in
the icon map. Both take the configured names now, supplied by the components
that render them.

Adds the hook to SubagentCall's mock factory: the spec renders the real
component, so an unmocked useMCPServerNames would reach the query with no
provider.
This commit is contained in:
Danny Avila 2026-07-27 09:17:49 -04:00
parent 07404c0cf9
commit c47e5b706b
6 changed files with 34 additions and 14 deletions

View file

@ -16,6 +16,7 @@ import { MessageContext } from '~/Providers/MessageContext';
import MessageIcon from '~/components/Share/MessageIcon';
import { subagentProgressByToolCallId } from '~/store';
import { useAgentsMapContext } from '~/Providers';
import { useMCPServerNames } from '~/hooks/MCP';
import { AttachmentGroup } from './Attachment';
import { useLocalize } from '~/hooks';
import Reasoning from './Reasoning';
@ -704,11 +705,13 @@ function ToolNameBadge({ name }: { name: string }): JSX.Element {
function ToolIdentifier({
rawName,
localize,
mcpServerNames,
}: {
rawName: string;
localize: ReturnType<typeof useLocalize>;
mcpServerNames?: readonly string[];
}): JSX.Element {
const parsed = parseToolName(rawName);
const parsed = parseToolName(rawName, mcpServerNames);
if (parsed.mcpServer) {
return (
<span className="inline-flex min-w-0 shrink items-baseline gap-1">
@ -740,6 +743,7 @@ function ToolIdentifier({
*/
function TickerLineView({ line }: { line: SubagentTickerLine }): JSX.Element {
const localize = useLocalize();
const mcpServerNames = useMCPServerNames();
if (line.kind === 'writing' || line.kind === 'reasoning') {
const prefix =
line.kind === 'writing'
@ -766,7 +770,7 @@ function TickerLineView({ line }: { line: SubagentTickerLine }): JSX.Element {
{line.toolNames.map((name, i) => (
<span key={`${i}-${name}`} className="flex min-w-0 items-baseline gap-1">
{i > 0 && <span className="shrink-0 text-text-tertiary">,</span>}
<ToolIdentifier rawName={name} localize={localize} />
<ToolIdentifier rawName={name} localize={localize} mcpServerNames={mcpServerNames} />
</span>
))}
{line.argsSnippet && (
@ -779,7 +783,11 @@ function TickerLineView({ line }: { line: SubagentTickerLine }): JSX.Element {
if (line.kind === 'tool_complete') {
return (
<li className="flex w-full items-baseline gap-1 overflow-hidden whitespace-nowrap">
<ToolIdentifier rawName={line.toolName} localize={localize} />
<ToolIdentifier
rawName={line.toolName}
localize={localize}
mcpServerNames={mcpServerNames}
/>
<span className="shrink-0 text-text-tertiary"></span>
<span
dir="rtl"

View file

@ -125,6 +125,11 @@ jest.mock('~/components/Share/MessageIcon', () => ({
),
}));
jest.mock('~/hooks/MCP', () => {
const mcpServerNames: string[] = [];
return { useMCPServerNames: () => mcpServerNames };
});
jest.mock('~/utils', () => ({
...jest.requireActual('~/utils/groupToolCalls'),
...jest.requireActual('~/utils/toolLabels'),

View file

@ -10,11 +10,11 @@ import type {
} from 'librechat-data-provider';
import type { PartWithIndex } from './ParallelContent';
import { useLocalize, useExpandCollapse, scheduleMessageContentLayoutReconcile } from '~/hooks';
import { useMCPIconMap, useMCPServerNames } from '~/hooks/MCP';
import { isBashProgrammaticToolCall } from './routing';
import { ASK_USER_QUESTION } from '~/utils/approval';
import { cn, getToolDisplayLabel } from '~/utils';
import { StackedToolIcons } from './ToolOutput';
import { useMCPIconMap } from '~/hooks/MCP';
import { AttachmentGroup } from './Parts';
import store from '~/store';
@ -126,6 +126,7 @@ export default function ToolCallGroup({
}: ToolCallGroupProps) {
const localize = useLocalize();
const mcpIconMap = useMCPIconMap();
const mcpServerNames = useMCPServerNames();
const rootRef = useRef<HTMLDivElement | null>(null);
const cancelLayoutReconcileRef = useRef<(() => void) | null>(null);
const retainedForPendingApprovalRef = useRef(false);
@ -179,7 +180,7 @@ export default function ToolCallGroup({
const labels: string[] = [];
for (const rawName of toolNames) {
if (!rawName) continue;
const label = getToolDisplayLabel(rawName, localize);
const label = getToolDisplayLabel(rawName, localize, mcpServerNames);
if (!seen.has(label)) {
seen.add(label);
labels.push(label);
@ -189,7 +190,7 @@ export default function ToolCallGroup({
return labels.join(', ');
}
return `${labels.slice(0, 3).join(', ')}, +${labels.length - 3}`;
}, [toolNames, localize]);
}, [toolNames, localize, mcpServerNames]);
const autoExpand = useRecoilValue(store.autoExpandTools);
const autoCollapse = !autoExpand && count >= 2 && allCompleted;

View file

@ -1,6 +1,7 @@
import { useMemo } from 'react';
import ToolIcon, { getToolIconType, getMCPServerName } from './ToolIcon';
import type { ToolIconType } from './ToolIcon';
import ToolIcon, { getToolIconType, getMCPServerName } from './ToolIcon';
import { useMCPServerNames } from '~/hooks/MCP';
import { cn } from '~/utils';
interface ResolvedIcon {
@ -22,12 +23,13 @@ export default function StackedToolIcons({
maxIcons = 3,
isAnimating = false,
}: StackedToolIconsProps) {
const mcpServerNames = useMCPServerNames();
const uniqueIcons = useMemo(() => {
const seen = new Set<string>();
const result: ResolvedIcon[] = [];
for (const name of toolNames) {
const type = getToolIconType(name);
const serverName = getMCPServerName(name);
const serverName = getMCPServerName(name, mcpServerNames);
const iconUrl = serverName ? mcpIconMap?.get(serverName) : undefined;
const key = iconUrl ? `mcp-${serverName}` : type;
if (!seen.has(key)) {
@ -36,7 +38,7 @@ export default function StackedToolIcons({
}
}
return result;
}, [toolNames, mcpIconMap]);
}, [toolNames, mcpIconMap, mcpServerNames]);
const visibleIcons = uniqueIcons.slice(0, maxIcons);
const overflowCount = uniqueIcons.length - visibleIcons.length;

View file

@ -91,11 +91,11 @@ export function getToolIconType(name: string): ToolIconType {
}
/** Extracts the MCP server name from a tool name with format `tool<delimiter>server`. */
export function getMCPServerName(toolName: string): string {
export function getMCPServerName(toolName: string, knownServerNames?: readonly string[]): string {
if (!toolName.includes(Constants.mcp_delimiter)) {
return '';
}
const [, serverName] = splitToolCallName(toolName);
const [, serverName] = splitToolCallName(toolName, knownServerNames);
return serverName ?? '';
}

View file

@ -46,9 +46,12 @@ export interface ParsedToolName {
* - `web_search` `{ mcpServer: '', toolName: 'web_search', friendlyKey: 'com_ui_tool_name_web_search' }`
* - `some_custom_tool` `{ mcpServer: '', toolName: 'some_custom_tool' }`
*/
export function parseToolName(rawName: string): ParsedToolName {
export function parseToolName(
rawName: string,
knownServerNames?: readonly string[],
): ParsedToolName {
if (rawName.includes(Constants.mcp_delimiter)) {
const [toolName, mcpServer = ''] = splitToolCallName(rawName);
const [toolName, mcpServer = ''] = splitToolCallName(rawName, knownServerNames);
return { raw: rawName, mcpServer, toolName };
}
const friendlyKey = TOOL_FRIENDLY_NAME_KEYS[rawName];
@ -72,8 +75,9 @@ export function parseToolName(rawName: string): ParsedToolName {
export function getToolDisplayLabel(
rawName: string,
localize: (key: TranslationKeys) => string,
knownServerNames?: readonly string[],
): string {
const parsed = parseToolName(rawName);
const parsed = parseToolName(rawName, knownServerNames);
if (parsed.mcpServer) return parsed.mcpServer;
if (parsed.friendlyKey) return localize(parsed.friendlyKey);
return parsed.toolName;