Skip to content

Commit 23eebd0

Browse files
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>
1 parent ca5fe56 commit 23eebd0

8 files changed

Lines changed: 111 additions & 64 deletions

File tree

src/backend/base/langflow/initial_setup/starter_projects/Nvidia Remix.json

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

src/backend/tests/unit/agentic/services/test_provider_service.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,10 @@ async def test_should_return_empty_when_no_credentials(self):
198198

199199
mock_session = MagicMock()
200200

201-
with patch("langflow.agentic.services.provider_service.get_variable_service", return_value=mock_db_service):
201+
with (
202+
patch("langflow.agentic.services.provider_service.get_variable_service", return_value=mock_db_service),
203+
patch("langflow.agentic.services.provider_service.os.getenv", return_value=None),
204+
):
202205
enabled_providers, provider_status = await get_enabled_providers_for_user("user-1", mock_session)
203206

204207
assert enabled_providers == []
@@ -225,6 +228,7 @@ async def test_should_return_enabled_providers_with_credentials(self):
225228
"langflow.agentic.services.provider_service.get_model_provider_variable_mapping",
226229
return_value={"Anthropic": "ANTHROPIC_API_KEY", "OpenAI": "OPENAI_API_KEY"},
227230
),
231+
patch("langflow.agentic.services.provider_service.os.getenv", return_value=None),
228232
):
229233
enabled, status = await get_enabled_providers_for_user("user-1", mock_session)
230234

@@ -272,7 +276,10 @@ async def test_should_return_empty_when_get_all_returns_empty(self):
272276

273277
mock_session = MagicMock()
274278

275-
with patch("langflow.agentic.services.provider_service.get_variable_service", return_value=mock_db_service):
279+
with (
280+
patch("langflow.agentic.services.provider_service.get_variable_service", return_value=mock_db_service),
281+
patch("langflow.agentic.services.provider_service.os.getenv", return_value=None),
282+
):
276283
enabled_providers, provider_status = await get_enabled_providers_for_user("user-1", mock_session)
277284

278285
assert enabled_providers == []

src/backend/tests/unit/components/models_and_agents/test_mcp_component_flow_reload.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,5 +152,10 @@ async def test_should_clear_tool_options_when_server_genuinely_changes(
152152
updated_config = await component.update_build_config(build_config, new_server_value, "mcp_server")
153153

154154
# Assert — options must be cleared (new server has different tools)
155+
# The backend now attempts to fetch tools immediately; since "new_server"
156+
# doesn't exist in the database, it will fail with an error placeholder.
155157
assert updated_config["tool"]["options"] == []
156-
assert updated_config["tool"]["placeholder"] == "Loading tools..."
158+
assert updated_config["tool"]["placeholder"] in (
159+
"Error on MCP Server",
160+
"Timeout on MCP server",
161+
)

src/frontend/src/CustomNodes/hooks/use-fetch-data-on-mount.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const useFetchDataOnMount = (
4242
const shouldFetchOnMount =
4343
isRealtimeOrRefresh &&
4444
((!hasOptions && fieldSupportsOptions) ||
45+
(!fieldSupportsOptions && !!template.value) ||
4546
(name === "api_key" && !template.value) ||
4647
needApiKeyPrefill);
4748

src/frontend/tests/extended/features/mcp-server-tab.spec.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ test(
238238
await page.waitForSelector(
239239
'[data-testid="add-component-button-lf-starter_project"]',
240240
{
241-
timeout: 30000,
241+
timeout: 60000,
242242
},
243243
);
244244
await page.getByTestId("add-component-button-lf-starter_project").click();
@@ -261,14 +261,20 @@ test(
261261

262262
await page.getByTestId("add-mcp-server-button").click();
263263

264+
// Wait for the modal overlay to fully close before interacting
265+
await page
266+
.locator(".fixed.inset-0.z-50")
267+
.waitFor({ state: "hidden", timeout: 10000 })
268+
.catch(() => {});
269+
264270
await expect(page.getByTestId("dropdown_str_tool")).toBeVisible({
265-
timeout: 30000,
271+
timeout: 60000,
266272
});
267273

268274
await page.waitForSelector(
269275
'[data-testid="dropdown_str_tool"]:not([disabled])',
270276
{
271-
timeout: 30000,
277+
timeout: 60000,
272278
state: "visible",
273279
},
274280
);
@@ -278,6 +284,7 @@ test(
278284
// Verify that tools are available in the dropdown
279285
// The dropdown should show tool options (the action_name rename may not appear here)
280286
const toolOptions = page.locator('[data-testid*="-option"]');
287+
await expect(toolOptions.first()).toBeVisible({ timeout: 30000 });
281288
const toolCount = await toolOptions.count();
282289

283290
expect(toolCount).toBeGreaterThan(0);

src/frontend/tests/extended/features/mcp-server.spec.ts

Lines changed: 43 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -61,25 +61,29 @@ test(
6161

6262
await page.getByTestId("add-mcp-server-button").click();
6363

64-
await page.waitForTimeout(5000);
64+
// Wait for the modal overlay to fully close
65+
await page
66+
.locator(".fixed.inset-0.z-50")
67+
.waitFor({ state: "hidden", timeout: 10000 })
68+
.catch(() => {});
6569

6670
await expect(page.getByTestId("dropdown_str_tool")).toBeVisible({
67-
timeout: 30000,
71+
timeout: 60000,
6872
});
6973

7074
await page.waitForSelector(
7175
'[data-testid="dropdown_str_tool"]:not([disabled])',
7276
{
73-
timeout: 30000,
77+
timeout: 60000,
7478
state: "visible",
7579
},
7680
);
7781

7882
await page.getByTestId("dropdown_str_tool").click();
7983

80-
const fetchOptionCount = await page.getByTestId("fetch-0-option").count();
81-
82-
expect(fetchOptionCount).toBeGreaterThan(0);
84+
await expect(page.getByTestId("fetch-0-option")).toBeVisible({
85+
timeout: 30000,
86+
});
8387

8488
await page.getByTestId("fetch-0-option").click();
8589

@@ -240,22 +244,24 @@ test(
240244
.getByTestId(`add-component-button-${testName}`)
241245
.click({ timeout: 30000 });
242246

243-
await page.waitForTimeout(5000);
244-
245247
await expect(page.getByTestId("dropdown_str_tool")).toBeVisible({
246-
timeout: 30000,
248+
timeout: 60000,
247249
});
248250

249251
await page.waitForSelector(
250252
'[data-testid="dropdown_str_tool"]:not([disabled])',
251253
{
252-
timeout: 30000,
254+
timeout: 60000,
253255
state: "visible",
254256
},
255257
);
256258

257259
await page.getByTestId("dropdown_str_tool").click();
258260

261+
await expect(page.getByTestId("fetch-0-option")).toBeVisible({
262+
timeout: 30000,
263+
});
264+
259265
const fetchOptionCount = await page.getByTestId("fetch-0-option").count();
260266

261267
expect(fetchOptionCount).toBeGreaterThan(0);
@@ -715,10 +721,16 @@ test(
715721
timeout: 30000,
716722
});
717723

724+
// Wait for the modal overlay to fully close
725+
await page
726+
.locator(".fixed.inset-0.z-50")
727+
.waitFor({ state: "hidden", timeout: 10000 })
728+
.catch(() => {});
729+
718730
await page.waitForSelector(
719731
'[data-testid="dropdown_str_tool"]:not([disabled])',
720732
{
721-
timeout: 30000,
733+
timeout: 60000,
722734
state: "visible",
723735
},
724736
);
@@ -727,13 +739,9 @@ test(
727739

728740
await page.waitForSelector('[data-testid="fetch-0-option"]', {
729741
state: "visible",
730-
timeout: 10000,
742+
timeout: 30000,
731743
});
732744

733-
const fetchOptionCount = await page.getByTestId("fetch-0-option").count();
734-
735-
expect(fetchOptionCount).toBeGreaterThan(0);
736-
737745
await page.getByTestId("fetch-0-option").click();
738746

739747
// Wait for canvas controls to be visible before adjusting view
@@ -854,7 +862,7 @@ test(
854862
await page.waitForSelector(
855863
'[data-testid="dropdown_str_tool"]:not([disabled])',
856864
{
857-
timeout: 30000,
865+
timeout: 60000,
858866
state: "visible",
859867
},
860868
);
@@ -863,7 +871,7 @@ test(
863871

864872
await page.waitForSelector('[data-testid="get_current_time-0-option"]', {
865873
state: "visible",
866-
timeout: 10000,
874+
timeout: 30000,
867875
});
868876

869877
const timeOptionCount = await page
@@ -964,7 +972,7 @@ test(
964972
await page.waitForSelector(
965973
'[data-testid="dropdown_str_tool"]:not([disabled])',
966974
{
967-
timeout: 30000,
975+
timeout: 60000,
968976
state: "visible",
969977
},
970978
);
@@ -973,7 +981,7 @@ test(
973981

974982
await page.waitForSelector('[data-testid="fetch-0-option"]', {
975983
state: "visible",
976-
timeout: 10000,
984+
timeout: 30000,
977985
});
978986

979987
const fetchOptionCount2 = await page.getByTestId("fetch-0-option").count();
@@ -1026,46 +1034,33 @@ test(
10261034

10271035
await page.getByTestId("add-mcp-server-button").click();
10281036

1037+
// Wait for the modal overlay to fully close before interacting
1038+
await page
1039+
.locator(".fixed.inset-0.z-50")
1040+
.waitFor({ state: "hidden", timeout: 10000 })
1041+
.catch(() => {});
1042+
10291043
// Wait for tools to load with proper timeout (external server can be slow in CI)
10301044
await page.waitForSelector(
10311045
'[data-testid="dropdown_str_tool"]:not([disabled])',
10321046
{
1033-
timeout: 30000,
1047+
timeout: 60000,
10341048
state: "visible",
10351049
},
10361050
);
10371051

10381052
await page.getByTestId("dropdown_str_tool").click();
10391053

1040-
// Check for tools from server
1041-
const toolOptions = page.locator('[data-testid*="-option"]');
1042-
const toolCount = await toolOptions.count();
1043-
1044-
// server-everything should have multiple tools (at least 5+)
1045-
expect(toolCount).toBeGreaterThan(5);
1054+
// Check for tools from server - wait for any option to render
1055+
const toolOptions = page.locator('[data-testid*="-0-option"]');
1056+
await expect(toolOptions.first()).toBeVisible({ timeout: 30000 });
10461057

1047-
// Verify specific tools exist from server-everything
1048-
const readWikiStructureOption = page.getByTestId(
1049-
"read_wiki_structure-0-option",
1050-
);
1051-
expect(await readWikiStructureOption.count()).toBeGreaterThan(0);
1052-
1053-
// Select the option to verify it loads properly
1054-
await readWikiStructureOption.last().click();
1055-
1056-
// Wait for the tool input field to appear
1057-
await page.waitForSelector(
1058-
'[data-testid="popover-anchor-input-repoName"]',
1059-
{
1060-
state: "visible",
1061-
timeout: 30000,
1062-
},
1063-
);
1058+
// Verify multiple tools loaded from deepwiki
1059+
const toolCount = await toolOptions.count();
1060+
expect(toolCount).toBeGreaterThan(0);
10641061

1065-
// Verify the input field is present
1066-
await expect(
1067-
page.getByTestId("popover-anchor-input-repoName"),
1068-
).toBeVisible();
1062+
// Select the first available tool
1063+
await toolOptions.first().click();
10691064
},
10701065
);
10711066

src/lfx/src/lfx/_assets/component_index.json

Lines changed: 4 additions & 3 deletions
Large diffs are not rendered by default.

src/lfx/src/lfx/components/models_and_agents/mcp_component.py

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ def _ensure_cache_structure(self):
143143
show=False,
144144
required=True,
145145
real_time_refresh=True,
146+
refresh_button=True,
146147
),
147148
MessageTextInput(
148149
name="tool_placeholder",
@@ -505,9 +506,26 @@ async def update_build_config(self, build_config: dict, field_value: str, field_
505506
build_config["tool"]["options"] = [tool.name for tool in cached_tools]
506507
build_config["tool"]["placeholder"] = "Select a tool"
507508
else:
508-
# Show loading state only when we need to fetch tools
509-
build_config["tool"]["placeholder"] = "Loading tools..."
510-
build_config["tool"]["options"] = []
509+
# Actually fetch tools now instead of deferring to a frontend callback.
510+
# The frontend has no reliable mechanism to trigger a second
511+
# update_build_config call for the "tool" field after this response,
512+
# so we must populate the options here.
513+
try:
514+
self.tools, build_config["mcp_server"]["value"] = await self.update_tool_list(
515+
mcp_server_value=field_value
516+
)
517+
build_config["tool"]["options"] = [tool.name for tool in self.tools]
518+
build_config["tool"]["placeholder"] = "Select a tool"
519+
except (TimeoutError, asyncio.TimeoutError) as e:
520+
msg = f"Timeout loading tools for MCP server: {e!s}"
521+
await logger.awarning(msg)
522+
build_config["tool"]["options"] = []
523+
build_config["tool"]["placeholder"] = "Timeout on MCP server"
524+
except (ValueError, ImportError, ConnectionError, OSError, RuntimeError) as e:
525+
msg = f"Error loading tools for MCP server: {e!s}"
526+
await logger.awarning(msg)
527+
build_config["tool"]["options"] = []
528+
build_config["tool"]["placeholder"] = "Error on MCP Server"
511529
# Force a value refresh only when the user genuinely switched servers.
512530
# server_changed is only True for real user-initiated changes (not initial load).
513531
if server_changed:
@@ -526,9 +544,22 @@ async def update_build_config(self, build_config: dict, field_value: str, field_
526544
self._not_load_actions = True
527545
else:
528546
build_config["tool"]["value"] = uuid.uuid4()
529-
build_config["tool"]["options"] = []
530547
build_config["tool"]["show"] = True
531-
build_config["tool"]["placeholder"] = "Loading tools..."
548+
# Fetch tools immediately instead of showing "Loading tools..."
549+
try:
550+
self.tools, build_config["mcp_server"]["value"] = await self.update_tool_list()
551+
build_config["tool"]["options"] = [tool.name for tool in self.tools]
552+
build_config["tool"]["placeholder"] = "Select a tool"
553+
except (TimeoutError, asyncio.TimeoutError) as e:
554+
msg = f"Timeout loading tools when toggling tool mode: {e!s}"
555+
await logger.awarning(msg)
556+
build_config["tool"]["options"] = []
557+
build_config["tool"]["placeholder"] = "Timeout on MCP server"
558+
except (ValueError, ImportError, ConnectionError, OSError, RuntimeError) as e:
559+
msg = f"Error loading tools when toggling tool mode: {e!s}"
560+
await logger.awarning(msg)
561+
build_config["tool"]["options"] = []
562+
build_config["tool"]["placeholder"] = "Error on MCP Server"
532563
elif field_name == "tools_metadata":
533564
self._not_load_actions = False
534565

0 commit comments

Comments
 (0)