fix: Restore MCP tool dropdown visibility when adding component from sidebar (#12550)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Cristhian Zanforlin Lousa
2026-04-07 19:48:06 -03:00
committed by GitHub
parent ca5fe56e1b
commit 23eebd037d
8 changed files with 111 additions and 64 deletions

File diff suppressed because one or more lines are too long

View File

@ -198,7 +198,10 @@ class TestGetEnabledProvidersForUser:
mock_session = MagicMock()
with patch("langflow.agentic.services.provider_service.get_variable_service", return_value=mock_db_service):
with (
patch("langflow.agentic.services.provider_service.get_variable_service", return_value=mock_db_service),
patch("langflow.agentic.services.provider_service.os.getenv", return_value=None),
):
enabled_providers, provider_status = await get_enabled_providers_for_user("user-1", mock_session)
assert enabled_providers == []
@ -225,6 +228,7 @@ class TestGetEnabledProvidersForUser:
"langflow.agentic.services.provider_service.get_model_provider_variable_mapping",
return_value={"Anthropic": "ANTHROPIC_API_KEY", "OpenAI": "OPENAI_API_KEY"},
),
patch("langflow.agentic.services.provider_service.os.getenv", return_value=None),
):
enabled, status = await get_enabled_providers_for_user("user-1", mock_session)
@ -272,7 +276,10 @@ class TestGetEnabledProvidersForUser:
mock_session = MagicMock()
with patch("langflow.agentic.services.provider_service.get_variable_service", return_value=mock_db_service):
with (
patch("langflow.agentic.services.provider_service.get_variable_service", return_value=mock_db_service),
patch("langflow.agentic.services.provider_service.os.getenv", return_value=None),
):
enabled_providers, provider_status = await get_enabled_providers_for_user("user-1", mock_session)
assert enabled_providers == []

View File

@ -152,5 +152,10 @@ class TestMCPComponentFlowReload(ComponentTestBaseWithoutClient):
updated_config = await component.update_build_config(build_config, new_server_value, "mcp_server")
# Assert — options must be cleared (new server has different tools)
# The backend now attempts to fetch tools immediately; since "new_server"
# doesn't exist in the database, it will fail with an error placeholder.
assert updated_config["tool"]["options"] == []
assert updated_config["tool"]["placeholder"] == "Loading tools..."
assert updated_config["tool"]["placeholder"] in (
"Error on MCP Server",
"Timeout on MCP server",
)

View File

@ -42,6 +42,7 @@ const useFetchDataOnMount = (
const shouldFetchOnMount =
isRealtimeOrRefresh &&
((!hasOptions && fieldSupportsOptions) ||
(!fieldSupportsOptions && !!template.value) ||
(name === "api_key" && !template.value) ||
needApiKeyPrefill);

View File

@ -238,7 +238,7 @@ test(
await page.waitForSelector(
'[data-testid="add-component-button-lf-starter_project"]',
{
timeout: 30000,
timeout: 60000,
},
);
await page.getByTestId("add-component-button-lf-starter_project").click();
@ -261,14 +261,20 @@ test(
await page.getByTestId("add-mcp-server-button").click();
// Wait for the modal overlay to fully close before interacting
await page
.locator(".fixed.inset-0.z-50")
.waitFor({ state: "hidden", timeout: 10000 })
.catch(() => {});
await expect(page.getByTestId("dropdown_str_tool")).toBeVisible({
timeout: 30000,
timeout: 60000,
});
await page.waitForSelector(
'[data-testid="dropdown_str_tool"]:not([disabled])',
{
timeout: 30000,
timeout: 60000,
state: "visible",
},
);
@ -278,6 +284,7 @@ test(
// Verify that tools are available in the dropdown
// The dropdown should show tool options (the action_name rename may not appear here)
const toolOptions = page.locator('[data-testid*="-option"]');
await expect(toolOptions.first()).toBeVisible({ timeout: 30000 });
const toolCount = await toolOptions.count();
expect(toolCount).toBeGreaterThan(0);

View File

@ -61,25 +61,29 @@ test(
await page.getByTestId("add-mcp-server-button").click();
await page.waitForTimeout(5000);
// Wait for the modal overlay to fully close
await page
.locator(".fixed.inset-0.z-50")
.waitFor({ state: "hidden", timeout: 10000 })
.catch(() => {});
await expect(page.getByTestId("dropdown_str_tool")).toBeVisible({
timeout: 30000,
timeout: 60000,
});
await page.waitForSelector(
'[data-testid="dropdown_str_tool"]:not([disabled])',
{
timeout: 30000,
timeout: 60000,
state: "visible",
},
);
await page.getByTestId("dropdown_str_tool").click();
const fetchOptionCount = await page.getByTestId("fetch-0-option").count();
expect(fetchOptionCount).toBeGreaterThan(0);
await expect(page.getByTestId("fetch-0-option")).toBeVisible({
timeout: 30000,
});
await page.getByTestId("fetch-0-option").click();
@ -240,22 +244,24 @@ test(
.getByTestId(`add-component-button-${testName}`)
.click({ timeout: 30000 });
await page.waitForTimeout(5000);
await expect(page.getByTestId("dropdown_str_tool")).toBeVisible({
timeout: 30000,
timeout: 60000,
});
await page.waitForSelector(
'[data-testid="dropdown_str_tool"]:not([disabled])',
{
timeout: 30000,
timeout: 60000,
state: "visible",
},
);
await page.getByTestId("dropdown_str_tool").click();
await expect(page.getByTestId("fetch-0-option")).toBeVisible({
timeout: 30000,
});
const fetchOptionCount = await page.getByTestId("fetch-0-option").count();
expect(fetchOptionCount).toBeGreaterThan(0);
@ -715,10 +721,16 @@ test(
timeout: 30000,
});
// Wait for the modal overlay to fully close
await page
.locator(".fixed.inset-0.z-50")
.waitFor({ state: "hidden", timeout: 10000 })
.catch(() => {});
await page.waitForSelector(
'[data-testid="dropdown_str_tool"]:not([disabled])',
{
timeout: 30000,
timeout: 60000,
state: "visible",
},
);
@ -727,13 +739,9 @@ test(
await page.waitForSelector('[data-testid="fetch-0-option"]', {
state: "visible",
timeout: 10000,
timeout: 30000,
});
const fetchOptionCount = await page.getByTestId("fetch-0-option").count();
expect(fetchOptionCount).toBeGreaterThan(0);
await page.getByTestId("fetch-0-option").click();
// Wait for canvas controls to be visible before adjusting view
@ -854,7 +862,7 @@ test(
await page.waitForSelector(
'[data-testid="dropdown_str_tool"]:not([disabled])',
{
timeout: 30000,
timeout: 60000,
state: "visible",
},
);
@ -863,7 +871,7 @@ test(
await page.waitForSelector('[data-testid="get_current_time-0-option"]', {
state: "visible",
timeout: 10000,
timeout: 30000,
});
const timeOptionCount = await page
@ -964,7 +972,7 @@ test(
await page.waitForSelector(
'[data-testid="dropdown_str_tool"]:not([disabled])',
{
timeout: 30000,
timeout: 60000,
state: "visible",
},
);
@ -973,7 +981,7 @@ test(
await page.waitForSelector('[data-testid="fetch-0-option"]', {
state: "visible",
timeout: 10000,
timeout: 30000,
});
const fetchOptionCount2 = await page.getByTestId("fetch-0-option").count();
@ -1026,46 +1034,33 @@ test(
await page.getByTestId("add-mcp-server-button").click();
// Wait for the modal overlay to fully close before interacting
await page
.locator(".fixed.inset-0.z-50")
.waitFor({ state: "hidden", timeout: 10000 })
.catch(() => {});
// Wait for tools to load with proper timeout (external server can be slow in CI)
await page.waitForSelector(
'[data-testid="dropdown_str_tool"]:not([disabled])',
{
timeout: 30000,
timeout: 60000,
state: "visible",
},
);
await page.getByTestId("dropdown_str_tool").click();
// Check for tools from server
const toolOptions = page.locator('[data-testid*="-option"]');
// Check for tools from server - wait for any option to render
const toolOptions = page.locator('[data-testid*="-0-option"]');
await expect(toolOptions.first()).toBeVisible({ timeout: 30000 });
// Verify multiple tools loaded from deepwiki
const toolCount = await toolOptions.count();
expect(toolCount).toBeGreaterThan(0);
// server-everything should have multiple tools (at least 5+)
expect(toolCount).toBeGreaterThan(5);
// Verify specific tools exist from server-everything
const readWikiStructureOption = page.getByTestId(
"read_wiki_structure-0-option",
);
expect(await readWikiStructureOption.count()).toBeGreaterThan(0);
// Select the option to verify it loads properly
await readWikiStructureOption.last().click();
// Wait for the tool input field to appear
await page.waitForSelector(
'[data-testid="popover-anchor-input-repoName"]',
{
state: "visible",
timeout: 30000,
},
);
// Verify the input field is present
await expect(
page.getByTestId("popover-anchor-input-repoName"),
).toBeVisible();
// Select the first available tool
await toolOptions.first().click();
},
);

File diff suppressed because one or more lines are too long

View File

@ -143,6 +143,7 @@ class MCPToolsComponent(ComponentWithCache):
show=False,
required=True,
real_time_refresh=True,
refresh_button=True,
),
MessageTextInput(
name="tool_placeholder",
@ -505,9 +506,26 @@ class MCPToolsComponent(ComponentWithCache):
build_config["tool"]["options"] = [tool.name for tool in cached_tools]
build_config["tool"]["placeholder"] = "Select a tool"
else:
# Show loading state only when we need to fetch tools
build_config["tool"]["placeholder"] = "Loading tools..."
build_config["tool"]["options"] = []
# Actually fetch tools now instead of deferring to a frontend callback.
# The frontend has no reliable mechanism to trigger a second
# update_build_config call for the "tool" field after this response,
# so we must populate the options here.
try:
self.tools, build_config["mcp_server"]["value"] = await self.update_tool_list(
mcp_server_value=field_value
)
build_config["tool"]["options"] = [tool.name for tool in self.tools]
build_config["tool"]["placeholder"] = "Select a tool"
except (TimeoutError, asyncio.TimeoutError) as e:
msg = f"Timeout loading tools for MCP server: {e!s}"
await logger.awarning(msg)
build_config["tool"]["options"] = []
build_config["tool"]["placeholder"] = "Timeout on MCP server"
except (ValueError, ImportError, ConnectionError, OSError, RuntimeError) as e:
msg = f"Error loading tools for MCP server: {e!s}"
await logger.awarning(msg)
build_config["tool"]["options"] = []
build_config["tool"]["placeholder"] = "Error on MCP Server"
# Force a value refresh only when the user genuinely switched servers.
# server_changed is only True for real user-initiated changes (not initial load).
if server_changed:
@ -526,9 +544,22 @@ class MCPToolsComponent(ComponentWithCache):
self._not_load_actions = True
else:
build_config["tool"]["value"] = uuid.uuid4()
build_config["tool"]["options"] = []
build_config["tool"]["show"] = True
build_config["tool"]["placeholder"] = "Loading tools..."
# Fetch tools immediately instead of showing "Loading tools..."
try:
self.tools, build_config["mcp_server"]["value"] = await self.update_tool_list()
build_config["tool"]["options"] = [tool.name for tool in self.tools]
build_config["tool"]["placeholder"] = "Select a tool"
except (TimeoutError, asyncio.TimeoutError) as e:
msg = f"Timeout loading tools when toggling tool mode: {e!s}"
await logger.awarning(msg)
build_config["tool"]["options"] = []
build_config["tool"]["placeholder"] = "Timeout on MCP server"
except (ValueError, ImportError, ConnectionError, OSError, RuntimeError) as e:
msg = f"Error loading tools when toggling tool mode: {e!s}"
await logger.awarning(msg)
build_config["tool"]["options"] = []
build_config["tool"]["placeholder"] = "Error on MCP Server"
elif field_name == "tools_metadata":
self._not_load_actions = False