Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/warm-dolphins-swim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@inkeep/agents-manage-ui": patch
---

Warn when trying to connect already connected MCP tools and show 'No tools' instead of '0' badge
Original file line number Diff line number Diff line change
Expand Up @@ -564,11 +564,13 @@ export const Agent: FC<AgentProps> = ({
) {
const targetNode = nodes.find((n) => n.id === params.target);
if (targetNode && targetNode.type === NodeType.MCP) {
const subAgentId = params.source;
if (edges.some((edge) => edge.target === targetNode.id)) {
toast.error('This MCP tool is already connected. Remove the existing connection first.');
return;
}
updateNodeData(targetNode.id, {
...targetNode.data,
subAgentId,
relationshipId: null, // Will be set after saving to database
subAgentId: params.source,
});
}
}
Expand Down
18 changes: 8 additions & 10 deletions agents-manage-ui/src/components/agent/nodes/mcp-node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,13 @@ const TruncateToolBadge: FC<{
};

export function MCPNode(props: NodeProps & { data: MCPNodeData }) {
'use memo';

const { data, selected } = props;
const { tenantId, projectId } = useParams<{ tenantId: string; projectId: string }>();
const { toolLookup, agentToolConfigLookup, edges } = useAgentStore((state) => ({
const { toolLookup, agentToolConfigLookup } = useAgentStore((state) => ({
toolLookup: state.toolLookup,
agentToolConfigLookup: state.agentToolConfigLookup,
edges: state.edges,
}));

// Get skeleton data from initial page load (status: 'unknown', availableTools: [])
Expand All @@ -85,15 +86,13 @@ export function MCPNode(props: NodeProps & { data: MCPNodeData }) {
const name = data.name || `Tool: ${data.toolId}`;
const imageUrl = data.imageUrl ?? toolData?.imageUrl;

const availableTools = toolData?.availableTools;

const activeTools = getActiveTools({
availableTools: availableTools,
availableTools: toolData?.availableTools,
activeTools: toolData?.config?.type === 'mcp' ? toolData.config.mcp.activeTools : undefined,
});

const selectedTools = getCurrentSelectedToolsForNode(props, agentToolConfigLookup, edges);
const toolPolicies = getCurrentToolPoliciesForNode(props, agentToolConfigLookup, edges);
const selectedTools = getCurrentSelectedToolsForNode(props, agentToolConfigLookup);
const toolPolicies = getCurrentToolPoliciesForNode(props, agentToolConfigLookup);

const orphanedTools = findOrphanedTools(selectedTools, activeTools);
const hasOrphanedTools = orphanedTools.length > 0;
Expand All @@ -116,7 +115,7 @@ export function MCPNode(props: NodeProps & { data: MCPNodeData }) {
const totalCount = activeTools?.length ?? 0;

if (selectedCount === 0) {
return ['0'];
return [];
}

// If all tools are selected, show total count
Expand All @@ -142,8 +141,7 @@ export function MCPNode(props: NodeProps & { data: MCPNodeData }) {
};

const toolBadges = getToolDisplay().map((label) => {
const isSynthetic =
label === '0' || label.startsWith('+') || label.endsWith('(ALL)') || label.includes('(ALL)');
const isSynthetic = label.startsWith('+') || label.includes('(ALL)');

return {
label,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,7 @@ export function MCPServerNodeEditor({
const { markUnsaved } = useAgentActions();

// Get skeleton data from store
const { toolLookup, edges } = useAgentStore((state) => ({
toolLookup: state.toolLookup,
edges: state.edges,
}));
const toolLookup = useAgentStore((state) => state.toolLookup);

// Lazy-load actual tool status
const { data: liveToolData, isLoading: isLoadingToolStatus } = useMcpToolStatusQuery({
Expand All @@ -71,8 +68,8 @@ export function MCPServerNodeEditor({
const toolData = liveToolData ?? skeletonToolData;

const getCurrentHeaders = useCallback((): Record<string, string> => {
return getCurrentHeadersForNode(selectedNode, agentToolConfigLookup, edges);
}, [selectedNode, agentToolConfigLookup, edges]);
return getCurrentHeadersForNode(selectedNode, agentToolConfigLookup);
}, [selectedNode, agentToolConfigLookup]);

// Local state for headers input (allows invalid JSON while typing)
const [headersInputValue, setHeadersInputValue] = useState('{}');
Expand All @@ -94,14 +91,8 @@ export function MCPServerNodeEditor({
: undefined,
});

const selectedTools = getCurrentSelectedToolsForNode(selectedNode, agentToolConfigLookup, edges);

const currentToolPolicies = getCurrentToolPoliciesForNode(
selectedNode,
agentToolConfigLookup,
edges
);

const selectedTools = getCurrentSelectedToolsForNode(selectedNode, agentToolConfigLookup);
const currentToolPolicies = getCurrentToolPoliciesForNode(selectedNode, agentToolConfigLookup);
const orphanedTools = findOrphanedTools(selectedTools, activeTools);

// Track if we've already shown the warning for this node to avoid repeated toasts
Expand Down
22 changes: 9 additions & 13 deletions agents-manage-ui/src/lib/utils/orphaned-tools-detector.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { Edge } from '@xyflow/react';
import type { MCPNodeData } from '@/components/agent/configuration/node-types';

export interface ActiveTool {
Expand Down Expand Up @@ -27,16 +26,15 @@ export function getCurrentSelectedToolsForNode(
agentToolConfigLookup: Record<
string,
Record<string, { toolId: string; toolSelection?: string[] | null }>
>,
_edges: Edge[]
>
): string[] | null {
// First check if we have temporary selections stored on the node (from recent clicks)
if ((node.data as any).tempSelectedTools !== undefined) {
if (node.data.tempSelectedTools !== undefined) {
return (node.data as any).tempSelectedTools;
}

// If node has relationshipId, find config by relationshipId
const relationshipId = (node.data as any).relationshipId;
const { relationshipId } = node.data;
if (relationshipId) {
for (const toolsMap of Object.values(agentToolConfigLookup)) {
const config = toolsMap[relationshipId];
Expand All @@ -58,16 +56,15 @@ export function getCurrentHeadersForNode(
agentToolConfigLookup: Record<
string,
Record<string, { toolId: string; headers?: Record<string, string> }>
>,
_edges: Edge[]
>
): Record<string, string> {
// First check if we have temporary headers stored on the node (from recent edits)
if ((node.data as any).tempHeaders !== undefined) {
if (node.data.tempHeaders !== undefined) {
return (node.data as any).tempHeaders;
}

// If node has relationshipId, find config by relationshipId
const relationshipId = (node.data as any).relationshipId;
const { relationshipId } = node.data;
if (relationshipId) {
for (const toolsMap of Object.values(agentToolConfigLookup)) {
const config = toolsMap[relationshipId];
Expand All @@ -89,16 +86,15 @@ export function getCurrentToolPoliciesForNode(
agentToolConfigLookup: Record<
string,
Record<string, { toolId: string; toolPolicies?: Record<string, { needsApproval?: boolean }> }>
>,
_edges: Edge[]
>
): Record<string, { needsApproval?: boolean }> {
// First check if we have temporary toolPolicies stored on the node (from recent edits)
if ((node.data as any).tempToolPolicies !== undefined) {
if (node.data.tempToolPolicies !== undefined) {
return (node.data as any).tempToolPolicies;
}

// If node has relationshipId, find config by relationshipId
const relationshipId = (node.data as any).relationshipId;
const { relationshipId } = node.data;
if (relationshipId) {
for (const toolsMap of Object.values(agentToolConfigLookup)) {
const config = toolsMap[relationshipId];
Expand Down
Loading