feat: add servers persistence to MCP connection component, add MCP connections settings page (#8388)

* Added mcpinput to the backend

* Fixed list selection component to contain descriptions

* Added mcp component in the frontend with mock values

* Added mcp rendering on Parameter Render Component

* Changed input to be more concise and to have dynamic placeholder

* Added header search placeholder

* Fixed styling to match new input

* Removed unused params

* Adds AddMcpServerModal's first mock version

* Adds Add button on mcp component and list selection component

* First pass at mcp api

* Add PATCH endpoint

* Add DELETE endpoint

* fix: Bump version numbers for langflow and langflow-base to 1.4.3 and 0.4.3 respectively

* fix: Remove Igor Carvalho from maintainers list in pyproject.toml

* fix(agent): reset model list when provider changes

Switching the provider in the Agent component sometimes left models from
the previous provider visible/selected.
We now filter  against the new , ensuring only
models that belong to the active provider remain.

* src/frontend/src/components/core/dropdownComponent/index.tsx
  – add  guard when rebuilding

* tests/extended/regression/general-bugs-dropdown-select-not-in-list.spec.ts
  – expand coverage for “model not in list” edge-cases

Co-authored-by: Cristian Lousa <cristian.lousa@gmail.com>

* fix: Update Pokédex Agent template (#8373)

* Implement adding and getting MCP servers, implemented addMcpServerModal

* Added sse and stdio ways of adding a server

* Added no actions handling

* added new mcp type to constants

* Added headers to add mcp server modal

* Changed mcp component to allow persistent mcp servers

* fix input list component gradient

* fix add server modal to patch when initial data is present, and to clean variables when switching tabs

* changed message on add mcp server

* Added required mutations for mcp page

* Added mcp servers page

* Changed design of page

* Fixed delete problems and added delete confirmation

* fixed wrong error parsing

* changed padding

* Made added server be used on mcp component

* refactor: remove references to the langflow store (#8354)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: KimberlyFields <46325568+KimberlyFields@users.noreply.github.com>
Co-authored-by: Ítalo Johnny <italojohnnydosanjos@gmail.com>
Co-authored-by: Mendon Kissling <59585235+mendonk@users.noreply.github.com>
fix: apikey lock issue and add option to disable tracking (#8361)

* Fixed search on sidebar

* fixed infinite use effect

* Fixed error handling

* Fixed tool mode disappearing

* fixed key pair button submitting form

* Fixed bugs

* Added required

* Changed message

* Disabled other tabs when modifying

* Removed tool dropdown if the mcp server is empty

* parsed name

* fixed data test id not applying

* fixed mcp component

* Fixed component not working when only stdio command is present

* refactored tests

* Updated mcp_component to remove old non default keys

* Added data-testids

* Modified tests to include settings page functionality

* [autofix.ci] apply automated fixes

* Refactor out the core part of the mcp

* [autofix.ci] apply automated fixes

* Added placeholders on frontend components for errors

* Fixed bugs with mcp component

* updated bug

* fix: made empty project appear instead of empty flows list when mcp is enabled (#8336)

* try to fix

* Fix MCP persistence

* Update mcp_component.py

* Update mcp.py

* [autofix.ci] apply automated fixes

* fix: Bump version numbers for langflow and langflow-base to 1.4.3 and 0.4.3 respectively

* fix: Remove Igor Carvalho from maintainers list in pyproject.toml

* fix(agent): reset model list when provider changes

Switching the provider in the Agent component sometimes left models from
the previous provider visible/selected.
We now filter  against the new , ensuring only
models that belong to the active provider remain.

* src/frontend/src/components/core/dropdownComponent/index.tsx
  – add  guard when rebuilding

* tests/extended/regression/general-bugs-dropdown-select-not-in-list.spec.ts
  – expand coverage for “model not in list” edge-cases

Co-authored-by: Cristian Lousa <cristian.lousa@gmail.com>

* refactor: remove references to the langflow store (#8354)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: KimberlyFields <46325568+KimberlyFields@users.noreply.github.com>
Co-authored-by: Ítalo Johnny <italojohnnydosanjos@gmail.com>
Co-authored-by: Mendon Kissling <59585235+mendonk@users.noreply.github.com>
fix: apikey lock issue and add option to disable tracking (#8361)

* fix: made empty project appear instead of empty flows list when mcp is enabled (#8336)

* fix mcp client async problems

* fixed mcp sse access

* [autofix.ci] apply automated fixes

* Made values be maintained when refreshing page

* Fixed bugs with tool mode and switching from tool mode to not tool mode

* Update mcp_component.py

* Update test_mcp_component.py

* Don't expose file by name as external endpoint

* Update files.py

* Update files.py

* Add checks for id

* Refactor tests

* Update test_mcp_component.py

* Update test_mcp_component.py

* Update test_mcp_component.py

* updated tests

* re-added placeholder on input for tests to not fail

* updated session selector in order for tests to work

---------

Co-authored-by: Eric Hare <ericrhare@gmail.com>
Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org>
Co-authored-by: Mike Fortman <michael.fortman@datastax.com>
Co-authored-by: Cristian Lousa <cristian.lousa@gmail.com>
Co-authored-by: Edwin Jose <edwin.jose@datastax.com>
Co-authored-by: Deon Sanchez <69873175+deon-sanchez@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: KimberlyFields <46325568+KimberlyFields@users.noreply.github.com>
Co-authored-by: Ítalo Johnny <italojohnnydosanjos@gmail.com>
Co-authored-by: Mendon Kissling <59585235+mendonk@users.noreply.github.com>
This commit is contained in:
Lucas Oliveira 2025-06-11 16:21:38 -03:00 • committed by GitHub
commit 60ccdb500f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
56 changed files with 2381 additions and 876 deletions

View file

@ -199,7 +199,6 @@ test(
// Create a new flow with MCP component
await page.getByTestId("blank-flow").click();
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("mcp connection");
@ -214,57 +213,54 @@ test(
});
await page.getByTestId("fit_view").click();
await zoomOut(page, 3);
// Switch to SSE tab and paste the URL
await page.getByTestId("tab_1_sse").click();
await expect(page.getByTestId("dropdown_str_tool")).toBeHidden();
await page.waitForSelector('[data-testid="textarea_str_sse_url"]', {
try {
await page.getByText("Add MCP Server", { exact: true }).click({
timeout: 5000,
});
} catch (error) {
await page
.getByTestId("mcp-server-dropdown")
.click({ timeout: 3000 });
await page.getByText("Add MCP Server", { exact: true }).click({
timeout: 5000,
});
}
await page.waitForSelector('[data-testid="add-mcp-server-button"]', {
state: "visible",
timeout: 30000,
});
await page.waitForTimeout(2000);
await page.getByTestId("textarea_str_sse_url").fill("");
await page.getByTestId("textarea_str_sse_url").fill(sseUrl);
await page.waitForSelector('[data-testid="json-input"]', {
state: "visible",
timeout: 30000,
});
await page.waitForTimeout(2000);
await page.getByTestId("json-input").fill(configJsonLinux || "");
// Wait for the tools to become available
let attempts = 0;
const maxAttempts = 3;
let dropdownEnabled = false;
await page.getByTestId("add-mcp-server-button").click();
while (attempts < maxAttempts && !dropdownEnabled) {
await page.getByTestId("refresh-button-sse_url").click();
await expect(page.getByTestId("dropdown_str_tool")).toBeVisible({
timeout: 30000,
});
try {
await page.waitForSelector(
'[data-testid="dropdown_str_tool"]:not([disabled])',
{
timeout: 10000,
state: "visible",
},
);
dropdownEnabled = true;
} catch (error) {
attempts++;
console.log(`Retry attempt ${attempts} for refresh button`);
}
}
await page.waitForSelector(
'[data-testid="dropdown_str_tool"]:not([disabled])',
{
timeout: 10000,
state: "visible",
},
);
if (!dropdownEnabled) {
throw new Error(
"Dropdown did not become enabled after multiple refresh attempts",
);
}
// Verify tools are available
await page.waitForTimeout(3000);
await page.getByTestId("dropdown_str_tool").click();
await page.waitForTimeout(3000);
const fetchOptionCount = await page.getByText("mcp_test_name").count();
expect(fetchOptionCount).toBeGreaterThan(0);
// If we get here, the test passed

View file

@ -29,36 +29,48 @@ test(
await zoomOut(page, 3);
await page.getByTestId("dropdown_str_tool").isDisabled();
await expect(page.getByTestId("dropdown_str_tool")).toBeHidden();
let attempts = 0;
const maxAttempts = 3;
let dropdownEnabled = false;
while (attempts < maxAttempts && !dropdownEnabled) {
await page.getByTestId("refresh-button-command").click();
await page.waitForTimeout(3000);
try {
await page.waitForSelector(
'[data-testid="dropdown_str_tool"]:not([disabled])',
{
timeout: 10000,
state: "visible",
},
);
dropdownEnabled = true;
} catch (error) {
attempts++;
console.log(`Retry attempt ${attempts} for refresh button`);
}
try {
await page.getByText("Add MCP Server", { exact: true }).click({
timeout: 5000,
});
} catch (error) {
await page.getByTestId("mcp-server-dropdown").click({ timeout: 3000 });
await page.getByText("Add MCP Server", { exact: true }).click({
timeout: 5000,
});
}
if (!dropdownEnabled) {
throw new Error(
"Dropdown did not become enabled after multiple refresh attempts",
);
}
await page.waitForSelector('[data-testid="add-mcp-server-button"]', {
state: "visible",
timeout: 30000,
});
await page.getByTestId("stdio-tab").click();
await page.waitForSelector('[data-testid="stdio-name-input"]', {
state: "visible",
timeout: 30000,
});
await page.getByTestId("stdio-name-input").fill("test server");
await page.getByTestId("stdio-command-input").fill("uvx mcp-server-fetch");
await page.getByTestId("add-mcp-server-button").click();
await expect(page.getByTestId("dropdown_str_tool")).toBeVisible({
timeout: 30000,
});
await page.waitForSelector(
'[data-testid="dropdown_str_tool"]:not([disabled])',
{
timeout: 10000,
state: "visible",
},
);
await page.getByTestId("dropdown_str_tool").click();
@ -89,124 +101,86 @@ test(
expect(urlOptionCount).toBeGreaterThan(0);
await page.getByTestId("tab_1_sse").click();
await page.getByTestId("user_menu_button").click({ timeout: 3000 });
await page.waitForSelector('[data-testid="textarea_str_sse_url"]', {
await page.getByTestId("menu_settings_button").click({ timeout: 3000 });
await page.waitForSelector('[data-testid="sidebar-nav-MCP Connections"]', {
timeout: 30000,
});
await page
.getByTestId("sidebar-nav-MCP Connections")
.click({ timeout: 3000 });
await page.waitForSelector('[data-testid="add-mcp-server-button-page"]', {
timeout: 3000,
});
await expect(page.getByText("test_server")).toBeVisible({
timeout: 3000,
});
await page
.getByTestId(`mcp-server-menu-button-test_server`)
.click({ timeout: 3000 });
await page
.getByText("Edit", { exact: true })
.first()
.click({ timeout: 3000 });
await page.waitForSelector('[data-testid="add-mcp-server-button"]', {
state: "visible",
timeout: 30000,
});
let sseURLCount = await page.getByTestId("textarea_str_sse_url").count();
expect(sseURLCount).toBeGreaterThan(0);
await page.waitForSelector('[data-testid="dropdown_str_tool"]:disabled', {
timeout: 30000,
await expect(page.getByTestId("json-tab")).toBeDisabled({
timeout: 3000,
});
attempts = 0;
dropdownEnabled = false;
await page.waitForTimeout(1000);
while (attempts < maxAttempts && !dropdownEnabled) {
await page.waitForTimeout(3000);
try {
await page.waitForSelector(
'[data-testid="dropdown_str_tool"]:not([disabled])',
{
timeout: 30000,
state: "visible",
},
);
dropdownEnabled = true;
} catch (error) {
attempts++;
console.log(`Retry attempt ${attempts} for second refresh button`);
await page.getByTestId("refresh-button-sse_url").click();
}
}
if (!dropdownEnabled) {
throw new Error(
"Dropdown did not become enabled after multiple refresh attempts",
);
}
await page.getByTestId("tab_0_stdio").click();
await page.waitForTimeout(2000);
await page.getByTestId("fit_view").click();
attempts = 0;
dropdownEnabled = false;
while (attempts < maxAttempts && !dropdownEnabled) {
await page.getByTestId("refresh-button-command").click();
await page.waitForTimeout(3000);
try {
await page.waitForSelector(
'[data-testid="dropdown_str_tool"]:not([disabled])',
{
timeout: 10000,
state: "visible",
},
);
dropdownEnabled = true;
} catch (error) {
attempts++;
console.log(`Retry attempt ${attempts} for second refresh button`);
}
}
if (!dropdownEnabled) {
throw new Error(
"Dropdown did not become enabled after multiple refresh attempts",
);
}
await page.getByTestId("dropdown_str_tool").click();
fetchOptionCount = await page.getByTestId("fetch-0-option").count();
await page.getByTestId("fetch-0-option").click();
await page.waitForTimeout(2000);
await page.getByTestId("fit_view").click();
expect(fetchOptionCount).toBeGreaterThan(0);
await page.waitForSelector('[data-testid="int_int_max_length"]', {
state: "visible",
timeout: 30000,
await expect(page.getByTestId("stdio-tab")).not.toBeDisabled({
timeout: 3000,
});
maxLengthOptionCount = await page.getByTestId("int_int_max_length").count();
expect(maxLengthOptionCount).toBeGreaterThan(0);
urlOptionCount = await page
.getByTestId("anchor-popover-anchor-input-url")
.count();
expect(urlOptionCount).toBeGreaterThan(0);
await page.getByTestId("tab_1_sse").click();
await page.waitForSelector('[data-testid="textarea_str_sse_url"]', {
state: "visible",
timeout: 30000,
await expect(page.getByTestId("sse-tab")).toBeDisabled({
timeout: 3000,
});
sseURLCount = await page.getByTestId("textarea_str_sse_url").count();
expect(await page.getByTestId("stdio-command-input").inputValue()).toBe(
"uvx mcp-server-fetch",
);
expect(sseURLCount).toBeGreaterThan(0);
await page.getByTestId("add-mcp-server-button").click();
await page.waitForSelector('[data-testid="dropdown_str_tool"]:disabled', {
timeout: 30000,
await page
.getByTestId(`mcp-server-menu-button-test_server`)
.click({ timeout: 3000 });
await page
.getByText("Delete", { exact: true })
.first()
.click({ timeout: 3000 });
await page.waitForSelector(
'[data-testid="btn_delete_delete_confirmation_modal"]',
{
timeout: 3000,
},
);
await page
.getByTestId("btn_delete_delete_confirmation_modal")
.click({ timeout: 3000 });
await page.waitForSelector('[data-testid="add-mcp-server-button-page"]', {
timeout: 3000,
});
await page.waitForTimeout(3000);
await expect(page.getByText("test_server")).not.toBeVisible({
timeout: 3000,
});
},
);