mirror of
https://github.com/langflow-ai/langflow.git
synced 2026-07-25 15:49:14 +08:00
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:
committed by
GitHub
parent
ca5fe56e1b
commit
23eebd037d
File diff suppressed because one or more lines are too long
@ -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 == []
|
||||
|
||||
@ -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",
|
||||
)
|
||||
|
||||
@ -42,6 +42,7 @@ const useFetchDataOnMount = (
|
||||
const shouldFetchOnMount =
|
||||
isRealtimeOrRefresh &&
|
||||
((!hasOptions && fieldSupportsOptions) ||
|
||||
(!fieldSupportsOptions && !!template.value) ||
|
||||
(name === "api_key" && !template.value) ||
|
||||
needApiKeyPrefill);
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user