Skip to content

Commit 7f838ac

Browse files
authored
🐛 In the MCP configuration, connectivity is verified, and after a tool is deleted or added (or its status changes), the tool list and agent list are refreshed synchronously. #1345
2 parents 232c819 + d193f16 commit 7f838ac

File tree

5 files changed

+328
-54
lines changed

5 files changed

+328
-54
lines changed

frontend/app/[locale]/setup/agents/components/AgentSetupOrchestrator.tsx

Lines changed: 243 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
searchToolConfig,
1919
updateToolConfig,
2020
} from "@/services/agentConfigService";
21-
import { Agent, AgentSetupOrchestratorProps } from "@/types/agentConfig";
21+
import { Agent, AgentSetupOrchestratorProps, Tool } from "@/types/agentConfig";
2222
import log from "@/lib/logger";
2323

2424
import SubAgentPool from "./agent/SubAgentPool";
@@ -93,6 +93,11 @@ export default function AgentSetupOrchestrator({
9393
const [isEmbeddingAutoUnsetOpen, setIsEmbeddingAutoUnsetOpen] =
9494
useState(false);
9595
const lastProcessedAgentIdForEmbedding = useRef<number | null>(null);
96+
97+
// Flag to track if we need to refresh enabledToolIds after tools update
98+
const shouldRefreshEnabledToolIds = useRef(false);
99+
// Track previous tools prop to detect when it's updated
100+
const previousToolsRef = useRef<Tool[] | undefined>(undefined);
96101

97102
// Edit agent related status
98103
const [isEditingAgent, setIsEditingAgent] = useState(false);
@@ -259,10 +264,15 @@ export default function AgentSetupOrchestrator({
259264

260265
// Listen for changes in the tool status, update the selected tool
261266
useEffect(() => {
262-
if (!tools || !enabledToolIds || isLoadingTools) return;
263-
264-
const enabledTools = tools.filter((tool) =>
265-
enabledToolIds.includes(Number(tool.id))
267+
if (!tools || isLoadingTools) return;
268+
// Allow empty enabledToolIds array (it's valid when no tools are selected)
269+
if (enabledToolIds === undefined || enabledToolIds === null) return;
270+
271+
// Filter out unavailable tools (is_available === false) to prevent deleted MCP tools from showing
272+
const enabledTools = tools.filter(
273+
(tool) =>
274+
enabledToolIds.includes(Number(tool.id)) &&
275+
tool.is_available !== false
266276
);
267277

268278
setSelectedTools(enabledTools);
@@ -338,6 +348,205 @@ export default function AgentSetupOrchestrator({
338348
};
339349
}, [t]);
340350

351+
// Listen for tools updated events and refresh enabledToolIds if agent is selected
352+
useEffect(() => {
353+
const handleToolsUpdated = async () => {
354+
// If there's a selected agent (mainAgentId or editingAgent), refresh enabledToolIds
355+
const currentAgentId = (isEditingAgent && editingAgent
356+
? Number(editingAgent.id)
357+
: mainAgentId
358+
? Number(mainAgentId)
359+
: undefined) as number | undefined;
360+
361+
if (currentAgentId) {
362+
try {
363+
// First, refresh the tools list to ensure it's up to date
364+
// Pass false to prevent showing success message (MCP modal will show its own message)
365+
if (onToolsRefresh) {
366+
// First, synchronize the selected tools once using search_info.
367+
await refreshAgentToolSelectionsFromServer(currentAgentId);
368+
// Then refresh the tool list
369+
await onToolsRefresh(false);
370+
// Wait for React state to update and tools prop to be updated
371+
// Use setTimeout to ensure tools prop is updated before refreshing enabledToolIds
372+
await new Promise((resolve) => setTimeout(resolve, 300));
373+
// Set flag to refresh enabledToolIds after tools prop updates
374+
shouldRefreshEnabledToolIds.current = true;
375+
}
376+
} catch (error) {
377+
log.error("Failed to refresh tools after tools update:", error);
378+
}
379+
}
380+
};
381+
382+
window.addEventListener("toolsUpdated", handleToolsUpdated);
383+
384+
return () => {
385+
window.removeEventListener("toolsUpdated", handleToolsUpdated);
386+
};
387+
}, [mainAgentId, isEditingAgent, editingAgent, onToolsRefresh, t]);
388+
389+
const refreshAgentToolSelectionsFromServer = useCallback(
390+
async (agentId: number) => {
391+
try {
392+
const agentInfoResult = await searchAgentInfo(agentId);
393+
if (agentInfoResult.success && agentInfoResult.data) {
394+
const remoteTools = Array.isArray(agentInfoResult.data.tools)
395+
? agentInfoResult.data.tools
396+
: [];
397+
const enabledIdsFromServer = remoteTools
398+
.filter(
399+
(remoteTool: any) =>
400+
remoteTool && remoteTool.is_available !== false
401+
)
402+
.map((remoteTool: any) => Number(remoteTool.id))
403+
.filter((id) => !Number.isNaN(id));
404+
405+
const filteredIds = enabledIdsFromServer.filter((toolId) => {
406+
const toolMeta = tools?.find(
407+
(tool) => Number(tool.id) === Number(toolId)
408+
);
409+
return toolMeta && toolMeta.is_available !== false;
410+
});
411+
412+
const dedupedIds = Array.from(new Set(filteredIds));
413+
setEnabledToolIds(dedupedIds);
414+
log.info("Refreshed agent tool selection from search_info", {
415+
agentId,
416+
toolIds: dedupedIds,
417+
});
418+
} else {
419+
log.error(
420+
"Failed to refresh agent tool selection via search_info",
421+
agentInfoResult.message
422+
);
423+
}
424+
} catch (error) {
425+
log.error(
426+
"Failed to refresh agent tool selection via search_info:",
427+
error
428+
);
429+
}
430+
},
431+
[tools, setEnabledToolIds, setSelectedTools]
432+
);
433+
434+
// Refresh enabledToolIds when tools prop updates after toolsUpdated event
435+
useEffect(() => {
436+
const prevTools = previousToolsRef.current;
437+
const haveTools = tools && tools.length > 0;
438+
const prevLen = prevTools?.length ?? 0;
439+
const currLen = tools?.length ?? 0;
440+
const idsChanged =
441+
prevTools === undefined ||
442+
JSON.stringify(prevTools?.map((t) => t.id).sort()) !==
443+
JSON.stringify((tools || []).map((t) => t.id).sort());
444+
const grew = currLen > prevLen;
445+
446+
// Always update the previous ref for future comparisons
447+
previousToolsRef.current = tools;
448+
449+
// If there are no tools, nothing to do
450+
if (!haveTools) {
451+
return;
452+
}
453+
454+
const currentAgentId = (isEditingAgent && editingAgent
455+
? Number(editingAgent.id)
456+
: mainAgentId
457+
? Number(mainAgentId)
458+
: undefined) as number | undefined;
459+
460+
if (!currentAgentId) {
461+
shouldRefreshEnabledToolIds.current = false;
462+
return;
463+
}
464+
465+
const refreshEnabledToolIds = async () => {
466+
try {
467+
// Small delay to allow tools prop to stabilize after updates
468+
await new Promise((resolve) => setTimeout(resolve, 50));
469+
await refreshAgentToolSelectionsFromServer(currentAgentId);
470+
} catch (error) {
471+
log.error(
472+
"Failed to refresh enabled tool IDs after tools update:",
473+
error
474+
);
475+
}
476+
shouldRefreshEnabledToolIds.current = false;
477+
};
478+
479+
// Trigger when:
480+
// 1) We explicitly flagged a refresh after a toolsUpdated event, OR
481+
// 2) The tool list grew (e.g., an MCP tool was added) or IDs changed,
482+
// which indicates the available tool set has changed and we should re-sync
483+
if (shouldRefreshEnabledToolIds.current || grew || idsChanged) {
484+
// Optimistically update selected tools to reduce perceived delay/flicker
485+
if (haveTools && Array.isArray(enabledToolIds) && enabledToolIds.length > 0) {
486+
try {
487+
const optimisticSelected = (tools || []).filter((tool) =>
488+
enabledToolIds.includes(Number(tool.id))
489+
);
490+
setSelectedTools(optimisticSelected);
491+
} catch (e) {
492+
log.warn("Optimistic selection update failed; will rely on refresh", e);
493+
}
494+
}
495+
refreshEnabledToolIds();
496+
}
497+
}, [
498+
tools,
499+
mainAgentId,
500+
isEditingAgent,
501+
editingAgent,
502+
enabledToolIds,
503+
refreshAgentToolSelectionsFromServer,
504+
]);
505+
506+
// Immediately reflect UI selection from enabledToolIds and latest tools (no server wait)
507+
useEffect(() => {
508+
const haveTools = Array.isArray(tools) && tools.length > 0;
509+
if (!haveTools) {
510+
setSelectedTools([]);
511+
return;
512+
}
513+
if (!Array.isArray(enabledToolIds) || enabledToolIds.length === 0) {
514+
setSelectedTools([]);
515+
return;
516+
}
517+
try {
518+
const nextSelected = (tools || []).filter((tool) =>
519+
enabledToolIds.includes(Number(tool.id))
520+
);
521+
setSelectedTools(nextSelected);
522+
} catch (e) {
523+
log.warn("Failed to sync selectedTools from enabledToolIds", e);
524+
}
525+
}, [enabledToolIds, tools, setSelectedTools]);
526+
527+
// When tools change, sanitize enabledToolIds against availability to prevent transient flicker
528+
useEffect(() => {
529+
if (!Array.isArray(tools) || tools.length === 0) {
530+
return;
531+
}
532+
if (!Array.isArray(enabledToolIds)) {
533+
return;
534+
}
535+
const availableIdSet = new Set(
536+
(tools || [])
537+
.filter((t) => t && t.is_available !== false)
538+
.map((t) => Number(t.id))
539+
.filter((id) => !Number.isNaN(id))
540+
);
541+
const sanitized = enabledToolIds.filter((id) => availableIdSet.has(Number(id)));
542+
if (
543+
sanitized.length !== enabledToolIds.length ||
544+
sanitized.some((id, idx) => Number(id) !== Number(enabledToolIds[idx]))
545+
) {
546+
setEnabledToolIds(sanitized);
547+
}
548+
}, [tools, enabledToolIds, setEnabledToolIds]);
549+
341550
// Handle the creation of a new Agent
342551
const handleCreateNewAgent = async () => {
343552
// Set to create mode
@@ -646,10 +855,14 @@ export default function AgentSetupOrchestrator({
646855
setFewShotsContent?.(agentDetail.few_shots_prompt || "");
647856

648857
// Load Agent tools
858+
// Filter out unavailable tools (is_available === false) to prevent deleted MCP tools from showing
649859
if (agentDetail.tools && agentDetail.tools.length > 0) {
650-
setSelectedTools(agentDetail.tools);
651-
// Set enabled tool IDs
652-
const toolIds = agentDetail.tools.map((tool: any) => Number(tool.id));
860+
const availableTools = agentDetail.tools.filter(
861+
(tool: any) => tool.is_available !== false
862+
);
863+
setSelectedTools(availableTools);
864+
// Set enabled tool IDs only for available tools
865+
const toolIds = availableTools.map((tool: any) => Number(tool.id));
653866
setEnabledToolIds(toolIds);
654867
} else {
655868
setSelectedTools([]);
@@ -883,11 +1096,28 @@ export default function AgentSetupOrchestrator({
8831096
};
8841097

8851098
// Refresh tool list
886-
const handleToolsRefresh = useCallback(async () => {
887-
if (onToolsRefresh) {
888-
onToolsRefresh();
889-
}
890-
}, [onToolsRefresh]);
1099+
const handleToolsRefresh = useCallback(
1100+
async (showSuccessMessage = true) => {
1101+
if (onToolsRefresh) {
1102+
// Before refreshing the tool list, synchronize the selected tools using search_info.
1103+
const currentAgentId = (isEditingAgent && editingAgent
1104+
? Number(editingAgent.id)
1105+
: mainAgentId
1106+
? Number(mainAgentId)
1107+
: undefined) as number | undefined;
1108+
if (currentAgentId) {
1109+
await refreshAgentToolSelectionsFromServer(currentAgentId);
1110+
}
1111+
const refreshedTools = await onToolsRefresh(showSuccessMessage);
1112+
if (refreshedTools) {
1113+
shouldRefreshEnabledToolIds.current = true;
1114+
}
1115+
return refreshedTools;
1116+
}
1117+
return undefined;
1118+
},
1119+
[onToolsRefresh, isEditingAgent, editingAgent, mainAgentId, refreshAgentToolSelectionsFromServer]
1120+
);
8911121

8921122
// Get button tooltip information
8931123
const getLocalButtonTitle = () => {

0 commit comments

Comments
 (0)